Skip to content

fix: resolve multi-line unindent issue with Shift+Tab#424

Merged
wyu71 merged 1 commit intolinuxdeepin:masterfrom
wyu71:master
Feb 27, 2026
Merged

fix: resolve multi-line unindent issue with Shift+Tab#424
wyu71 merged 1 commit intolinuxdeepin:masterfrom
wyu71:master

Conversation

@wyu71
Copy link
Contributor

@wyu71 wyu71 commented Feb 26, 2026

  • Added UnindentTextCommand class to handle multi-line unindentation
  • Fixed unindentText() to detect and process multi-line selections
  • Added boundary checks to prevent position out of range warnings
  • Preserved text selection state after unindent operation

Log: resolve multi-line unindent issue with Shift+Tab
pms: BUG-351091

Summary by Sourcery

Handle unindenting for both single-line and multi-line text selections while preserving undo/redo behavior.

Bug Fixes:

  • Fix Shift+Tab unindent so it correctly removes indentation across multi-line selections without out-of-range cursor positions.

Enhancements:

  • Add a dedicated UnindentTextCommand with redo/undo support that tracks removed indentation per line and restores the original selection.
  • Keep existing single-line unindent behavior while routing multi-line unindent operations through the undo stack.

- Added UnindentTextCommand class to handle multi-line unindentation
- Fixed unindentText() to detect and process multi-line selections
- Added boundary checks to prevent position out of range warnings
- Preserved text selection state after unindent operation

Log: resolve multi-line unindent issue with Shift+Tab
pms: BUG-351091
@deepin-ci-robot
Copy link

deepin pr auto review

这段代码实现了一个多行文本取消缩进(Unindent)的功能,主要通过新增 UnindentTextCommand 类来支持撤销/重做(Undo/Redo)操作。以下是对代码的语法逻辑、代码质量、性能和安全性方面的详细审查及改进建议:

1. 语法逻辑与代码逻辑

1.1 dtextedit.cpp 中的 unindentText()

  • 逻辑问题:选择范围计算

    • 问题:在 unindentText 中,当有选区时,代码计算了 pos1 (start) 和 pos2 (end) 并交换确保 pos1 < pos2。然后计算了对应的行号 line1line2
    • 潜在风险QTextCursor::blockNumber() 的行为取决于光标位置。如果光标位于行尾(换行符之前或之后),结果可能略有不同。更重要的是,如果用户选择了从第2行末尾到第3行开头,逻辑上应该处理第2行和第3行。目前的逻辑 cursor.setPosition(pos2); int line2 = cursor.blockNumber(); 通常能正确获取包含该位置的块号,但需要确认是否包含空行或跨行选择时的边界情况。
    • 建议:确保在 UnindentTextCommand 构造函数中验证 startline <= endline
  • 逻辑问题:单行与多行处理

    • 观察:代码将原有的单行处理逻辑保留在 else 分支中,而多行处理逻辑在 if (cursor.hasSelection()) 中。
    • 建议UnindentTextCommand 看起来设计为处理多行(通过传入 line1, line2)。理论上,单行操作也可以复用这个 Command 类,只需传入相同的 startpos/endpos 和 startline/endline。这样可以统一逻辑,减少代码重复。

1.2 indenttextcommond.cpp 中的 UnindentTextCommand

  • 逻辑问题:redo() 中的光标位置更新

    • 问题:在 redo() 中,循环处理每一行时使用了 cursor.movePosition(QTextCursor::NextBlock)。如果在文档末尾处理,或者某些行是空行,光标移动行为需要明确。
    • 严重问题:在 redo() 循环内部,每次循环开始时都调用了 cursor.movePosition(QTextCursor::StartOfBlock)。这没问题。但是,循环开始前设置了 cursor.setPosition(m_startpos)。如果 m_startpos 不在行首,第一次循环的 StartOfBlock 会将光标移回该行行首,这是正确的。但是,随后的 NextBlock 依赖于光标当前位置。
    • 潜在风险:如果在删除字符(cursor.removeSelectedText())后,文档布局发生变化,虽然 QTextDocument 会处理位置更新,但连续使用 movePosition 有时在复杂文档中可能不如直接通过 QTextBlock 迭代稳健。
  • 逻辑问题:redo() 中的选区恢复

    • 问题:代码在 redo() 结尾尝试恢复选区。
      if(m_hasselected){
          // ...
          cursor.setPosition(newStartPos);
          cursor.setPosition(newEndPos, QTextCursor::KeepAnchor);
          m_edit->setTextCursor(cursor);
      }
    • 风险newStartPosnewEndPos 的计算是基于删除字符的数量回退的。如果 m_startpos 原本就在行首,且前面没有空格/Tab,newStartPos 可能会变成负数(代码中有 < 0 检查,置为0)。但如果 m_startpos 在行首,newStartPos 置为 0 可能会导致选区跳到文档开头(如果 m_startpos 所在的块不是第一个块),逻辑上这里应该保持 m_startpos 不变,因为该行并未删除字符。
    • 改进m_removedChars 记录了每行删除的字符数。计算 newStartPos 时,应只减去该行实际删除的字符数(即 m_removedChars[0])。如果 m_removedChars[0] 为 0,则 newStartPos 不应改变。目前的代码似乎是这样做的,但要注意边界条件。
  • 逻辑问题:undo() 中的恢复

    • 问题:在 undo() 中,代码遍历行并插入空格或 Tab。
      cursor.setPosition(m_startpos);
      for(int i=m_startline;i<=m_endline;i++){
          cursor.movePosition(QTextCursor::StartOfBlock);
          // ...
          cursor.movePosition(QTextCursor::NextBlock);
      }
    • 问题:与 redo() 类似,这里依赖光标移动。如果在 undo() 过程中文档结构发生变化(虽然不太可能,因为只是插入文本),可能会有问题。
    • 建议:更稳健的方法是使用 QTextDocument::findBlockByNumber() 直接定位到目标块,而不是依赖 NextBlock 的连续移动。

2. 代码质量

  • 命名规范

    • 文件名 indenttextcommond.cpp 拼写错误,应为 command
    • 变量名 m_hasselected 建议改为 m_hasSelection(驼峰命名法)。
    • 变量名 m_pUndoStack(推测在头文件中)使用了匈牙利命名法的前缀 p,而其他成员变量如 m_edit 没有使用。建议统一风格,如 m_undoStack
  • 注释

    • 头文件中的注释 //the start postion of selected text. 有拼写错误 "postion" -> "position"。
    • 建议为 UnindentTextCommand 的构造函数参数和复杂逻辑添加更详细的 Doxygen 风格注释。
  • 日志输出

    • 代码中包含大量的 qDebug()qInfo()。在发布版本中,这些日志可能会影响性能。建议使用 QLoggingCategory 进行分类控制,或者在关键路径上减少日志输出。
  • 内存管理

    • UnindentTextCommand 继承自 QUndoCommand,通常由 QUndoStack 管理,内存管理是安全的。
    • new UnindentTextCommand(...)push 到栈中,符合 Qt 的设计模式。

3. 代码性能

  • 文档操作

    • redo()undo() 中,每次循环都进行 cursor.movePosition 和文本修改操作。对于大量行(例如数千行),这可能会触发多次文档重绘和布局计算。
    • 改进建议:可以考虑使用 QTextDocument 的块迭代器直接访问 QTextBlock,而不是通过 QTextCursor 移动。此外,在修改大量文本前,可以调用 m_edit->document()->blockSignals(true) 或类似的机制(如果适用)来暂时抑制更新,修改完后再恢复,最后统一更新光标。
  • 字符串操作

    • cursor.insertText(QString(removed, ' '));undo() 中创建临时字符串。对于性能敏感场景,这通常是可以接受的,但如果 removed 很大(虽然这里受限于 tabSpaceNumber),可能会有轻微影响。

4. 代码安全

  • 数组越界

    • m_removedChars 的大小为 endline - startline + 1。访问时使用 i - m_startline,逻辑上是安全的,因为 im_startlinem_endline
    • 建议:在构造函数中添加断言 assert(endline >= startline);
  • 空指针

    • m_edit 指针在构造函数中初始化。如果 TextEdit 对象在 Command 执行前被销毁,会导致悬空指针。不过,由于 Command 是由 TextEdit 创建并推入自己的 UndoStack,通常 TextEdit 的生命周期会长于 Command。这是 Qt Undo/Redo 框架的典型用法,相对安全。
  • 异常安全

    • 代码中没有显式的异常处理。Qt 通常不使用异常,但如果 m_editdocument() 返回无效指针,可能会导致崩溃。
    • 建议:在 redo/undo 开始时检查 m_editm_edit->document() 是否有效。

综合改进建议代码示例

针对 indenttextcommond.cpp 中的 redo 方法,优化光标移动逻辑:

void UnindentTextCommand::redo()
{
    if (!m_edit || !m_edit->document()) return;

    qInfo() << "UnindentTextCommand redo - removing indents from lines:"
            << m_startline << "-" << m_endline;
    
    // 使用 beginEditBlock/endEditBlock 将多个操作视为单个操作,提高性能并减少重绘
    auto cursor = m_edit->textCursor();
    cursor.beginEditBlock(); 

    // 使用 findBlockByNumber 直接定位,避免连续 movePosition 的累积误差
    QTextDocument *doc = m_edit->document();
    QTextBlock startBlock = doc->findBlockByNumber(m_startline);
    QTextBlock endBlock = doc->findBlockByNumber(m_endline);
    
    int totalRemoved = 0;
    int currentLineIndex = 0;

    for (QTextBlock block = startBlock; block.isValid() && block.blockNumber() <= m_endline; block = block.next()) {
        cursor.setPosition(block.position());
        
        int removed = 0;
        QString text = block.text();
        
        if (!text.isEmpty()) {
            if (text[0] == '\t') {
                cursor.deleteChar();
                removed = 1;
            } else if (text[0] == ' ') {
                int cnt = 0;
                // 检查行首空格,最多检查 m_tabSpaceNumber 个
                while (cnt < text.length() && text[cnt] == ' ' && cnt < m_tabSpaceNumber) {
                    cnt++;
                }
                if (cnt > 0) {
                    cursor.setPosition(block.position());
                    cursor.setPosition(block.position() + cnt, QTextCursor::KeepAnchor);
                    cursor.removeSelectedText();
                    removed = cnt;
                }
            }
        }

        if (currentLineIndex < m_removedChars.size()) {
            m_removedChars[currentLineIndex] = removed;
        }
        totalRemoved += removed;
        currentLineIndex++;
    }

    // 恢复选区
    if (m_hasselected) {
        int newStartPos = m_startpos;
        int newEndPos = m_endpos;

        // 只有当第一行确实删除了字符时,才调整起始位置
        if (!m_removedChars.isEmpty() && m_removedChars[0] > 0) {
            newStartPos -= m_removedChars[0];
        }
        
        newEndPos -= totalRemoved;

        // 边界检查
        if (newStartPos < 0) newStartPos = 0;
        if (newEndPos < newStartPos) newEndPos = newStartPos;

        cursor.setPosition(newStartPos);
        cursor.setPosition(newEndPos, QTextCursor::KeepAnchor);
        m_edit->setTextCursor(cursor);
    }

    cursor.endEditBlock();
    qDebug() << "UnindentTextCommand redo exit";
}

总结

这段代码主要增加了多行取消缩进的功能,整体逻辑是可行的。主要的改进点在于:

  1. 使用 QTextBlock 迭代器代替光标移动,提高健壮性。
  2. 使用 beginEditBlock/endEditBlock 优化性能。
  3. 修正选区恢复时的边界计算逻辑。
  4. 统一命名规范和修正拼写错误。
  5. 增加必要的空指针检查和断言。

@sourcery-ai
Copy link

sourcery-ai bot commented Feb 26, 2026

Reviewer's Guide

Adds a new UnindentTextCommand to support multi-line unindent via Shift+Tab, refactors TextEdit::unindentText() to detect selections and delegate appropriately while preserving cursor/selection state, and updates license headers.

Sequence diagram for multi-line ShiftTab unindent handling

sequenceDiagram
    actor User
    participant TextEdit
    participant QUndoStack
    participant UnindentTextCommand

    User->>TextEdit: trigger ShiftTab
    activate TextEdit
    TextEdit->>TextEdit: unindentText()
    TextEdit->>TextEdit: cursor = textCursor()
    alt cursor hasSelection
        TextEdit->>TextEdit: compute pos1, pos2, line1, line2
        TextEdit->>UnindentTextCommand: new UnindentTextCommand(this,pos1,pos2,line1,line2,m_tabSpaceNumber)
        TextEdit->>QUndoStack: push(UnindentTextCommand)
        activate QUndoStack
        QUndoStack->>UnindentTextCommand: redo()
        deactivate QUndoStack
    else no selection
        TextEdit->>TextEdit: move cursor to StartOfBlock
        TextEdit->>TextEdit: select first char
        alt first char is tab
            TextEdit->>DeleteBackCommand: new DeleteBackCommand(cursor,this)
            TextEdit->>QUndoStack: push(DeleteBackCommand)
            activate QUndoStack
            QUndoStack->>DeleteBackCommand: redo()
            deactivate QUndoStack
        else first char is space
            TextEdit->>TextEdit: count spaces up to m_tabSpaceNumber
            TextEdit->>TextEdit: select spaces
            TextEdit->>DeleteBackCommand: new DeleteBackCommand(cursor,this)
            TextEdit->>QUndoStack: push(DeleteBackCommand)
            activate QUndoStack
            QUndoStack->>DeleteBackCommand: redo()
            deactivate QUndoStack
        end
    end
    TextEdit-->>User: text unindented
Loading

Updated class diagram for text unindent commands

classDiagram
    class QUndoCommand
    class QUndoStack
    class QTextCursor
    class QVector_int

    class TextEdit {
        int m_tabSpaceNumber
        QUndoStack* m_pUndoStack
        QTextCursor textCursor()
        void setTextCursor(QTextCursor cursor)
        void unindentText()
    }

    class IndentTextCommand {
        +IndentTextCommand(TextEdit* edit)
        +~IndentTextCommand()
        +void redo()
        +void undo()
    }

    class UnindentTextCommand {
        +UnindentTextCommand(TextEdit* edit,int startpos,int endpos,int startline,int endline,int tabSpaceNumber)
        +~UnindentTextCommand()
        +void redo()
        +void undo()
        TextEdit* m_edit
        int m_startpos
        int m_endpos
        int m_startline
        int m_endline
        bool m_hasselected
        int m_tabSpaceNumber
        QVector_int m_removedChars
    }

    class DeleteBackCommand {
        +DeleteBackCommand(QTextCursor cursor,TextEdit* edit)
        +void redo()
        +void undo()
    }

    QUndoCommand <|-- IndentTextCommand
    QUndoCommand <|-- UnindentTextCommand
    QUndoCommand <|-- DeleteBackCommand

    TextEdit --> QUndoStack : uses m_pUndoStack
    TextEdit --> UnindentTextCommand : creates for multiline
    TextEdit --> DeleteBackCommand : creates for single line

    UnindentTextCommand --> TextEdit : operates on
    UnindentTextCommand --> QVector_int : tracks removed chars
    UnindentTextCommand --> QTextCursor : local cursor operations

    DeleteBackCommand --> TextEdit : operates on
    DeleteBackCommand --> QTextCursor

    IndentTextCommand --> TextEdit : operates on
    IndentTextCommand --> QTextCursor
Loading

File-Level Changes

Change Details Files
Introduce UnindentTextCommand to perform multi-line unindent with undo/redo support.
  • Add UnindentTextCommand class with constructor capturing selection range, tab width and editor pointer
  • Implement redo() to iterate over selected lines, remove leading tab or up to tab-width spaces per line, record removed character counts, and restore adjusted selection safely
  • Implement undo() to reinsert previously removed indentation per line and restore original selection
src/editor/indenttextcommond.cpp
src/editor/indenttextcommond.h
Extend TextEdit::unindentText() to handle both multi-line selections and single-line behavior while preserving selection state.
  • Detect whether the cursor has a selection; when it does, normalize start/end positions and line numbers and push an UnindentTextCommand to the undo stack
  • Retain original single-line unindent logic (tab or spaces) in the no-selection path using DeleteBackCommand
  • Ensure calculations use m_tabSpaceNumber and avoid out-of-range cursor positions by keeping selection adjustments within valid bounds
src/editor/dtextedit.cpp
Update SPDX copyright ranges.
  • Extend copyright year ranges in editor-related source and header files to 2026
