タグファイルから補完

2012年8月22日 (水) 14:55時点におけるSnipsnipsnip (トーク | 投稿記録)による版

ctagsの生成したtagsファイルから検索して単語のメニューを出します。

項目を選ぶとその単語が挿入されます。

単語が完全な形の場合、タグジャンプを行います。

kazyさんのctags.exeでtagsファイルを生成、タグジャンプを行うマクロとあわせてお使いください。(単独で使ってもタグジャンプ機能はあります)

  • インストール
  1. 下のスクリプトをコピーして、MeryのMy Macroフォルダなどに保存
  2. マクロを登録し、Ctrl+→やCtrl+Shift+SpaceやCtrl+Qキーなどにお好みで設定する
  3. Mery.exeのあるフォルダに[ctags.exe]を置く
  • 初回起動
  1. タグファイルが生成されていないので、どこに生成するか尋ねられる
  2. 指定したディレクトリ以下のソースファイル全てが再帰的にタグファイルに登録される
  3. 「タグファイルの作成が完了しました」とステータスバーに出るまで待つ
  • タグファイルの作成後
  1. 補完したい単語にカーソルを置き、マクロを起動する
  2. すでに単語が完全な場合、メニューを選ぶとその定義にジャンプする
  3. メニューの項目を選ぶと、その単語が挿入される
  4. メニューの最後の「タグファイルを更新する」を選ぶと、タグファイルを作り直すことができる
// タグファイルから補完.js r5
// http://www.haijin-boys.com/wiki/%E3%82%BF%E3%82%B0%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%8B%E3%82%89%E8%A3%9C%E5%AE%8C

/** 設定 **/

// タグファイルの名前
var TAGS = "tags"

// ctags.exeの名前
// 相対パスの場合は以下のディレクトリから順に探します
//   ・mery本体と同じディレクトリ
var CTAGS = "ctags.exe"

// ctagsに与える引数 (初期設定:位置を行番号で記憶(必須)、フォルダを再帰的に処理、関数の引数を記憶)
var CTAGS_FLAGS = "--excmd=number --recurse --fields=+S"

// 単語の途中で補完する際の挙動
// trueにするとカーソルの置かれた単語全体から補完
// falseにするとカーソルより左から補完します
var WHOLE_WORD = true

// 単語の検索の挙動
// trueにすると検索時大文字と小文字を区別しません
// falseにすると区別します
var CASE_INSENSITIVE = true

/** 本体 **/

var FileSystem = new ActiveXObject("Scripting.FileSystemObject")
var Shell = new ActiveXObject("WScript.Shell")

main()

function main()
{
  var ctags = makeCtags()
  var tags = makeTags(ctags)
  var query = getQueryWord()
  var result = tags.lookup(query)
  var word = chooseWord(result)
  
  if ("string" === typeof word)
  {
    writeWord(word)
  }
  else if (word === false)
  {
    tags.update()
  }
  else
  {
    jumpToDefinition(word)
  }
}

function makeCtags()
{
  return { run: run }

  function run(dir)
  {
    var ctags = findCtags()
    
    try
    {
      Shell.CurrentDirectory = dir
      window.Status = 'タグファイルを作成しています お待ちください'
      Shell.Run(ctags + " -o " + TAGS + " " + CTAGS_FLAGS, 0, true)
    }
    catch (e)
    {
    }
    
    window.Status = 'タグファイルを作成しました'
    return FileSystem.BuildPath(dir, TAGS)
  }
  
  function findCtags()
  {
    if (FileSystem.FileExists(CTAGS))
    {
      return '"' + CTAGS + '"'
    }
    
    var dir = FileSystem.GetParentFolderName(editor.FullName)
    var ctags = FileSystem.BuildPath(dir, CTAGS)
    
    if (FileSystem.FileExists(ctags))
    {
      return '"' + ctags + '"'
    }
    
    throw new Error("ctags.exeが見つかりませんでした\nMery本体のディレクトリに置くか、絶対パスを設定してください")
  }
}

function makeTags(ctags)
{
  var fullname = findTagfile()
  var text = loadTagfile()
  
  return { lookup: lookup, update: update }
  
  function getPath()
  {
    return FileSystem.GetParentFolderName(fullname)
  }
  
  function lookup(word)
  {
    if (/^\s*$/.test(word)) { return }
    
    word = reEscape(word)
    
    var re = new RegExp("^" + word + ".*$", CASE_INSENSITIVE ? "igm" : "gm")
    var lines = text.match(re)
    
    if (!lines) { return [] }
    
    return calcWords().concat(calcDefinitions(word))
    
    function calcWords()
    {
      if (lines.length === 1) { return [] }
      
      var words = new Array(lines.length)
      
      words[0] = lines[0].substring(0, lines[0].indexOf("\t"))
      
      var n = 0
      for (var i = 1; i < lines.length; i++)
      {
        var word = lines[i].substring(0, lines[i].indexOf("\t"))
        
        if (words[n] !== word)
        {
          n++
          words[n] = word
        }
      }
      
      words.length = n + 1

      words.sort(function(a, b)
      {
        return a.localeCompare(b)
      })
      
      return words
    }
    
    function calcDefinitions(word)
    {
      var defs = new Array(lines.length)
      var n = 0
      
      for (var i = 0; i < lines.length; i++)
      {
        var tab1 = lines[i].indexOf("\t")
        
        if (tab1 !== word.length) { continue }
        
        var tab2 = lines[i].indexOf("\t", tab1 + 1)
        var endnum = lines[i].indexOf(";", tab2 + 1)
        var sig = lines[i].indexOf("signature:", endnum + 1)
        
        var name = lines[i].substring(0, tab1)
        var filename = lines[i].substring(tab1 + 1, tab2)
        var line = lines[i].substring(tab2 + 1, endnum)
        var signature = sig < 0 ? "" : lines[i].substring(sig + "signature:".length)
        
        var fullname = FileSystem.BuildPath(getPath(), filename)
        
        defs[n] = [fullname, line, filename, name + signature]
        defs[n].toString = joinme
        n++
      }
      
      defs.length = n
      
      return defs
    }
    
    function joinme()
    {
      return this[3] + " (" + this[2] + " : " + this[1] + ")"
    }
  }
  
  function reEscape(w)
  {
    return w.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&")
  }
  
  function loadTagfile()
  {
    if (!fullname)
    {
      fullname = makeTagfile()
    }
    
    return slurp(fullname)
  }
  
  function findTagfile()
  {
    var dir = document.Path
    
    while (FileSystem.FolderExists(dir))
    {
      var file = FileSystem.BuildPath(dir, TAGS)
      if (FileSystem.FileExists(file)) { return file }
      dir = FileSystem.GetParentFolderName(dir)
    }
  }
  
  function makeTagfile()
  {
    var dir = window.Prompt("タグファイル作成フォルダ", document.Path)
    
    if (!dir)
    {
      window.Quit()
    }
    
    return ctags.run(dir)
  }
  
  function update()
  {
    return ctags.run(getPath())
  }
  
  function slurp(filename)
  {
    var f = FileSystem.OpenTextFile(filename)
    var text = f.ReadAll()
    f.Close()
    return text
  }
}

function getQueryWord()
{
  var s = document.selection
  
  if (!s.IsEmpty) { return s.Text }
  
  if (WHOLE_WORD)
  {
    s.SelectWord()
    
    // hoge|(); → hogeFuga|(); のように補完 
    if (s.Text.length == 1 && !/\w/.test(s.Text))
    {
      s.Collapse(meCollapseStart)
      s.WordLeft(true)
    }
  }
  else
  {
    s.WordLeft(true)
  }
  
  if (/\A\s+\z/.test(s.Text))
  {
    s.Collapse(meCollapseEnd)
  }
  
  return s.Text
}

function chooseWord(words)
{
  if (words && words.length === 1 && typeof words[0] === "string")
  {
    return words[0]
  }

  var menu = makeMenu()
  var i = menu.Track(0)
  
  return i === -1 ? false : words && words[i - 1]
  
  function makeMenu()
  {
    var menu = window.CreatePopupMenu()
    initMenu(menu)
    menu.Add("タグファイルを更新する", -1)
    return menu
  }
  
  function initMenu(menu)
  {
    if (!words || words.length == 0) { return }
    
    var n = 0
    function addHotkey(word)
    {
      if (n > 34) { return word }
      n++
      return "&" + n.toString(36) + ". " + word
    }
    
    for (var i = 0; i < words.length; i++)
    {
      menu.Add(addHotkey(words[i]), i + 1)
    }
    
    menu.Add(undefined, 0, meMenuSeparator)
  }
}

function jumpToDefinition(def)
{
  if (!def) { return }
  window.Status = def + "にジャンプします"
  var file = def[0]
  var line = def[1]
  
  editor.OpenFile(file, 0, meOpenAllowNewWindow)
  editor.ActiveDocument.selection.SetActivePoint(mePosLogical, 1, line, false)
  editor.ActiveDocument.Activate()
}

function writeWord(word)
{
  if (word)
  {
    document.selection.Text = word
  }
}
スポンサーリンク