ついカッとなって,IdeaVIMのword motionをVim並にしてみた

IdeaVIMは日本語などのマルチバイトを特別扱いしてくれないため,「ABCでEFG」のような文字列は1語として扱われる(vimの場合,「ABC|で|EFG」の3語になる)。それでも,仕方ないなかと思い使い続けてきたんだけど,jVi for NBがマルチバイトを区別できたのを見て,思わずパッチを当ててしまった。


今は反省満足している。:-P


こっから,あてたパッチの解説。


まずは,IdeaVIMでword motionの実装箇所を探ってみる。MotionWordRightHander(com.maddyhome.idea.vim.handler.motion.textパッケージ)あたりを起点に探ってみると,こんなコールグラフになる。

MotionWordRightHander
 → MotionGroup.moveCaretToNextWord()
   → SearchHelper.findNextWord()
     → SearchHelper.findNextWordOne()


SearchHelper.findNextWordOne()を覗いてみると,(↓)こんな感じに文字種の境目をwordの境目としているのがわかる。

private static int findNextWordOne(CharSequence chars, int pos, int size, int step, boolean skipPunc, boolean spaceWords) {
  boolean found = false;
   :
  pos += step;
  while (pos >= 0 && pos < size && !found) {
    int newType = CharacterHelper.charType(chars.charAt(pos), skipPunc);
    if (newType != type) {
      if (newType == CharacterHelper.TYPE_SPACE && step >= 0 && !spaceWords) {
        pos = skipSpace(chars, pos, step, size);
        res = pos;
      }
      else if (step < 0) {
        res = pos + 1;
      }
      else {
        res = pos;
      }

      type = CharacterHelper.charType(chars.charAt(res), skipPunc);
      found = true;
    }

    pos += step;
  }

  if (found) {
    :


てことは,CharacterHelper.charType()で判別可能な文字種を増やせば,wordとして認識されるバリエーションも増えるなと思い,やおらこんなパッチを当ててみた。

Index: src/com/maddyhome/idea/vim/helper/CharacterHelper.java
===================================================================
--- src/com/maddyhome/idea/vim/helper/CharacterHelper.java	Tue Mar 18 17:32:44 JST 2008
+++ src/com/maddyhome/idea/vim/helper/CharacterHelper.java	Tue Mar 18 17:32:44 JST 2008
@@ -1,5 +1,7 @@
 package com.maddyhome.idea.vim.helper;
 
+import java.util.Arrays;
+
 /*
  * IdeaVim - A Vim emulator plugin for IntelliJ Idea
  * Copyright (C) 2003-2005 Rick Maddy
@@ -31,6 +33,12 @@
     public static final int TYPE_CHAR = 1;
     public static final int TYPE_PUNC = 2;
     public static final int TYPE_SPACE = 3;
+    public static final int TYPE_KANA = 4;
+    public static final int TYPE_WIDE_ALPHANUM = 5;
+    public static final int TYPE_WIDE_PUNC = 6;
+    public static final int TYPE_WIDE_HIRAGANA = 7;
+    public static final int TYPE_WIDE_KATAKANA = 8;
+    public static final int TYPE_OTHER = 9;
 
     /**
      * This returns the type of the supplied character. The logic is as follows:<br>
@@ -48,15 +56,32 @@
         {
             return TYPE_SPACE;
         }
-        else if (skipPunc || Character.isLetterOrDigit(ch) || ch == '_')
+        else if (skipPunc || isHalfAlphaNum(ch) || ch == '_')
         {
             return TYPE_CHAR;
         }
-        else
-        {
+        else if (isHalfSymbol(ch)) {
             return TYPE_PUNC;
         }
+        else if (isHalfKana(ch)) {
+            return TYPE_KANA;
-    }
+        }
+        else if (isWideAlphaNum(ch)) {
+            return TYPE_WIDE_ALPHANUM;
+        }
+        else if (isWideSymbol(ch)) {
+            return TYPE_WIDE_PUNC;
+        }
+        else if (Character.UnicodeBlock.of(ch) == Character.UnicodeBlock.HIRAGANA) {
+            return TYPE_WIDE_HIRAGANA;
+        }
+        else if (Character.UnicodeBlock.of(ch) == Character.UnicodeBlock.KATAKANA) {
+            return TYPE_WIDE_KATAKANA;
+        }
+        else {
+            return TYPE_OTHER;
+        }
+    }
 
     /**
      * Changes the case of the supplied character based on the supplied change type
@@ -88,4 +113,32 @@
 
         return ch;
     }
+
+    private static boolean isHalfAlphaNum(char ch) {
+        return (0x30 <= ch && ch <= 0x39) || (0x41 <= ch && ch <= 0x5A) || (0x61 <= ch && ch <= 0x7A);
-}
+    }
+    private static boolean isWideAlphaNum(char ch) {
+        return (0xFF10 <= ch && ch <= 0xFF19) || (0xFF21 <= ch && ch <= 0xFF3A) || (0xFF41 <= ch && ch <= 0xFF5A);
+    }
+
+    private static boolean isHalfSymbol(char ch) {
+        return Arrays.binarySearch(allHalfSymbol, ch) > 0;
+    }
+    private static boolean isWideSymbol(char ch) {
+        return Arrays.binarySearch(allWideSymbol, ch) > 0;
+    }
+    private static boolean isHalfKana(char ch) {
+        return Arrays.binarySearch(allHalfkana, ch) > 0;
+    }
+
+    private static char[] allHalfSymbol = "、。,.:;?!`^_/〜|‘’“”()[]{}+−=<>¥$%#&*@".toCharArray();
+    private static char[] allWideSymbol = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~".toCharArray();
+    private static char[] allHalfkana = "。「」、・ヲァィゥェォャュョッーアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン゙゚".toCharArray();
+
+    static {
+        Arrays.sort(allHalfSymbol);
+        Arrays.sort(allWideSymbol);
+        Arrays.sort(allHalfkana);
+    }
+
+}

vimの実装を参考にしたわけじゃないけど,実験してみたらvimのword motionは,「半角英数字/半角シンボル/半角カタカナ/全角英数字/全角ひらがな/全角カタカナ/全角シンボル/その他(漢字)」と空白を区切にしているようなので,振る舞いをマネてみた。


あまり効率良さそうな実装じゃないけど,特に負荷かかってる様子もないので,しばらく使ってみることにする。