src/editor/indenttextcommond.cpp
src/editor/dtextedit.cpp
src/editor/indenttextcommond.h

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • The new UnindentTextCommand contains quite a lot of qDebug/qInfo logging for constructor, destructor, redo, and undo, which will fire very frequently for Shift+Tab; consider reducing or guarding these logs to avoid noisy output in normal usage.
  • The unindent logic is now split between UnindentTextCommand for selections and the old DeleteBackCommand path for single lines; consider routing the single-line case through UnindentTextCommand as well to keep behavior consistent and reduce duplicated indentation logic.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new UnindentTextCommand contains quite a lot of qDebug/qInfo logging for constructor, destructor, redo, and undo, which will fire very frequently for Shift+Tab; consider reducing or guarding these logs to avoid noisy output in normal usage.
- The unindent logic is now split between UnindentTextCommand for selections and the old DeleteBackCommand path for single lines; consider routing the single-line case through UnindentTextCommand as well to keep behavior consistent and reduce duplicated indentation logic.

## Individual Comments

### Comment 1
<location path="src/editor/indenttextcommond.cpp" line_range="123-131" />
<code_context>
+    for(int i=m_startline;i<=m_endline;i++){
+        cursor.movePosition(QTextCursor::StartOfBlock);
+        int lineStart = cursor.position();
+        int removed = 0;
+
+        //check if line starts with tab
+        if(m_edit->document()->characterAt(lineStart) == '\t'){
+            cursor.deleteChar();
+            removed = 1;
+        }
+        //check if line starts with spaces
+        else if(m_edit->document()->characterAt(lineStart) == ' '){
+            int cnt = 0;
+            int pos = lineStart;
</code_context>
<issue_to_address>
**issue (bug_risk):** Undo logic can restore a tab when the original indentation was a single space.

In `redo()`, lines starting with spaces set `removed = cnt` (1..m_tabSpaceNumber), but in `undo()` the `removed == 1` case is treated as a tab and restores `"\t"`. So a line that originally had a single leading space is restored with a tab. Instead of inferring from `removed`, you should record whether each line’s removed indentation was a tab or spaces (e.g., a parallel `QVector<bool>` or small enum).
</issue_to_address>

### Comment 2
<location path="src/editor/indenttextcommond.cpp" line_range="152-160" />
<code_context>
+    }
+
+    //reset selection.
+    if(m_hasselected){
+        qDebug() << "UnindentTextCommand redo, m_hasselected, totalRemoved:" << totalRemoved;
+        if(m_startline == m_endline){
+            qDebug() << "UnindentTextCommand redo, m_startline == m_endline";
+            int newStartPos = m_startpos - m_removedChars[0];
+            if(newStartPos < 0) newStartPos = 0;
+            cursor.setPosition(newStartPos);
+            cursor.movePosition(QTextCursor::StartOfBlock);
+            cursor.movePosition(QTextCursor::EndOfBlock,QTextCursor::KeepAnchor);
+            m_edit->setTextCursor(cursor);
+        }
</code_context>
<issue_to_address>
**question (bug_risk):** Single-line selection is expanded to the full line after unindent redo.

In the single-line case, `redo()` always selects the full block, ignoring the original selection range. Previously, unindent preserved the selection on that line, so this is a behavior change that may confuse users. If you want to keep the original range, you could adjust `[m_startpos, m_endpos]` by the removed count for that line rather than reselecting the entire block.
</issue_to_address>

### Comment 3
<location path="src/editor/indenttextcommond.cpp" line_range="90" />
<code_context>
     qDebug() << "IndentTextCommand undo exit";
 }
+
+UnindentTextCommand::UnindentTextCommand(TextEdit* edit,int startpos,int endpos,int startline,int endline,int tabSpaceNumber):
+    m_edit(edit),
+    m_startpos(startpos),
</code_context>
<issue_to_address>
**issue (complexity):** Consider refactoring UnindentTextCommand to be anchored on a stored QTextCursor and QTextBlock range rather than multiple position/line members and the live editor cursor.

You can reduce a lot of the positional/state complexity by anchoring the command around a stored `QTextCursor` and iterating `QTextBlock`s directly, instead of juggling `m_startpos`/`m_endpos`/`m_startline`/`m_endline` and the editor’s live cursor.

Concretely:

1. **Store a private cursor and block range in the command**

```cpp
class UnindentTextCommand : public QUndoCommand {
public:
    UnindentTextCommand(TextEdit *edit, int tabSpaceNumber)
        : m_edit(edit)
        , m_cursor(edit->textCursor())
        , m_tabSpaceNumber(tabSpaceNumber)
    {
        m_hasselected = m_cursor.hasSelection();
        m_cursor.setPosition(m_cursor.selectionStart());
        m_firstBlock = m_cursor.block();
        m_cursor.setPosition(m_cursor.selectionEnd());
        m_lastBlock = m_cursor.block();

        const int blockCount = blockDistance(m_firstBlock, m_lastBlock) + 1;
        m_removedChars.resize(blockCount);
    }

private:
    static int blockDistance(const QTextBlock &from, const QTextBlock &to) {
        int n = 0;
        for (QTextBlock b = from; b != to; b = b.next())
            ++n;
        return n;
    }

    TextEdit *m_edit;
    QTextCursor m_cursor;
    QTextBlock m_firstBlock;
    QTextBlock m_lastBlock;
    QVector<int> m_removedChars;
    bool m_hasselected = false;
    int m_tabSpaceNumber = 4;
};
```

This removes the need for `m_startpos`, `m_endpos`, `m_startline`, `m_endline` and avoids depending on `m_edit->textCursor()` in `redo()`/`undo()`.

2. **Redo: iterate blocks and work from `block.position()`**

```cpp
void UnindentTextCommand::redo()
{
    int totalRemoved = 0;
    int index = 0;

    for (QTextBlock block = m_firstBlock;
         block.isValid() && block.position() <= m_lastBlock.position();
         block = block.next(), ++index)
    {
        QTextCursor c(block);
        const int lineStart = block.position();
        int removed = 0;

        const QChar ch = m_edit->document()->characterAt(lineStart);
        if (ch == '\t') {
            c.deleteChar();
            removed = 1;
        } else if (ch == ' ') {
            int pos = lineStart;
            int cnt = 0;
            while (m_edit->document()->characterAt(pos) == ' ' && cnt < m_tabSpaceNumber) {
                ++pos;
                ++cnt;
            }
            if (cnt > 0) {
                c.setPosition(lineStart);
                c.setPosition(pos, QTextCursor::KeepAnchor);
                c.removeSelectedText();
                removed = cnt;
            }
        }

        m_removedChars[index] = removed;
        totalRemoved += removed;
    }

    if (m_hasselected) {
        // recompute selection from block positions + removed deltas instead of raw offsets
        QTextCursor selCursor(m_firstBlock);
        const int startOffset = m_removedChars.isEmpty() ? 0 : m_removedChars.first();
        selCursor.setPosition(m_firstBlock.position() - startOffset);

        QTextBlock afterLast = m_lastBlock.next();
        int endPos = afterLast.isValid() ? afterLast.position() : m_edit->document()->characterCount();
        endPos -= totalRemoved;
        selCursor.setPosition(endPos, QTextCursor::KeepAnchor);

        m_edit->setTextCursor(selCursor);
    }
}
```

Key changes:

- Work per `QTextBlock` instead of line indexes and repeatedly resetting a shared cursor.
- Selection restoration is derived from `block.position()` and aggregated `removed` values, not manual `m_startpos`/`m_endpos` arithmetic.

3. **Undo: symmetric block-based restore**

```cpp
void UnindentTextCommand::undo()
{
    int index = 0;

    for (QTextBlock block = m_firstBlock;
         block.isValid() && block.position() <= m_lastBlock.position();
         block = block.next(), ++index)
    {
        QTextCursor c(block);
        const int removed = m_removedChars.value(index);
        if (removed == 1) {
            c.insertText("\t");
        } else if (removed > 1) {
            c.insertText(QString(removed, ' '));
        }
    }

    if (m_hasselected) {
        // just restore original stored cursor selection
        m_edit->setTextCursor(m_cursor);
    }
}
```

This keeps all functionality (multi-line unindent with undo, per-line removed-char tracking) while:

- Eliminating the explicit `m_startpos`/`m_endpos`/`m_startline`/`m_endline` state.
- Decoupling the command from whatever the widget’s cursor happens to be at call time.
- Removing low-level selection offset math in favor of block-based computation.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@deepin-ci-robot
Copy link

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: lzwind, wyu71

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@wyu71 wyu71 merged commit 2f0c330 into linuxdeepin:master Feb 27, 2026
18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants