はじめに
Vimにはモードラインという、ファイル自体に記述したVimの各種オプションを読み取り、ファイルを開く際に自動で設定を行う機能がある。
例えば下記のようなコメントをRubyプログラムの先頭または末尾あたりに書いておくとする。
# vim:set expandtab tabstop=2 fenc=euc-jp ff=unix ft=ruby:
すると、このファイルを開く際、Vimは自動で
- Tabの代わりに空白2文字を挿入
- 文字コードはeuc-jp
- 改行コードはLF
- ファイルタイプはRuby
とエディタの設定を変更してくれる。
詳しくはマニュアルの日本語訳を読んでいただきたいが、VimからAtomに移行した際にはこのモードラインを解釈してほしいケースがときたま存在する。
物は試しとモードラインの解釈から設定の反映までを試みることにした。
もちろん、VimとAtomでは設定項目が異なってくるため、ひとまずは個人的によく使う「文字コード」「改行コード」「ファイルタイプ」の設定を行うことを目標とする。
モードラインのパース
モードラインの書式は上記のマニュアル内に記載があるため、それらに沿った書式を正規表現でパースすることとした。
parseVimModeLine = (line) ->regexp = /(vi|vim|\s+ex):\s*(se(t)*)*\s+([^:]+)*\s*:/matches = line.matchregexpoptions = nullifmatchesoptions = {}foroptioninmatches[4].split(" ")[key,value]=option.split"="options[key]=value?trueifkey!=""options
「はじめに」に記載した例を上記関数に渡すと下記のようなオブジェクトを返してくれる。
{expandtab: true,tabstop: "2",fenc: "euc-jp",ff: "unix",ft: "ruby"}
文字コードのセット
encoding-selectorのdetectEncoding
を参考に、文字コードのセットを行う部分だけを取り出した。
iconv-liteが対応している文字コードしか通せないため、例えばeucJP-ms
と記載されていた場合はfalse
を返すこととなる。
setEncoding = (encoding) ->iconv = require'iconv-lite'editor = atom.workspace.getActiveTextEditor()returnfalseunlessiconv.encodingExists(encoding)encoding = encoding.toLowerCase().replace(/[^0-9a-z]|:\d{4}$/g,'')editor.setEncoding(encoding)
上記parseVimModeLine
の返り値のfileencoding
またはfenc
の値を渡せば設定可能となる。
options = parseVimModeLine(line)setEncoding(options.fileencoding?options.fenc)
上記のような書き方の場合、fileencoding
、fenc
どちらも設定がない場合はundefined
が渡ることとなるが、特にエラーが発生する訳でもないのでプロパティの存在チェックは行っていない。
改行コードのセット
line-ending-selectorの実装を参考に改行コードの設定を行う関数を実装した。
設定と同時にバッファ内の改行コードを置換するようにしているため、実際の改行コードとモードラインに記載された改行コードが異なる場合は多少注意が必要となる。
setLineEnding = (lineEnding) ->editor = atom.workspace.getActiveTextEditor()buffer = editor?.getBuffer()returnunlessbufferbuffer.setPreferredLineEnding(lineEnding)buffer.setText(buffer.getText().replace(/\r\n|\r|\n/g,buffer.getPreferredLineEnding()))
モードラインに記載するものはunix
、dos
、mac
という記法であるため、これらと実際の改行コードを結び付けてやる必要がある。
format = options.fileformat?options.fflineEnding: {unix: "\n"dos: "\r\n"mac: "\r"}setLineEnding(lineEnding[format])
なお、CR
はline-ending-selectorの選択肢には表示されていないが、試してみたところ正しく設定がされ、ステータスバーの改行コード欄もCR
と表示されていた。
ファイルタイプのセット
2015/11/09 atom.grammars.selectGrammar
を利用するように修正
grammar-selectorの実装を参考にファイルタイプの設定を行う関数を実装したのだが、atom.grammars.selectGrammar
といううってつけのメソッドが存在するため書き換えた。
この場合、例えばCore Packagesのlanguage-ruby-on-railsのものであるRuby on Rails
は選択されず、railsプロジェクトであってもRuby
が選択されてしまうこととなる。
今のところAtomでrailsプロジェクトを扱う予定はないのでこのまま利用しようと思うが、railsプロジェクトと通常のRubyプログラムを書き分ける際は何かしら解決策を考える必要がある。
setFileType = (type) ->editor = atom.workspace.getActiveTextEditor()returnunlesseditorgrammar = atom.grammars.selectGrammar(type)ifgrammarisntatom.grammars.nullGrammaratom.grammars.setGrammarOverrideForPath(editor.getPath(),grammar)editor.setGrammar(grammar)
利用する際はパースしたfiletype
、ft
の値をそのまま引数に渡すようにしたが、場合によっては読み替えを行うようにする必要がある。
setFileType(options.filetype?options.ft)
現時点ではこれで困らなかったのでそのまま利用しているが、必要ならば「改行コードのセット」のように読み替え用のオブジェクトを用意し、undefined
であればfiletype
に設定されているものをそのまま渡すようにするとよいと思う。
ファイルの読み込み時に実行する
2015/11/09 atom.grammars.selectGrammar
を利用するように修正
上記の関数をinit.coffee
に記載し、ファイルの読み込み時に実行されるようにする。
atom.workspace.onDidOpen->unlessdetectVimModeLine()detectEncoding()detectVimModeLine = ->editor = atom.workspace.getActiveTextEditor()lineEnding: {unix: "\n"dos: "\r\n"mac: "\r"}tryfirstLine = editor.lineTextForBufferRow(0)lastLine = editor.lineTextForBufferRow(editor.getLastBufferRow())catcherrorreturnfalseoptions = parseVimModeLine(firstLine)?parseVimModeLine(lastLine)returnfalseunlessoptionsencoding = options.fileencoding?options.fencformat = options.fileformat?options.fftype = options.filetype?options.ftsetLineEnding(lineEnding[format])setFileType(type)unlesssetEncoding(encoding)detectEncoding()returntrueparseVimModeLine = (line) ->regexp = /(vi|vim|ex):\s*(se(t)*)*\s+([^:]+)*\s*:/matches = line.matchregexpoptions = nullifmatchesoptions = {}foroptioninmatches[4].split(" ")[key,value]=option.split"="options[key]=value?trueifkey!=""optionssetEncoding = (encoding) ->iconv = require'iconv-lite'editor = atom.workspace.getActiveTextEditor()returnfalseunlessiconv.encodingExists(encoding)encoding = encoding.toLowerCase().replace(/[^0-9a-z]|:\d{4}$/g,'')editor.setEncoding(encoding)returntruesetLineEnding = (lineEnding) ->editor = atom.workspace.getActiveTextEditor()buffer = editor?.getBuffer()returnunlessbufferbuffer.setPreferredLineEnding(lineEnding)buffer.setText(buffer.getText().replace(/\r\n|\r|\n/g,buffer.getPreferredLineEnding()))setFileType = (type) ->editor = atom.workspace.getActiveTextEditor()returnunlesseditorgrammar = atom.grammars.selectGrammar(type)ifgrammarisntatom.grammars.nullGrammaratom.grammars.setGrammarOverrideForPath(editor.getPath(),grammar)editor.setGrammar(grammar)detectEncoding = ->editor = atom.workspace.getActiveTextEditor()tryfilePath = editor.getPath()catcherrorreturnreturnunlessfs.existsSync(filePath)jschardet = require'jschardet'fs.readFilefilePath,(error, buffer) =>returniferror?{encoding}=jschardet.detect(buffer)?{}encoding = 'utf8'ifencodingis'ascii'setEncoding(encoding)
detectEncoding
はencoding-selectorのdetectEncoding
のうち、文字コード判定箇所のみを抜き出したものとなる。
モードラインによる判定に失敗した場合はfalse
を返すようにしているので、モードラインそのものが設定されていなかったり、利用できない文字コードが設定されている場合はファイルの内容を読んで文字コードを判定している。
また、本来のモードラインの仕様であれば先頭または末尾の数行を読むようになっているが、この実装では先頭1行、末尾1行しか読んでいない。
これは筆者の環境では先頭または末尾にしか書いていないからなので、本来の実装に沿うのであればこの部分を書き換える必要がある。