textobj と syntax の連携 #987

Open
koron opened this Issue Nov 24, 2016 · 14 comments

Projects

None yet

5 participants

@koron
Member
koron commented Nov 24, 2016

提案概要

textobj を syntax を用いて柔軟に定義できるようにする。

具体的な提案内容

  • 任意の syntax group を textobj として任意のキーにマップできるようにする
  • (オプション) ABNF などの汎用的な記法から、syntax ファイルを生成できるようにする

結果できること

新しいプログラミング言語の任意の要素に対する text-object が一瞬で作れる

(from https://twitter.com/_tyru_/status/801794126786150404)

@tyru
Member
tyru commented Nov 24, 2016 edited
  • 任意の syntax group を textobj として任意のキーにマップできるようにする

これには厳密に言うと2つ課題があると思っていて、まず syntax group と textobj のマッピングをどのように実現するかという点と、@haya14busa さんの言う通り、Python など、syntax からは「関数」のブロックが分からない言語もあります。

はい,僕もt9mdさんの話をきいて真っ先にそのツラミを思いつきました.実際にやろうとすると python とかは関数のおわりがsyntaxだけではわからないのでfoldも活用するといった知見を得ましたがVim でもやれるかどうか...
https://twitter.com/haya14busa/status/801801451009736704

ただし、「厳密に」と言った通り同時に解決できるような気もしていて、2つ目の Python のような言語の syntax でどのように関数ブロックを紐づけるのか、という課題をクリアできるほど柔軟な設計が思いつけば、それが1つ目のマッピングの設計の課題も解決すると思っています。

  • (オプション) ABNF などの汎用的な記法から、syntax ファイルを生成できるようにする

ABNF をパースしてオートマトンを構築して正規表現に変換して syntax file に吐き出してやれば実現できそうな気がします(自分にやれるとは(ry)。

@koron
Member
koron commented Nov 24, 2016

Python など、syntax からは「関数」のブロックが分からない言語もあります

それは論理がおかしくて、Python 自身は関数の終了を認識しているわけですから、
同じように作られていない Python の syntax がいかんという整理になります。
(もしかしたら、現状の Vim の syntax の機能ではそれを記述できない、
という可能性はありますが、それはそれでまた別問題です)

@tyru
Member
tyru commented Nov 24, 2016 edited

Python 自身は関数の終了を認識しているわけですから、同じように作られていない Python の syntax がいかんという整理になります。

なるほど確かに。原理的には作れるはずですね。

@tyru
Member
tyru commented Nov 24, 2016

Lingr でも話題に上がったのでリンクだけ貼っておきます。
http://lingr.com/room/vim/archives/2016/11/24#message-23726640

あとカーソル下の syntax group 限定ですが、こういうプラグインが既にありました。
https://github.com/kana/vim-textobj-syntax

@t9md
t9md commented Nov 24, 2016 edited

そのまま翻訳はできないと思いますが、vim-mode-plus の text-object-function の 実装で私がやったことを参考までに記載します。

実装箇所
https://github.com/t9md/atom-vim-mode-plus/blob/v0.69.0/lib/text-object.coffee#L704-L730

text-object fold の子供クラスとして実装。
やっていることは

  1. カーソル行が含まれる fold を全部取ってくる。
  2. 各fold は(ザックリ言って)"開始行、終了行"のペアなので、この中から目的のFoldをピックアップ
    すれば、これが text-object function と見なす。
  3. 開始業の token 毎に(word 毎に) syntax を調べて、function 宣言ぽい syntax だったら、関数開始行(これは fold 開始行でもある)と判断
  4. 最後に、ピックアップした fold の inner-, a- のアジャストをして linewise な範囲を返す。

このアプローチは 一行で終わる関数はピックアップできない。が、まあそんなケースあまり多くないし無視、という割り切りをしている。

関数の入れ子どうするか、という話題がありますが、カーソル行が含まれる fold のうち、最も近いfold(上に遡って fold の開始行を検査すればよい)を取ればいいので、難しい問題にはならないです。
[追記] うーん、fold がすでに入れ子を考慮しているからだな。。楽なのは。コードが綺麗に indent されていたとすると、python だと、indent level が浅くなったときに function body が終了。javascript のような {,} で body を囲むものだと、開始行と同じインデントレベルで} が現れたら function-body が終了と見做す。つまり、indent が汚いものは綺麗に取れなくても良いと割り切れれば、シンプルに実装できるのではないか。
Atom は fold が indent ベースで作られるから、結局 indent 情報に頼ってやっているとも言えます。

@koron
Member
koron commented Nov 24, 2016

解決すべき問題は3つかな

  • textobj で任意のキーを任意のsyntax groupにマップする箇所(比較的自明で簡単)
    1. カーソルの位置から、順方向に目的のsyntax groupを探す
    2. 見つけたらそのsyntax group の終わりを探す
    3. 次に逆方向へ、その syntax group の初めを探す
    4. 選択範囲として、あとは通常の textobj の処理に渡す
  • syntax の能力を調べ、場合によっては拡張する(大変かも)
    • 単純なオートマトンだと、入れ子表現がアレ
    • プッシュダウン・オートマトン なら、大丈夫なはず(速度の問題はでそう)
    • チューリング完全? だったら笑うしかねぇなコレw
  • なにかしらの 文脈自由文法 の記述から syntax ファイルを生成する (面倒そうだがツールは揃ってそう)
    • 前提として Vim の syntax がプッシュダウン・オートマトン以上の能力を持っている必要がある

てなことから、syntax の能力を調べるのが、最初に手を付けるべき場所かも。

@koron
Member
koron commented Nov 24, 2016

直観的には プッシュダウン・オートマトン相当 じゃないかとは感じてる。
世の中の syntax 定義がそれを使いこなしているかどうかは別として。

@koron
Member
koron commented Nov 24, 2016 edited

仮に、これに必要な syntax ファイルを作り直す(syntaxファイルの自動生成)ならば、
golang の syntax あたりが一番良いかもしれない。ポピュラーかつシンプル&構文木が自明なので。

@tyru
Member
tyru commented Nov 25, 2016 edited

確か synstack() で入れ子になってる syntax group を確認できたはずなので、自分もプッシュダウンオートマトン相当の能力は備えてるんじゃないかと思ってます(予想)。

@mattn
Member
mattn commented Nov 25, 2016 edited
{
	// foo
	{
		// bar
	}
}

syntax がどうかは知らないですが、このテキストで foo/bar 各々の位置で searchpairpos("{", "", "}") を実行して異なる結果が得られるので実現は可能と思います。

@haya14busa
Member
haya14busa commented Nov 25, 2016 edited

syntax 定義をつかったヒューリスティック関数テキストオブジェクトプラグイン作れました https://github.com/haya14busa/vim-textobj-function-syntax
関数ネストも対応してます.

知見

  • pythonやcoffeescriptといったインデントベースのものは動かなかった(syntax定義的に関数bodyにあたるようなものがない)
  • Scala みたいな関数定義柔軟すぎる言語も関数bodyにあたるようなものがなかった.
  • Typescript(やgo) とかは関数bodyをsyntax regionで定義できそうだけど,braceしかなかった.syntax 定義の問題っぽい.
  • vim/javascript/ruby/lua などはいい感じに動く

vim-atom-mode と違って fold 情報までは使ってませんが,同様にとりあえず行指向だと仮定してる.別に行指向じゃないものも頑張れば作れそう.
filetype specific な関数オブジェクト定義があればそちらを優先できるので割と使える感じになった.

syntax 範囲取得実装

指定した syntax name にマッチする一番ネストの深い部分のstartとendを取得する.
https://github.com/haya14busa/vital-synblock/blob/bd0e26dd33ac532745d5fab2c3b273bd5b2aec1a/autoload/vital/__vital__/Vim/Synblock.vim#L7

関数body の syntax name 取得

涙ぐましい努力. https://github.com/haya14busa/vim-textobj-function-syntax/blob/0feea75ba2c361d5455655a785c33892ef50350c/autoload/textobj/function/syntax.vim#L52

  for re in ['funcblock', 'funcbody', 'functionblock', 'functionbody', 'methodblock', 'block', 'brace']

なにか syntax を拡張するなら syntax を汎用的な形で取得できるようになると嬉しい.

その他応用可能性

syntax based textobj で有用そうだなと思うのは関数textobjが真っ先に思いついたので実装しました.
他にはコメント/文字列リテラル/正規表現リテラル/etc.. とかもあるだろうけど,これらは https://github.com/kana/vim-textobj-syntax と似た実装で一瞬で作れそう.

@mattn
Member
mattn commented Nov 25, 2016

そうなんすよねー。辞書的に優先度付けてやらんと複数マッチしてしまうのがネック。

@koron
Member
koron commented Nov 26, 2016

僕が考えていたのは仮想の tomap で、こんな感じ。
syntax 毎に、どのキーをどの syntax group にマップするのか
Vim 本体側で設定できるようにしちゃう。

ftplugin/vim/tomaps.vim:

tomap f vimFunction

ftplugin/lua/tomaps.vim:

tomap f luaFunction

なので、涙ぐましい部分は、最終的に ftplugin か、各syntaxファイルで提供される、はずw

@tyru
Member
tyru commented Nov 26, 2016

確かに ftplugin でやるのが良さそうですね。
涙ぐましい部分の実装は caw.vim がやってるみたいに ftplugin を自動生成してやるのもアリかも、と思いました。
https://github.com/tyru/caw.vim/blob/e3d84c28b44cc4434fd6a85a8aaecffe4c0b4a66/macros/separate.vim

ただ tomap のインターフェースを工夫すれば割と辛くない感じにもなりそうかも…?

call textobj#function#syntax#tomap('f', [
\  'funcblock', 'funcbody', 'functionblock', 'functionbody', 'methodblock', 'block', 'brace'
\])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment