/正規表現 Enter は文書末尾方向に戦記表現にマッチする文字列を検索するコマンドだ。
?正規表現 Enter は文書先頭方向に向かって検索する。
文書末尾・先頭に到達した場合は、文書先頭・末尾から検索を続行し、もとの位置に戻ってきたらそこで諦める。
n N は再検索コマンド。n は直前の検索と同方向、N は逆方向に検索する。
ex コマンド同様に、ステータスバーに行エディットを表示し、そこで正規表現を入力可能にする。
これまで、vi のモードとして、INSERT, CMD, CMDLINE の3つがあった。CMDLINE は ex モード用である。 これに /? モード用の状態を追加してもいいのだが、/ と ? を区別する必要があるので、その情報を渡す必要がある。 どうせ渡すのであれば、exモード・順検索・逆検索の情報を渡すようにして、exモードと共通にした方が無駄がない。 よって、モードは CMDLINE で共有し、modeChanged() シグナルの第2引数に ':', '/', '?' のいずれかを渡すことで区別することにする。
コードは下記の様になる。
1: class ViEngine : public QObject 2: { 3: ..... 4: signals: 5: void modeChanged(Mode, ushort=0); 6: ..... 7: };
1: void ViEngine::setMode(Mode mode, ushort subMode) 2: { 3: if( mode != m_mode ) { 4: m_mode = mode; 5: m_noInsModeAtImeOpenStatus = true; 6: emit modeChanged(mode, subMode); 7: m_noInsModeAtImeOpenStatus = false; 8: } 9: }
1: bool ViEngine::doViCommand(const QChar &qch) 2: { 3: ..... 4: switch( ch ) { 5: ..... 6: case '/': 7: case '?': 8: case ':': 9: setMode(CMDLINE, ch); // コマンドラインモードへ 10: break; 11: } 12: ..... 13: }
MainWindow::onModeChanged() も引数を増やし、第2引数の文字をプロンプトとして表示するように修正する。
1: void MainWindow::onModeChanged(Mode mode, ushort subMode) 2: { 3: ..... 4: case CMDLINE: 5: m_cmdLineEdit->setText(QChar(subMode)); 6: m_cmdLineEdit->show(); 7: m_cmdLineEdit->setFocus(Qt::OtherFocusReason); 8: statusBar()->repaint(); 9: return; 10: } 11: ..... 12: }
1: void MainWindow::cmdLineTextChanged(const QString & text) 2: { 3: if( text.isEmpty() || (text[0] != ':' && text[0] != '/' && text[0] != '?') ) 4: m_viEngine->setMode(CMD); 5: }
最後に、コマンドラインで Enter が押されたら、プロンプトをチェックして、 exコマンド処理メソッド または 検索メソッドをコールしてやるだけだ。
1: void MainWindow::cmdLineReturnPressed() 2: { 3: QString text = m_cmdLineEdit->text(); 4: if( !text.isEmpty() ) { 5: if( text[0] == ':' ) 6: m_viEngine->processExCommand(text.mid(1)); 7: else 8: m_viEngine->doFind(text.mid(1), text[0] == '/'); 9: } 10: }
次に検索処理を実装するのだが、その前に検索処理を実際に行う関数を定義する。
まずは順方向検索を行う関数を定義する。正規表現オブジェクト、検索開始ブロック・インデックス、
何回検索を行うか、検索リミット(-1 ならばリミット無し)を引数として取るものとする。
リミットは末尾まで検索してもマッチするものが無く先頭から検索した時に、現在のカーソル位置で検索を停止するためのものだ。
コードは下記の様になる。正規表現検索は QRegExp が行ってくれるので楽ちんだ。
1: int moveCursorFindForward(const QRegExp &rex, QTextBlock &block, int ix, int &nth, int limit) 2: { 3: for(;;) { 4: int i = rex.indexIn(block.text(), ix); 5: if( i >= 0 ) { 6: if( !--nth ) { 7: int pos = i + block.position(); 8: if( limit < 0 || pos <= limit ) { // 初期カーソル位置に一周して戻ってきた場合もOK 9: return pos; 10: } else 11: return -1; 12: } 13: ++ix; 14: } else { 15: ix = 0; 16: block = block.next(); 17: if( !block.isValid() || limit >= 0 && block.position() > limit ) 18: return -1; 19: } 20: } 21: }
次は逆方向検索。基本的に indexIn() を lastIndexIn() に変えるだけ。 ただし、検索開始位置に -1 を指定すると末尾から検索してしまうので、ix が0以上かどうかをチェックし、 マイナスであれば前の行に移動するようにしている。
1: int moveCursorFindBackward(const QRegExp &rex, QTextBlock &block, int ix, int &nth, int limit) 2: { 3: int i; 4: for(;;) { 5: if( ix >= 0 && (i = rex.lastIndexIn(block.text(), ix)) >= 0 ) { // ix = -1 を指定すると最後から検索してしまう 6: if( !--nth ) { 7: int pos = i + block.position(); 8: if( limit < 0 || pos >= limit ) { // 初期カーソル位置に一周して戻ってきた場合もOK 9: return pos; 10: } else 11: return -1; 12: } 13: --ix; 14: } else { 15: block = block.previous(); 16: if( !block.isValid() || limit >= 0 && block.position() < limit ) 17: return -1; 18: ix = block.text().length(); 19: } 20: } 21: }
上記の2つの検索関数を使うと、ループをサポートし、マッチした場合は QTextCursor を更新する検索関数は以下のように記述できる。
1: bool moveCursorFind(QTextCursor &cur, const QRegExp &rex, bool forward, int nth) 2: { 3: QTextBlock block = cur.block(); 4: int curPos = cur.position(); 5: int ix = curPos - block.position(); 6: int pos; 7: if( forward ) { 8: ++ix; 9: if( (pos = moveCursorFindForward(rex, block, ix, nth, -1)) >= 0 ) { 10: cur.setPosition(pos); 11: return true; 12: } 13: // 文章末尾までに見つからなかったら、文書先頭から検索 14: block = block.document()->firstBlock(); 15: ix = 0; 16: if( (pos = moveCursorFindForward(rex, block, ix, nth, curPos)) >= 0 ) { 17: cur.setPosition(pos); 18: return true; 19: } 20: } else { 21: --ix; 22: if( (pos = moveCursorFindBackward(rex, block, ix, nth, -1)) >= 0 ) { 23: cur.setPosition(pos); 24: return true; 25: } 26: // 文章先頭までに見つからなかったら、文書末尾から検索 27: block = block.document()->lastBlock(); 28: ix = block.text().length(); 29: if( (pos = moveCursorFindBackward(rex, block, ix, nth, curPos)) >= 0 ) { 30: cur.setPosition(pos); 31: return true; 32: } 33: } 34: return false; 35: }
んで、ViEngine での検索処理は下記の様に記述できる。
1: void ViEngine::doFind(const QString &rexText, bool forward) 2: { 3: setMode(CMD); 4: QRegExp rex(rexText); 5: if( !rex.isValid() ) { 6: showMessage(tr("invalid regexp.")); 7: return; 8: } 9: QTextCursor cur = m_editor->textCursor(); 10: if( moveCursorFind(cur, rex, forward) ) { 11: m_findString = rexText; // 再検索の為に保存 12: m_findForward = forward; 13: m_editor->setTextCursor(cur); 14: } 15: }
ここまで来れば、再検索を行う n N の実装は簡単だ。
ViEngine::doViCommand(const QChar &qch) の switch 文に 'n', 'N' を追加し、再検索カーソル移動メソッドをコールしてあげるだけ。
1: bool ViEngine::doViCommand(const QChar &qch) 2: { 3: ..... 4: switch( ch ) { 5: ..... 6: case 'n': 7: cursorMoved = doFindNext(cur, repeatCount()); 8: break; 9: case 'N': 10: cursorMoved = doFindNext(cur, repeatCount(), /*reverse=*/true); 11: break; 12: } 13: ..... 14: }
再検索カーソル移動メソッドの定義は以下のとおり。
1: bool ViEngine::doFindNext(QTextCursor &cur, int nth, bool revers) 2: { 3: if( m_findString.isEmpty() ) 4: return false; 5: QRegExp rex(m_findString); 6: if( !rex.isValid() ) { 7: showMessage(tr("invalid regexp.")); 8: return false; 9: } 10: const bool forward = !revers ? m_findForward : !m_findForward; 11: return moveCursorFind(cur, rex, forward, nth); 12: }