タグファイルから補完

提供: MeryWiki
2024年12月31日 (火) 06:29時点におけるMSY-07 (トーク | 投稿記録)による版 (ソースコードの整形)
ナビゲーションに移動 検索に移動

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

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

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

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

インストール

  1. 下のスクリプトをコピーして、MeryのMy Macroフォルダなどに保存
  2. Mery.exeのあるフォルダにctags.exeを置く
  3. マクロを登録し、Ctrl+→やCtrl+Shift+SpaceやCtrl+Qキーなどにお好みで設定する

使い方

  1. 補完したい単語にカーソルを置き、マクロを起動する
  2. 初回起動の場合、タグファイルの作成を待つ
  3. すでに単語が完全な場合、メニューを選ぶとその定義にジャンプする
  4. メニューの項目を選ぶと、その単語が挿入される
  5. メニューの最後の「タグファイルを更新する」を選ぶと、タグファイルを作り直すことができる

カスタマイズ

変数を変えることで、挙動がいくらかカスタマイズできます。

名前 初期設定 効果
TAGS ".merytags" タグファイルの名前。
CTAGS "ctags.exe" ctags.exeの名前。

相対パスの場合はmery本体のあるディレクトリのものを使います。

"c:\\bin\\ctags.exe" のように絶対パスも指定できます。

CTAGS_FLAGS "--excmd=number --recurse --fields=+S" ctagsに与える引数。

初期設定の意味は左から

  • 位置を行番号で記憶 (タグジャンプに必要)
  • フォルダを再帰的に処理
  • 関数の引数を記憶
WHOLE_WORD true 単語の途中で補完する際の挙動。

trueにするとカーソルの置かれた単語全体から補完。

falseにするとカーソルより左から補完します。

CASE_INSENSITIVE true 単語の検索の挙動。

trueにすると検索時大文字と小文字を区別しません。

falseにすると区別します。

DEFAULT_TAGS_DIR "." タグを作るフォルダ。

"." にすると、編集中のファイルのあるフォルダでタグを作ります。

".." などにすると、編集中のファイルの一つ上のフォルダからタグを作ります。

CONFIRM_CREATE_TAGS true タグファイルがない場合の挙動。

trueにするとタグを作成するフォルダを尋ねます。

falseにするとフォルダを尋ねずに無言で作成を始めます。

変更履歴

  • r7 (2012-10-14)
    • 補完候補の単語が1つだけのときうまく補完されないのを修正
  • r6 (2012-08-23)
    • 確認せずにctagsを起動するオプション
  • r5 (2012-08-01)
    • C言語の関数で引数を表示、今開いてるファイル中の単語からも補完、文字の大小を区別しないオプション
  • r4 (2012-06-22)
    • ホットキー
  • r3
    • 単独でctags呼び出し
  • r2
    • 失念
  • r1
    • 失念

ソースコード

// タグファイルから補完.js r7 2012-10-24
// https://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

// https://hp.vector.co.jp/authors/VA025040/ctags/ の 日本語版ctags 5.8J2 で動作確認しています

/** 設定 **/

// タグファイルの名前
TAGS = ".merytags"

// ctags.exeの名前
// 相対パスの場合はmery本体のあるディレクトリのものを使います
// "c:\\bin\\ctags.exe" のように絶対パスも指定できます
CTAGS = "ctags.exe"

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

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

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

// タグを作るフォルダ
// "." にすると、編集中のファイルのあるフォルダでタグを作ります
// ".." などにすると、編集中のファイルの一つ上のフォルダからタグを作ります
DEFAULT_TAGS_DIR = "."

// タグファイルがない場合の挙動
// trueにするとタグを作成するフォルダを尋ねます
// falseにするとフォルダを尋ねずに無言で作成を始めます
CONFIRM_CREATE_TAGS = false

/** 本体 **/

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 opts = CASE_INSENSITIVE ? "igm" : "gm"
		var re = new RegExp("^" + word + ".*$", opts)
		var lines = text.match(re)

		if (!lines) {
			return []
		}

		return calcWords().concat(calcDefinitions(word))

		function calcWords() {
			var words = new Array(lines.length)

			words[0] = lines[0].substring(0, lines[0].indexOf("\t"))

			if (words.length === 1) {
				return words[0] === word ? [] : words
			}

			var n = 0
			for (var i = 1; i < lines.length; i++) {
				var w = lines[i].substring(0, lines[i].indexOf("\t"))

				if (words[n] !== w) {
					n++
					words[n] = w
				}
			}

			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 line = lines[i]
				var tab1 = line.indexOf("\t")

				if (tab1 !== word.length) {
					continue
				}

				var tab2 = line.indexOf("\t", tab1 + 1)
				var endnum = line.indexOf(";", tab2 + 1)
				var sig = line.indexOf("signature:", endnum + 1)

				var name = line.substring(0, tab1)
				var filename = line.substring(tab1 + 1, tab2)
				var linenum = line.substring(tab2 + 1, endnum)
				var signature = sig < 0 ? "" : line.substring(sig + "signature:".length)

				var fullname = FileSystem.BuildPath(getPath(), filename)

				defs[n] = [fullname, linenum, 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 = FileSystem.BuildPath(document.Path, DEFAULT_TAGS_DIR)

		if (CONFIRM_CREATE_TAGS) {
			dir = window.Prompt("タグファイル作成フォルダ", dir)
		}

		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
	}
}
スポンサーリンク