koturnの日記

普通の人です.ブログ上のコードはコピペ自由です.

各種言語における標準入力からのEOFまでの入力サンプル

はじめに

昨今,CodeIQpaizaといったサービスが登場し,自動採点型のプログラミングの問題に取り組む人が増えている(と思う). 自動採点型のプログラミングの問題では,基本的に標準入力から読み込みを行い,をれに応じて適切な出力をするというものだ. 入力ケースは何パターンか与えられ,その度に送信されたプログラムを実行する仕組みとなっている. このような標準入力から読み込みを行い,標準出力がちゃんとしたのであるかどうかを判定し,自動採点を行う形式のプログラミングの問題は,AOJAtCoderyukicoderなどの競技プログラミングサイトでは一般的なものであったが,CodeIQ,paizaなどの台頭により,より広く認知されるようになったのではないかと思う.

この記事では,僕の忘備録も兼ねて,各種言語における標準入力と標準出力のサンプルを掲載する. というのも,CodeIQの入出力サンプルにはヒドい例が掲載されている言語もあったからだ. 特に,Javaのサンプルは,

import java.io.*;
class Main {
    public static void main(String[] args) throws IOException {
        int c;
        while ((c = System.in.read()) != -1)
            System.out.println(c);
    }
}

となっており,何故か1文字ごとに読み込みを行うようになっている. また,この手の入出力サンプルは1行だけ読み込んで,それをそのまま処理というケースしか書かれていない.

とりあえず,

  1. 標準入力をEOFまで読み込み,
  2. 入力をトークン分割し,
  3. トークンを整数型に変換して.
  4. 標準出力に出力する

という単純なサンプルを掲載する. 具体的な入力としては,

10 20
30 40
50 60
70 80

のようなものだ.

ただ,実際にはデータ数(読み込めばよい行数)が1行目に与えられる問題が多く,EOFまで読み込む必要はないが,たまにEOFまで読み込まなければならない問題があるので,EOFまでの読み込みの例を記載するわけだ. 指定回数を読み込む方法については,各言語におけるループの仕方を調べればよいだけであり,ググれば簡単にヒットするし,ループが記述できないようでは問題自体も解けないであろうから割愛する.

言語

本記事で取り扱う言語は以下の通りである.

C

まずはC言語から. まぁ,C言語で問題を解く人はいないだろうが,サンプルとして掲載する.

#include <stdio.h>
#include <stdlib.h>

#define LINE_BUF_SIZE  1024


int
main(void)
{
  static char line[LINE_BUF_SIZE];

  while (fgets(line, sizeof(line), stdin) != NULL) {
    int a, b;
    if (sscanf(line, "%d %d", &a, &b) != 2) {
      fputs("sscanf: Convert error\n", stderr);
      return EXIT_FAILURE;
    }
  }
  return EXIT_SUCCESS;
}

競プロにおける入力はある程度入力される文字列の長さが想定できるので,それなりのバッファ容量を確保し, scanf() を用いても問題はないだろうが, scanf() を使ってるだけで気持ち悪いと感じるので, fgets()sscanf() を用いることにする. EOFに到達した場合, fgets()NULL を返却するので,それをEOFの判定に利用する. また, sscanf() の返り値は,変換に成功した個数なので,うまく整数に変換できたかどうかの確認に利用する.

なお,64bit整数を sscanf() で読み込むにあたってはやや問題があり, "%lld" といった書式指定文字列を用いないとうまくいかないかもしれない. これはC99から正式に採用され,それ以前ではコンパイラによっては非標準として実装されているレベルなので,やや気持ち悪いかもしれない.

C++

C++を用いている人は多いので,最早説明の必要はないだろう.

#include <cstdlib>
#include <iostream>


int
main()
{
  std::cin.tie(0);
  std::ios::sync_with_stdio(false);

  int a, b;
  while (std::cin >> a >> b) {
    std::cout << a << " " << b << std::endl;
  }
  return EXIT_SUCCESS;
}

一応解説しておくと,

std::cin.tie(0);

std::coutstd::cin の結び付きを解除し,

std::ios::sync_with_stdio(false);

で, stdio との同期を切る. これによって, std::coutstd::cin を用いた入出力を高速化できる. これは, std::cout / std::cin を用いる場合には有効である.

もっとも, std::printf() を用いた方がフォーマット出力は楽なので, std::cout の代わりに std::printf を用いている人も多いだろう. <iostream><cstdio> 系の関数を混在させる場合は,前述の高速化を用いてはいけない.

上記の例では,EOFの判定は std::cin のbool変換演算子を用いているが, std::cin.eof()true かどうかでEOFを判定するという手段もある.

なお,ガチ勢は using namespace std; と膨大なマクロとインクルードを記述したものを用いるだろう.

C Sharp

基本的な部分の解説の必要は無いだろう. C#の場合, Console.WriteLine() はデフォルトだと自動的にフラッシュされる設定になっているので,この自動フラッシュをオフにし,最後にまとめてフラッシュすることで,出力の部分の高速化が期待できる.

using System;
using System.IO;

class MainClass {
    static void Main() {
        Console.SetOut(new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = false });

        string line;
        while ((line = Console.ReadLine()) != null) {
            string[] tokens = line.Split(' ');
            int a = int.Parse(tokens[0]);
            int b = int.Parse(tokens[1]);
            Console.WriteLine(a + " " + b);
        }

        Console.Out.Flush();
    }
}

IEnumerable の操作を用いて,文字列から整数への変換を以下のように書くのもよいだろう.

using System;
using System.IO;

class MainClass {
    static void Main() {
        Console.SetOut(new StreamWriter(Console.OpenStandardOutput()) { AutoFlush = false });

        string line;
        while ((line = Console.ReadLine()) != null) {
            int[] tokens = line.Split(' ').Select(int.Parse).ToArray();
            int a = tokens[0];
            int b = tokens[1];
            Console.WriteLine("{0} {1}", a, b);
        }

        Console.Out.Flush();
    }
}

VB.NET

VB.NETはよくわからないので,C# のものを単純に翻訳してみただけだ. Line予約語らしいので,読み込んだ一行は inputtedLine という名前の変数に格納する.

Imports System
Imports System.IO

Public Class MainClass
    Shared Sub Main(args As String())
        Console.SetOut(New StreamWriter(Console.OpenStandardOutput()) With { .AutoFlush = false })

        Dim inputtedLine As String = Console.ReadLine()
        While (Not inputtedLine Is Nothing)
            Dim tokens As Integer() = inputtedLine.Split(" ").Select(Function(token) Integer.Parse(token)).ToArray()
            Dim a As Integer = tokens(0)
            Dim b As Integer = tokens(1)
            Console.WriteLine("{0} {1}", a, b)

            inputtedLine = Console.ReadLine()
        End While
        Console.Out.Flush()
    End Sub
End Class

Java

Javaはバージョン毎に進化しているので,Java7の場合とJava8の場合に分けて入出力サンプルを掲載する. Java6以下はもう滅びたと考えてよいので,サンプルを掲載する必要はないだろう. なお,基本的に例外処理は記述せず, main() メソッドからthrowする形で記述する. というのも,基本的に例外が発生した時点で,問題に正答することはできなくなっていると考えられるからだ.

Java7

Java7からtry-with-resource文が実装されたので, System.in の読み込み用のオブジェクトのクローズは自動的に行わせる.

import java.io.*;

public class Main {
    public static void main(String[] args) throws Exception {
        try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) {
            String line;
            while ((line = br.readLine()) != null) {
                String[] tokens = line.split(" ");
                int a = Integer.parseInt(tokens[0]);
                int b = Integer.parseInt(tokens[1]);
                System.out.println(a + " " + b);
            }
        }
    }
}

java.util.Scanner を用いた場合,split() メソッドによるトークン分割,整数型等への変換処理を自前で書かなくてもよくなる. ただし,java.util.Scanner は遅いという話があるので,入力数が多い場合は気をつけなくてはならない.

import java.util.*;

public class Main {
    public static void main(String[] args) throws Exception {
        try (Scanner sc = new Scanner(System.in)) {
            while (sc.hasNextInt()) {
                int a = sc.nextInt();
                int b = sc.nextInt();
                System.out.println(a + " " + b);
            }
        }
    }
}

Java8

Java8になって, java.io.BufferedReaderlines() メソッドという,Streamを生成するAPIが追加された. このメソッドを用いることで,標準入力からの読み込みをカッコよく書ける. Java8を用いて問題を解く場合,Streamを用いて楽に記述することも多いだろうから,そういう意味でも親和性が高いはずだ.

import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws Exception {
        try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) {
            br.lines()
                .map(line -> Arrays.stream(line.split(" "))
                        .mapToInt(Integer::parseInt)
                        .toArray())
                .forEach(tokens -> {
                    int a = toknes[0];
                    int b = toknes[1];
                    System.out.println(a + " " + b);
                });
        }
    }
}

もし,分割されたトークンを何らかのコンストラクタに突っ込み,目的とするオブジェクトの型の配列に変換したいのであれば,以下のようにするとよいだろう. この例では,各トークンから BigDecimalインスタンスを生成している. map() メソッドから返却されるストリームの toArray() メソッドは,引数が無い場合, Object 型の配列を返却するので,引数に目的の型の配列を生成する new の参照(メソッド参照?)を渡す必要がある.

import java.io.*;
import java.math.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws Exception {
        try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) {
            br.lines()
                .map(line -> Arrays.stream(line.split(" "))
                        .map(BigDecimal::new)
                        .toArray(BigDecimal[]::new))
                .forEach(tokens -> {
                    BigDecimal a = toknes[0];
                    BigDecimal b = toknes[1];
                    System.out.println(a + " " + b);
                });
        }
    }
}

なお,1行目にデータ数nが与えられる場合は,

        try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) {
            int n = Integer.parseInt(br.readLine());
            br.lines()
              // ...
        }

のように,事前に1回 readLine() メソッドを呼び出し,ファイルストリームの読み書き位置を移動しておくとよい.

なお, java.util.Scanner にStreamを生成するものはないらしいので,Stream APIを用いたいなら, java.io.BufferedReader を使おう.

Python

Pythonは2と3で大きく仕様が変更されているので,別々に記述する. ただ, fileinput をimportし,標準入力を 1行1行読み取るようにした場合,標準入力からの読み込みに関しては,Python2とPython3 の両対応ができる.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import fileinput


if __name__ == '__main__':
    for line in fileinput.input():
        tokens = line.strip().split()
        a, b = int(tokens[0]), int(tokens[1])

Python 2

fileinput.input() を用いる場合は以下のようになる.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import fileinput


if __name__ == '__main__':
    for line in fileinput.input():
        tokens = map(int, line.strip().split())
        a, b = tokens[0], tokens[1]
        print a, b

ただ,標準入力といえば, raw_input() を用いるのが楽である.raw_input() は1行の入力末尾の改行文字を削除してくれるので,自分で strip() メソッドを呼び出す必要が無くなる.

入力データ数 n が与えられる場合,リスト内包表記を用いて [raw_input() for i in range(n)] とすることで,指定された行数の行を一気にリストとして取得できる.

#!/usr/bin/env python
# -*- coding: utf-8 -*-


if __name__ == '__main__':
    n = int(raw_input())
    for line in [raw_input() for i in range(n)]:
        tokens = map(int, line.strip().split())
        a, b = tokens[0], tokens[1]
        print '%d %d' % (a, b)

整数に変換する部分をまとめて,以下のようにまとめるのもよいだろう.

#!/usr/bin/env python
# -*- coding: utf-8 -*-


if __name__ == '__main__':
    n = int(raw_input())
    for tokens in map(lambda line: map(int, line.strip().split()), [raw_input() for i in range(n)]):
        a, b = tokens[0], tokens[1]
        print '%d %d' % (a, b)

Python 3

Python3になってから,print 文が廃止され, print() 関数になってしまった. また, map() の返り値がイテレータになったので,リストに変換するために list() 関数をかませる必要がある.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import fileinput


if __name__ == '__main__':
    for line in fileinput.input():
        tokens = list(map(int, line.strip().split()))
        a, b = tokens[0], tokens[1]
        print(a, b)

Python3では raw_input() の代わりに input() を用いないといけない. Python2の例と同様に,リスト内包表記を用いることで,指定行数だけ入力をリストとして一気に受け取ることができる.

#!/usr/bin/env python
# -*- coding: utf-8 -*-


if __name__ == '__main__':
    n = int(input())
    for tokens in map(lambda line: list(map(int, line.strip().split())), [input() for i in range(n)]):
        a, b = tokens[0], tokens[1]
        print('%d %d' % (a, b))

Ruby

特に解説することはない.

#!/usr/bin/env ruby
# -*- coding: utf-8 -*-

if __FILE__ == $0
  while line = STDIN.gets
    tokens = line.chomp!.split.map!(&:to_i)
    a, b = tokens[0], tokens[1]
    puts "#{a} #{b}"
  end
end

Perl

Perlはほとんど書いたことはないが,以下のような形になるだろう. $line =~ s/\s+$//; とすることで,入力のstripを行うことができる.

#!/usr/bin/env perl

use strict;
use utf8;
use warnings;


if ($0 eq __FILE__) {
  while (defined(my $line = <STDIN>)) {
    $line =~ s/\s+$//;
    my @tokens = split(/ +/, $line);
    my $a = $tokens[0];
    my $b = $tokens[1];
    printf("%d %d\n", $a, $b);
  }
}

PHP

PHPには詳しくないので,以下のような形でよいのかどうかも怪しい.

#!/usr/bin/env php
<?php
if (basename(__FILE__) == basename($_SERVER['PHP_SELF'])) {
  while ($line = fgets(STDIN)) {
    $tokens = split(' +', $line);
    echo $tokens[0];
    echo $tokens[1];
  }
}
?>

Go

最近,何かと人気のGo言語である. fmt.Scan() を用いるのであれば,以下のようにするとよいだろう.

package main

import (
  "fmt"
)


func main() {
  var a int
  var b int
  for _, err := fmt.Scan(&a, &b); err == nil; _, err = fmt.Scan(&a, &b) {
    fmt.Println(a, b)
  }
}

bifio が利用可能な環境であれば,以下のようにしてもよさそうだ.

package main

import (
  "bufio"
  "fmt"
  "os"
  "strconv"
  "strings"
)


func main() {
  scanner := bufio.NewScanner(os.Stdin)

  for scanner.Scan() {
    tokens := strings.Split(scanner.Text(), " ")
    a, _ := strconv.Atoi(tokens[0])
    b, _ := strconv.Atoi(tokens[1])
    fmt.Println(a, b)
  }
  if err := scanner.Err(); err != nil {
    fmt.Fprintln(os.Stderr, "reading standard input:", err)
  }
}

Lua

Luaは文字列の分割が無いので,自分で実装する必要がある. なかなかゴチャゴチャとした見た目になる.

なお,Luaのコメント形式は -- foo もしくは --[[bar]] という形式であるが,1行目のみshebangが許容されるようになっているので,ありきたりなshebangを記述している.

#!/usr/bin/env lua

if ... then
  module(..., package.seeall)
end


function split(str, delim)
  if string.find(str, delim) == nil then
    return { str }
  end
  local result = {}
  local pat = '(.-)' .. delim .. '()'
  local lastPos
  for part, pos in string.gfind(str, pat) do
    table.insert(result, part)
    lastPos = pos
  end
  table.insert(result, string.sub(str, lastPos))
  return result
end


if not ... then
  line = io.read()
  while line do
    tokens = split(line, ' +')
    print(tonumber(tokens[1]))
    print(tonumber(tokens[2]))
    line = io.read()
  end
end

Javascript (Node)

Node.jsは,標準入力をイベントとして感知することができるようになっている. ただ,受け取った時点で順次処理をしていくのはかなり限定的なケースになると思われるので,入力を配列に格納し,EOFのイベントを検知したら,処理を行うという汎用的な形にして記載する.

(function() {
  'use strict';

  var lines = [];
  require('readline').createInterface({
    input: process.stdin,
    output: process.stdout
  }).on('line', function(line) {
    lines.push(line);
  });
  process.stdin.on('end', function() {
    lines.forEach(function(line) {
      var tokens = line.split(' ').map(Number);
      console.log(tokens[0] + ' ' + tokens[1]);
    });
  });
})();

何となくスコープ化しておいたが,ライブラリを作るわけでもないので,じゃんじゃかグローバル変数,関数を生やしても問題はないはずではある. しかし,気持ちの問題として,スコープ化したくなってしまうものだ.

AtCoderのサンプルのように,

(function(input) {
  'use strict';

  input.split("\n").forEach(function(line) {
    var tokens = line.split(' ').map(Number);
    console.log(tokens[0] + ' ' + tokens[1]);
  });
})(require('fs').readFileSync('/dev/stdin', 'utf8'));

としてもよいが,手元の環境がWindowsであるなら,このサンプルは /dev/stdin が無いため動作しない. したがって,Windows環境でも動作する前者のサンプルをオススメする.

Scheme

標準入力には read 関数を用いることで,分割されたトークンを読み込むことができる. ただし,readLisp式を読み込む関数なので, 半角カッコがある入力の受け取りに用いることはできない.

(define (main-function)
  (letrec ((main-loop (lambda ()
                        (let ((a (read)) (b (read)))
                          (if (or (eof-object? a)
                                  (eof-object? b))
                            #f
                            (begin (display a)
                                   (display " ")
                                   (display b)
                                   (newline)
                                   (main-loop)))))))
    (main-loop)))

(main-function)

Common Lisp

Schemeと同様に read 関数を用いて,読み込みを行うことで,トークン毎の読み込みが可能となる. Common Lispでは末尾再帰最適化の保証がされないため,ループマクロが好んで用いられるという点も踏まえると,以下のようにするのがよいのだろうか?

(defun main ()
  ; (declare (optimize (safety 0) (space 0) (debug 0) (speed 3)))
  (loop for a = nil then b
        and b = nil then
        (progn (princ a)
               (princ b))
        until (or (null (setq a (read)))
                  (null (setq b (read))))))

(main)

もし,1行読み込んで,半角スペースでsplitするなら,以下のようにするのがよいだろう.

(defun split-by-one-space (string)
  ; (declare (optimize (safety 0) (space 0) (debug 0) (speed 3)))
  (loop for i = 0 then (1+ j)
        as j = (position #\Space string :start i)
        collect (subseq string i j)
        while j))

(defun main ()
  ; (declare (optimize (safety 0) (space 0) (debug 0) (speed 3)))
  (loop for line = nil then
        (let ((tokens (mapcar #'parse-integer
                              (split-by-one-space line))))
          (princ tokens))
        until (null (setq line (read-line)))))

(main)

なお,上記の2つの例でコメントアウトしている部分(optimize宣言をしている部分)は,最適化指示をしている部分である. テストがうまく通った後はコメントアウトを外すと,高速化が期待できるだろう.

Clojure

Clojureはほぼ触ったことが無いが,Common Lispと同じノリで書けると信じて,書いてみる.

(defn main
  []
  (loop [line (read-line)]
    (if (nil? line)
      nil
      (do (let [tokens (map #(Long/parseLong %) (.split line " +"))]
            (println tokens))
          (recur (read-line))))))

(main)

多分,普段からClojureを書いている人から見れば,ツッコみどころが満載だが,とりあえずはこの形式で動作させることができると思う. CodeIQのサンプルでは再帰を用いているが,Clojureでは末尾再帰最適化は保証されないと聞いたような気がするので, looprecur を用いた方が良いのではないかと思った.

bash

なかなか用いる機会は少ないが,bashで問題を解く場合は以下のようになる.

#!/bin/bash -eu

declare -i a b
while read a b; do
  echo "$a $b"
done

read コマンドはEOFを読み込んだとき,非0を返却するので,それを利用して,EOFまでの読み込みを行う. declare -i a b の行は無くても動作するが,シェル変数 ab を整数型として宣言しておくと気分が良いと思う.

Vim Script on bash

環境はかなり限られるが,bashからVim Scriptを実行することもできるらしい

#!/bin/bash -eu

vim -u NONE -i NONE -N -n -e -s -S <(cat <<EOF
function! s:main(lines) abort
  let answer = ''
  for line in a:lines
    let tokens = map(split(line, ' '), 'str2nr(v:val)')
    let [a, b] = [tokens[0], tokens[1]]
    answer .= printf("%d %d\n", a, b)
  endfor
  return answer
endfunction

let s:lines = getline(1, '$')
enew
put =s:main(s:lines)
1 delete _
%print
EOF
) <(cat)

CodeIQではbashが使えるが,vimを用いることができなかった. しかし,AtCoderでは利用可能であるらしい.

今後

D言語ScalaHaskell,RやSwift等についても調べたい. 余裕があればAWKも.... おそらく,この記事に追記する形で記載していくと思う.

参考

CodeIQのスクエア・カルテット問題を解いた

はじめに

CodeIQで@riverplus氏による「 スクエア・カルテット」問題 という,初等整数論を絡めた面白いプログラミングの問題があったので,それについての記事を書いた. 高校生の数学を思い出す良い問題であり,楽しかった.

問題

2つの自然数の組 $(a, b)$ が与えられたとき,自然数 $x, y$ に関する次の方程式を考えます.

\begin{equation} x^2 + a^2 = y^2 + b^2 \label{eq:given-equation} \end{equation}

例えば, $(a, b) = (3, 10)$ のとき,方程式(\ref{eq:given-equation})の解は $(x, y) = (10, 3), (46, 45)$ の2組です.

自然数の組 $(a, b)$ に対し,方程式(\ref{eq:given-equation})の全ての解の $x + y$ の和を $F(a, b)$ と定義します. 例えば $F(3, 10) = 10 + 3 + 46 + 45 = 104$ です. 同様に, $F(10, 50) = 3500$ , $F(20, 100) = 15022$ となることが確かめられます.

標準入力から,半角空白区切りで 2つの自然数 $a, b$ ( $1 \leq a < b \leq 10^5$ )が与えられます. 標準出力に $F(a, b)$ の値を出力するプログラムを書いてください.

考え方

ここでは,$0 \notin \mathbb{N}$ とする.

与式(\ref{eq:given-equation})を変形して,

\begin{equation} (x + y)(x - y) = b^2 - a^2 = n \label{eq:converted} \end{equation}

とおく($a, b \in \mathbb{N}$ かつ $a > b$ より $n \in \mathbb{N}$). よって, $x + y \in \mathbb{N}$ , $x - y \in \mathbb{N}$ . 式(\ref{eq:converted})より,$x$, $y$ に関する連立方程式

\begin{eqnarray} \begin{cases} x + y = p & \\ x - y = q & \end{cases} \label{eq:xypq} \end{eqnarray}

を得る($p, q \in \mathbb{N}$ かつ $n = pq$). $i$ 番目の解 $(x_i, y_i)$ に $p_i, q_i$ が対応すると考えると,出力すべき値は,

\begin{equation} F(a, b) = \sum_i (x_i + y_i) = \sum_i p_i = \sum_i \dfrac{n}{q_i} \end{equation}

である. 連立方程式(\ref{eq:xypq})を解くと,

\begin{equation} (x, y) = \left( \dfrac{p + q}{2}, \dfrac{p - q}{2} \right) \end{equation}

を得る. $x \in \mathbb{N}$ なので,

\begin{equation} (p + q) \bmod 2 = 0 \label{eq:pq-constrain} \end{equation}

また,$x, y \in \mathbb{N}$ なので,

\begin{equation} x - y = q < p = x + y \end{equation}

$p, q$ は $n$ を2つの自然数積に分解したもの,すなわち,$n$ の約数のペアであることを踏まえると,

\begin{equation} (1 \leq) \:\: q < \sqrt{n} < p \:\: (\leq n) \end{equation}

以上より, $\sqrt{n}$ より小さく(「以下」ではない),かつ条件(\ref{eq:pq-constrain})を満たす $n$ の約数 $q_i$ を全て見つけ出し,対になる約数 $p_i = \dfrac{n}{q_i}$ の総和 $\sum_i p_i$ を計算して,出力すると良い.

おまけ

$a < b$ という制約が無く,

  1. $a = b$ , すなわち $n = 0$
  2. $a > b$ , すなわち $n < 0$

である場合も,思考実験として考えてみる.

$n = 0$ の場合, $(x, y)$ は $x = y$ なる任意の自然数

$n < 0$ の場合,$x + y > 0$ なので,

\begin{equation} x - y = q < 0 \end{equation}

となり, $q$ がマイナス符号を担当する($p \in \mathbb{N}, q \in \mathbb{Z}$). 連立方程式(\ref{eq:converted})を

\begin{eqnarray} \begin{cases} y + x = p & \\ y - x = -q & \end{cases} \end{eqnarray}

と変形し, $y \rightarrow x'$ , $x \rightarrow y'$ , $p \rightarrow p'$ , $-q \rightarrow q' (> 0)$ と置き直すことで, $n > 0$ のときと同様に処理できる.

\begin{eqnarray} \begin{cases} x' + y' = p' & \\ x' - y' = q' & \end{cases} \end{eqnarray}

$-n (> 0)$ (マイナス符号が無い場合)と比較して, $(x, y)$ の組み合わせは逆転しているが,出力すべき値はその和

\begin{equation} F(a, b) = \sum_i (y'_i + x'_i) = \sum_i p'_i = \sum_i \dfrac{-n}{q'_i} \end{equation}

なので, $n$ と $-n$ のときの $F(a, b)$ は等しい. すなわち, $F(a, b) = F(b, a)$ .

まとめ

  1. $n = b^2 - a^2$ ( $n = | b^2 - a^2 |$ ) とし, $\sqrt{n}$ より小さく,$\left( q + \dfrac{n}{q} \right) \bmod 2 = 0$ となる $n$ の約数 $q_i$ を全て求める
  2. $\sum_i \dfrac{n}{q_i}$ を出力する

本番入力値と考察

以下の6ケースが本番での入力値であった.

10 26
11 389
123 456
35672 61243
71200 82321
19126 98765

1つ目のケース $(a, b) = (10, 26)$ は, $b^2 - a^2 = 576 = 24^2$ となり,コーナーケースであった($p = q = 24$ ,すなわち $(x, y) = (24, 0)$ を含めてしまうのは誤り). また,問題文の例にあった $(a, b) = (10, 50)$ は, $b^2 - a^2 = 576 = 2400$ となり,$2400$ は $\lfloor \sqrt{2400} \rfloor = 48$ を約数に持つので,(ある意味,前述のものと対になる)コーナーケースであった.

この2つのケースを考えると, $n = b^2 - a^2$ の約数を単純に $1, 2, \ldots, \lfloor \sqrt{n} \rfloor - 1$ から見つけ出すのは,本番ケースに限るならばうまくいくが誤りである. 解決策としては,$n$ が2乗数であるかどうかを判定し,範囲を調製しなければならないが,コードで書くと汚くなる上に面倒である. そこで,$1, 2, \ldots, \lfloor \sqrt{n - \epsilon} \rfloor$ ($\epsilon$ は十分に小さな正の実数)から約数を探索するようにすると単純に処理できるはずだ.

解答例

bashJavaC++で解答での解答例を紹介する. $ \epsilon = 1.0 \times 10^{-10}$ とした.

ちなみに,入力は各ケースにつき1行のみだったので,それぞれの解答例のように,whileでEOFまで読み込みを行う必要はない.

bashでの解答

あえて,bashで解くというのも面白い.

#!/bin/bash -eu

declare -i a b
while read a b; do
  declare -i n=$((b ** 2 - a ** 2))
  declare -i qMax=`echo "sqrt($n - 0.00000000001)" | bc | sed 's/\.[0-9]*$//g'`
  declare -i q answer=0
  for q in `seq 1 $qMax`; do
    (( n % q == 0 )) && (( (q + n / q) % 2 == 0 )) && (( answer += n / q ))
  done
  echo $answer
done

bashだと,下手に組むとTLEになるので,やや難易度は高かった(1秒の壁は大きい). expr コマンドは時間がかかるので,基本的にbashの算術式で計算し,平方根などの算術式では計算できないものは bc コマンドに投げて計算するだけだ.

なお,以下のようなbashの算術式のfor文

for ((i = 0; i < 100; i++)) {
  # 処理
}

だと時間がかかるので, seq コマンドで $1$ から $\lfloor \sqrt{n - \epsilon} \rfloor$ までの連続する整数のリストを生成し,通常のシェルのforを用いるとよい. また,ifのパースは時間がかかると予想できるので,短絡評価を利用し,bashの算術式を繋げるとよいだろう.

Javaでの解答

まともな言語,例えばJavaでは以下のように率直に書けばよいだろう.

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.stream.LongStream;

public class Main {
    public static void main(String[] args) throws Exception {
        try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) {
            br.lines()
                .map(line -> Arrays.stream(line.split(" "))
                        .mapToLong(Long::parseLong)
                        .map(x -> x * x)
                        .toArray())
                .map(inputs -> inputs[1] - inputs[0])
                .map(n -> LongStream.rangeClosed(1, (long) Math.sqrt(n - 1e-10))
                        .filter(q -> n % q == 0 && ((q + n / q) & 0x01) == 0)
                        .map(q -> n / q)
                        .sum())
                .forEach(System.out::println);
        }
    }
}

Javaでは2の累乗の定数の乗算,除算,剰余はビット演算に置き換えた方が速い. C/C++ならば,2の累乗の定数の乗算,除算,剰余は最適化オプションを付与しなくてもビット演算に置き換えられるが,JavaコンパイラJITコンパイルのためにバイトコードを最適化しないようになっているため,2の累乗の定数の乗算,除算,剰余は,コンパイル後のバイトコードにおいても乗算,除算,剰余のままである. これは生成されるバイトコードを見れば一目瞭然である. しかし,JITコンパイルにより最適化されるかといえばそうではなく,手動でビット演算に置き換えた方が高速に動作する.

なお, $b^2 < 10^{10}$ なので,64bit整数型を用いないとオーバーフローする. この点は気をつけないといけない.

C++での解答

みんな大好きC++で書くと,以下のようになる. for文で逐次的に処理するので,連続する整数の配列やリストを生成する必要がなく, $\lfloor \sqrt{n - \epsilon} \rfloor$ の議論が不要になる. 大半の人はこういった解答をしていると思う. 試していないが,ループ毎に $q^2$ を計算するので, $\lfloor \sqrt{n - \epsilon} \rfloor$ を計算しておく場合と比較すると遅い気がするのだが,実際はどうなのだろうか?

#include <cstdlib>
#include <iostream>

typedef long long  llint;


int
main()
{
  std::cin.tie(0);
  std::ios::sync_with_stdio(false);

  llint a, b;
  while (std::cin >> a >> b) {
    llint n = b * b - a * a;
    llint answer = 0;
    for (llint q = 1; q * q < n; q++) {
      if (n % q != 0) continue;
      llint p = n / q;
      if ((p + q) % 2 == 1) continue;
      answer += p;
    }
    std::cout << answer << std::endl;
  }

  return EXIT_SUCCESS;
}

候補絞り込み型インターフェースを提供するプラグインについて

この記事は Vim Advent calendar 2015 の16日目の記事です.

僕は普段「だ・である」調でブログを書いていますが,今回の記事は多数の人に見ていただくことを考慮して,多くの人がブログで採用している「です・ます」調,語り口調で書いていくことにしましょう.

さて本題です. 候補を絞り込むインターフェースというのは人気で,Vimではunite.vimctrlp.vimなどが有名ですね. そして,コマンドラインツールfzfVimからも利用できるように,本体にVimプラグインが付属しています. また,ctrlp.vimにインスパイアされて開発されたプラグインとして,LeafCageさんによるalti.vimやkamichiduさんによるvim-milqiがあります. この記事では,前述の5つの候補絞り込み型インタフェースを提供するVimプラグイン

について,簡単な解説と比較をしたいと思います. FuzzyFindervim-kuについては取り上げません.

基本操作の比較

unite.vim

unite.vimでは,本体にたくさんの拡張が付属しており,第三者(特に日本人に多い)によって開発された拡張も多数公開されているので,自分で拡張を書く必要は無いですね. 基本的に, :Unite [unite source名] とすることで,unite.vimを起動できます.

unite.vim上でのキーマッピングは以下のようになっています. unite.vimは他の候補絞り込み型プラグインと異なり,ノーマルモードとインサートモードがあり,それぞれのモードで操作が異なります.

キーマッピング

ノーマルモード
キー 動作
i / I インサートモードに
a 候補選択時はアクションを選択,そうでなければ A と同じ
A 入力欄の末尾にカーソル移動をしてインサートモード
q or <C-g> unite.vim を終了し,1つ前のuntieバッファメニューを復元する
Q or g<C-g> unite.vim を終了する
<C-r> uniteを再起動する
<Space> / <S-Space> 候補の選択状態をトグル(複数選択できるsourceのみ)
* 全ての候補選択をトグルする(選択状態が反転する)
M 表示候補数を制限する
<Tab> アクションを選択する
<C-p> / <C-n> sourceを切り替える
<C-a> / <C-k> ログ / 候補を echo する
<C-l> 再描画する
<C-h> 1つ前のパスを削除する
gg or <C-Home> カーソルを1番上に
G or <C-End> カーソルを1番下に
j or <Down> カーソルを1つ下に
k or <Up> カーソルを1つ上に
J / K マッチしない候補をスキップしてカーソルを上 / 下に移動
g? 簡単なuniteのキーマッピングのヘルプを表示
N 候補を追加する(action_tableunite__new_candidate が定義されている場合のみ)
. ドットを絞り込み欄に入力し,インサートモードに
<CR> デフォルトアクションを行う
b ブックマークのアクションを行う
d 削除アクションを行う
e narrowアクションを行う
t 別のタブで開く
yy ヤンクアクションを行う
o オープンアクションを行う
x クイックマッチを用いて,デフォルトのアクションを行う(マークされた候補があるとダメ)
インサートモード
キー 動作
<Esc> ノーマルモード
<Tab> アクションを選択する
<C-n> or <Down> 次の行を選択
<C-p> or <Up> 前の行を選択
<C-f> / <C-b> 前 / 次の行を選択
<C-h> or <BS> カーソルの1つ前の文字を削除
<C-u> カーソルの前の入力を全て削除
<C-w> カーソルの前の単語を削除
<C-a> or <Home> カーソルを先頭に
<Left> / <Right> カーソルを右 / 左に移動
<C-l> 再描画する
<C-g> uniteを終了する
ビジュアルモード
キー 動作
<Space> 選択された範囲の候補全ての選択状態をトグル

本体付属のunite source

以下の表は,unite.vim本体から提供されているunite sourceの一部です. デフォルトのアクションについても併記しています.

他にもたくさんのunite sourceが付属しているので,調べてみると面白いと思います.

unite source名 機能
bookmark UniteBookmarkAdd でブックマークされたファイルを表示し,選択されたファイルを開く
buffer リストされているバッファ一覧を表示し,選択されたバッファを開く
change ファイルの変更履歴(:changeの結果)を表示し,選択された位置に移動
command 利用できるコマンドを表示し,選択されたコマンドをコマンドウィンドウに入力(引数のヒントも表示)
directory カレントディレクトリ以下のディレクトリを表示し,ファイラのように利用できる
file カレントディレクトリ以下のファイルとディレクトリを表示し,ファイラのように利用できる
file_rec カレントディレクトリ以下のファイルを再帰的に検索して表示し,ファイラのように利用できる
find 対象ディレクトリと名前を入力させて,findコマンドを実行した結果を表示
function 利用できる関数を表示し,選択された関数をコマンドウィンドウに入力(引数のヒントも提示)
grep 外部コマンド grep を行うように促し,grepを行った結果を一覧として表示.選択された位置に移動する
history/unite 過去に利用したunite sourceの履歴を表示
jump ジャンプリスト(:jumpsの結果)を表示し,選択された位置に移動
launcher パスが通っているところから,実行可能ファイルを候補として表示する
line 現在のバッファの全ての行を候補として表示し,選択された行に移動
mapping 全てのキーマッピングを表示し,選択されたものを実行する
output Vimコマンドを入力を促し,入力されたコマンドの出力を候補とする
process 起動中のプロセス一覧を表示し,選択した候補にTERMシグナルを送る(確認あり)
register Vimレジスタを表示し,選択されたレジスタの内容を現在位置に挿入
runtimepath runtimepath に追加されているディレクトリ一覧を表示し,選択されたディレクトリに移動
source unite sourceの一覧を表示し,選択されたsourceをもとにUniteを起動
tab 現在利用中のVimのタブ一覧(とタブ内のバッファをヒントとして)を表示し,選択されたタブに移動
vimgrep :vimgrep を行うように促し,vimgrepを行った結果を一覧として表示.選択された位置に移動する
window 現在のタブのウィンドウ一覧を表示し,選択されたウィンドウに移動

ctrlp.vim

プラグイン名になっているように,デフォルトでは <C-p> でCtrlPを起動します. あるいは, :CtrlP というコマンドで起動してもよいでしょう.

unite.vimと同様,ctrlp.vimの本体からも多数の拡張が提供されており,それぞれの拡張は :CtrlPXXXX のようなコマンドを実行することで利用可能です.

CtrlPバッファでのキーマッピングは以下のようになっています.

キー 動作
<C-c> or <Esc> CtrlPを終了する
<C-f> / <C-b> エクステンションを切り替える
<C-a> / <C-e> 入力ウィンドウのカーソルを先頭 / 最後に
<C-j> / <C-k> 候補ウィンドウのカーソルの上下移動
<C-d> フルパス検索モードとファイル名のみの検索モードを切り替え
<C-r> 正規表現検索モードと通常の検索モードのトグル
<CR> 同じタブに開く
<C-t> 新しいタブに開く
<C-v> 垂直分割で開く
<C-x> or <C-s> or <C-CR> 水平分割で開く
<C-p> 1つ前の入力履歴を入力欄に入れる
<C-n> 1つ後の入力履歴を入力欄に入れる
<F5> キャッシュを更新する
<F7> 選択した(マークを付けた)候補を候補から除外する
<C-z> 対象ファイルにマークを付ける(選択状態にする)

また,ctrlp.vimでは,:CtrlP 以外にもコマンドが提供されています. これらは, autoload/ctrlp.vim で定義されている組み込みの拡張とも言えるでしょう.

コマンド 機能
:CtrlPMRUFiles [dir] 最近利用したファイルを表示し,選択されたファイルを開く
:CtrlPBuffer リストされているバッファを表示し,選択されたバッファに移動する
:CtrlPLastMode [args] 最後に利用したCtrlP拡張を実行する,[args]--dir が指定されたとき,最後のワーキングディレクトリも用いる
:CtrlPClearCache or :ClearCtrlPCache CtrlPのキャッシュを削除する
:CtrlPAllClearCache or :ClearAllCtrlPCaches CtrlPのキャッシュを全て削除する
:CtrlPCurWD カレントディレクトリ以下のファイルを再帰的に表示
:CtrlPCurFile 現在編集中のファイルのディレクトリ以下のファイルを再帰的に表示
:CtrlPRoot わからない....

先に述べたように,ctrlp.vimの本体からは多数の拡張が提供されています. 先に述べた組み込みの拡張とは違い,多数の拡張は autoload/ctrlp/xxx.vim で定義されており,本体から分離されております.

拡張名 対応するコマンド 機能
tag :CtrlPTag 見つかったtagsファイル(オプション tags に依存)の項目を表示し,選択された項目の位置へジャンプする
buffertag :CtrlPBugTag [buffer] カレントバッファ,または指定されたバッファからtagsファイルの項目を表示し,選択された項目の位置へジャンプする
buffertag :CtrlPBugTagAll リストされているバッファ
quickfix :CtrlPQuickfix quickfixにある項目を表示し,選択されたエラー位置に移動する
dir :CtrlPDir [dir] カレントディレクトリ,または指定されたディレクトリ以下のディレクトリを表示し,選択されたディレクトリをワーキングディレクトリにする
rtscript :CtrlPRTS runtimepath 以下にあるファイルを表示し,選択されたファイルを開く
undo :CtrlPUndo Undo履歴を表示し,選択されたところまでundoする
list :CtrlPLine リストされている全てのバッファ,または指定されたバッファの行を表示し.選択された位置にジャンプする
changes :CtrlPChange [buffer] カレントバッファ,または指定されたバッファの変更箇所を表示し,選択された位置にジャンプする
changes :CtrlPChangeAll リストされているバッファ全ての変更履歴を表示し,選択された位置にジャンプする
mixed :CtrlPMixed カレントディレクトリ以下のファイル,バッファ,最近使用したファイルを表示し,選択されたものを開く
bookmarkdir :CtrlPBookmarkDir ブックマークされているディレクトリを表示し,
bookmarkdir :CtrlPBookmarkDirAdd [dir] カレントディレクトリ,または指定されたディレクトリをブックマークする(選択後,ブックマークのタイトルの入力が求められる)

fzf

fzfはコマンドラインツールとして呼び出されるので,Vimとして操作することはできません. 用意されているマッピングは以下のように単純なものです.

キー 動作
<C-c> or <C-g> or <Esc> fzfを終了する
<C-f> / <C-b> カーソルを移動する
<CR> 候補を選択する
<S-Tab> 複数選択可能なとき(オプション -m or --multi が指定されているとき),候補にマークを付ける

本体から拡張は提供されていないので,自分で書くことになるでしょう. 公式のwikiにサンプルが多数ありますので,それらを ~/.vimrc にコピペなどすればよいかと思います.

alti.vim

alti.vimグローバル変数 g:alti_default_mappings_base'standard''ctrlplike' を代入することで,操作のプリセットを変更することができます. このグローバル変数のデフォルト値は 'standard' なので,何もしなかったり,無効な値を入力すると,standardのキーマッピングになるでしょう.

standard

キー 動作
<BS> or <C-h> 入力欄のカーソルの前1文字を削除
<Del> or <C-d> 入力欄のカーソルの後ろ1文字を削除
<C-w> 入力欄のカーソルの前1単語を削除
<C-u> 入力欄をクリアする
<C-r> レジスタ挿入モードに
<C-x><C-n> or <C-_> 1つ後の入力履歴を入力欄に入れる
<C-x><C-p> or <C-s> 1つ前の入力履歴を入力欄に入れる
<C-a> / <C-e> 入力欄の先頭 / 末尾にカーソルを移動
<C-b> or <Left> 入力欄のカーソルを左に
<C-f> or <Right> 入力欄のカーソルを右に
<C-j> or <PageDown> or <kPageDown> 候補欄で次のページを表示する
<C-j> or <PageDown> or <kPageDown> 候補欄で前のページを表示する
<C-n> or <Down> 候補欄のカーソルを1つ下に
<C-p> or <Up> 候補欄のカーソルを1つ上に
<C-g>g or <C-g><C-g> or <Home> or <kHome> 候補窓で先頭にカーソルを移動
<C-g>G or <End> or <kEnd> 候補窓で末尾にカーソルを移動
<Tab> 候補窓のカーソル下の候補を,入力欄に入力する
<C-o> 選択候補についてのアクションメニューを出す
<Esc> or <C-c> alti.vimを終了する
<CR> 入力(あるいはカーソル下の候補)に基づいて,アクションを行う
<C-y> 拡張の定義辞書のキー default_actions に指定されたリストの0番目に指定されたアクションを実行
<C-v> 拡張の定義辞書のキー default_actions に指定されたリストの1番目に指定されたアクションを実行

ctrlplike

キー 動作
<BS> or <C-]> 入力欄のカーソルの前1文字を削除
<Del> or <C-d> 入力欄のカーソルの後ろ1文字を削除
<C-w> 入力欄のカーソルの前1単語を削除
<C-u> 入力欄をクリアする
<C-r> or <C-\> レジスタ挿入モードに
<C-n> 1つ後の入力履歴を入力欄に入れる
<C-p> 1つ前の入力履歴を入力欄に入れる
<C-a> / <C-e> 入力欄の先頭 / 末尾にカーソルを移動
<C-h> or <Left> 入力欄のカーソルを左に
<C-l> or <Right> 入力欄のカーソルを右に
<C-f> or <PageDown> or <kPageDown> 候補欄で次のページを表示する
<C-b> or <PageDown> or <kPageDown> 候補欄で前のページを表示する
<C-j> or <Down> 候補欄のカーソルを1つ下に
<C-k> or <Up> 候補欄のカーソルを1つ上に
<C-g>g or <C-g><C-g> or <Home> or <kHome> 候補窓で先頭にカーソルを移動
<C-g>G or <End> or <kEnd> 候補窓で末尾にカーソルを移動
<Tab> 候補窓のカーソル下の候補を,入力欄に入力する
<C-o> 選択候補についてのアクションメニューを出す
<Esc> or <C-c> alti.vimを終了する
<CR> 入力(あるいはカーソル下の候補)に基づいて,アクションを行う
<C-y> 拡張の定義辞書のキー default_actions に指定されたリストの0番目に指定されたアクションを実行
<C-v> 拡張の定義辞書のキー default_actions に指定されたリストの1番目に指定されたアクションを実行

このプラグインも本体に拡張は付属していませんが,使い勝手はかなり良いので,拡張を自分で作って利用するとよいでしょう.

vim-milqi

vim-milqiはドキュメントが無いので,ソースコードを読んだ結果を書きます. このあたりを見る限り,ctrlp.vimから持ってきてると推測できるので,ほとんどctrlp.vimと同じ操作であると言ってよいでしょう.

vim-milqiの本体にも拡張は付属していませんが, :MilqiFromUnite というコマンドが提供されており,引数にunite source名を指定することで,unite sourceから候補を取得し,アクションを実行することができます. 他のプラグインでは提供されていない機能なので,なかなか面白いと思いました. しかし,unite sourceの候補にkindが指定されていない場合,エラーとなるので注意が必要です.

拡張の作りやすさの比較

各絞り込み検索プラグインの拡張の作りやすさを比較してみましょう. まず,拡張において実現できる特徴的なことを比較すると,以下のようになります.

プラグイン 複数選択 複数のアクションの提供 非同期の候補取得 プロンプトの変更
unite.vim ○(起動オプション)
ctrlp.vim △(候補選択時のキー) × ×
fzf × ×
alti.vim × ◎(動的に変更可)
vim-milqi × × ×

ここでいう「非同期の候補取得」とは,定期的に候補を取得処理を呼び出し,選択可能な候補数を増やす(あるいは減らすこともできるでしょう)ことです. Vimでは非同期処理をする手段がちゃんとした形で提供されていないので,非同期の候補の取得は,Shougo/vimproc.vimと, updatetimefeedkeys() , そして, CursorHoldCursorMoved という autocmd を組み合わせたポーリングによって行われるのが主流です. +clientserver の機能と RemoteReply を組み合わせても非同期処理は実現できますが,+clientserver になっているVimは多くないので,こちらが採用されることはありません.

非同期の候補取得でアニメーションを実現するという面白い使い方もあり,例えばsupermomonga/jazzradio.vimでは音量レベルのようなアニメーションを行うunite sourceが提供されています.

さて,同じ目的を達成するそれぞれの拡張を作ってみましょう. 今回は「apple, banana, cakeという3つの候補を表示し,それぞれ選択されたものを echomsg する」という非常に単純なものを作ることにします.

とにかく,コードを書かないと始まりませんね. 以下,それぞれのコードと解説です. なお,このサンプルの拡張は koturn/vim-exts にあります.

plugin/exts.vim : インタフェース

if exists('g:loaded_exts')
  finish
endif
let g:loaded_exts = 1
let s:save_cpo = &cpo
set cpo&vim

command! CtrlPExts  call ctrlp#init(ctrlp#exts#id())
command! FZFExts  call fzf#run(fzf#exts#option())
command! AltiExts  call alti#init(alti#exts#define())
command! MilqiExts  call milqi#candidate_first(milqi#exts#define())
" command! MilqiExts  call milqi#query_first(milqi#exts#define())

let &cpo = s:save_cpo
unlet s:save_cpo

インターフェースを定義しているだけなので,特に言及することはありません. uniteだけはコマンド定義しなくて良いので楽ですね. vim-milqiを用いるコマンドについて,コメントアウトしているものがありますが,これについてはvim-milqiの項目で解説します.

autoload/unite/sources/exts.vim : unite.vimの拡張

let s:save_cpo = &cpo
set cpo&vim


let s:source = {
      \ 'name': 'exts',
      \ 'description': 'descriptions',
      \ 'action_table': {},
      \ 'default_action': 'my_action'
      \}

let s:source.action_table.my_action = {
      \ 'description': 'my action'
      \}
function! s:source.action_table.my_action.func(candidate) abort
  echomsg a:candidate.word
endfunction

function! s:source.gather_candidates(args, context) abort
  return map(['apple', 'banana', 'cake'], '{
        \ "word": v:val
        \}')
endfunction


function! unite#sources#exts#define() abort
  return s:source
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

unite#sources#exts#define() でunite sourceに関する辞書を返却するようにします. 今回は word のみ指定していますが, abbrkind など,他にも重要な項目もありますので, :h unite-notation-{candidate} で調べてみるとよいでしょう. この関数はunite.vim本体から呼び出されます.

上記の例では,myaction というアクションのみを source に対して定義し,<CR> で選択したときのアクションである default_action に指定しています.

また,今回はやっていませんが,s:source.action_table.my_actionis_selectable: 1 を追加すると複数選択できるようになり,s:source.action_table.myaction.func() の引数が,選択した候補の辞書ではなく,複数の辞書を含むリストになります.

なお,作成したunite sourceに引数を渡して起動したい場合は, Unite exts:arg1:arg2 のように,コロン区切りで引数を指定する形になります. この引数は, s:source.gather_candidates の第一引数 a:args というリストで受け取ることができます. この引数を補完する関数は, s:source のキー complete に,補完関数の関数参照を指定します.

function! s:source.complete(args, context, arglead, cmdline, cursorpos) abort
  return filter(['foo', 'bar', 'piyo'], 'v:val =~? ^"' . a:arglead . '"')
endfunction

のような形になり, <Tab> によって選択できる候補のリストを返却すればよいだけです. ユーザー定義コマンドの補完関数と似たようなもので,引数が2つほど増えているだけですね.

ちなみに,アクションはunite kindとして分離可能であり,丁寧なunite拡張を書くのであれば,候補の取得はsourceで,アクションの担当はkindにした方がよいでしょう(前述の通り,vim-milqiはKindを利用します). kindを指定しなかった場合,untie.vimではkindにcommonが指定されたものとします.

autoload/ctrlp/exts.vim : ctrlp.vimの拡張

if get(g:, 'loaded_ctrlp_exts', 0)
  finish
endif
let g:loaded_ctrlp_exts = 1
let s:save_cpo = &cpo
set cpo&vim

let s:ctrlp_builtins = ctrlp#getvar('g:ctrlp_builtins')

function! s:get_sid_prefix() abort
  return matchstr(expand('<sfile>'), 'function \zs<SNR>\d\+_\zeget_sid_prefix$')
endfunction
let s:sid_prefix = s:get_sid_prefix()
delfunction s:get_sid_prefix

let g:ctrlp_ext_vars = add(get(g:, 'ctrlp_ext_vars', []), {
      \ 'init': s:sid_prefix . 'init()',
      \ 'accept': s:sid_prefix . 'accept',
      \ 'lname': 'sample extension',
      \ 'sname': 'sample',
      \ 'type': 'line',
      \ 'nolim': 1
      \})
let s:id = s:ctrlp_builtins + len(g:ctrlp_ext_vars)
unlet s:ctrlp_builtins s:sid_prefix


function! ctrlp#exts#id() abort
  return s:id
endfunction


function! s:init() abort
  let candidates = ['apple', 'banana', 'cake']
  return candidates
endfunction

function! s:accept(mode, str) abort
  call ctrlp#exit()
  echomsg a:str
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

ctrlp.vimの拡張の作り方に関しては,以下の記事に詳しく書いてあるので,そちらを見ていただいた方がよいでしょう.

ざっと説明すると, s:init() は候補の取得を担当する関数で, s:accept() がアクションを担当する関数になります. ctrlp#exts#id() は,この拡張のIDを返却する関数で,ctrlp.vimの本体の関数 ctrlp#init() に渡すことで,ctrlpが起動します. 他の候補選択型インターフェースと比較して,作成した拡張の起動に辞書をそのまま渡せる形にしてほしかったと感じます.

autoload/fzf/exts.vim : fzfの拡張

let s:save_cpo = &cpo
set cpo&vim


function! fzf#sampleoption() abort
  return {
        \ 'down': 20,
        \ 'sink': function('s:sink'),
        \ 'source': s:gather_candidates()
        \}
endfunction

function! s:gather_candidates() abort
  return ['apple', 'banana', 'cake']
endfunction

function! s:sink(candidate) abort
  echo len(readfile(a:candidate))
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

fzfはファイルを分離する必要は無いのですが,他のプラグインに習って分離してみました. 拡張の作り方については,以前書いた

を参考にするとよいでしょう.

autoload/alti/exts.vim : alti.vimの拡張

let s:save_cpo = &cpo
set cpo&vim

function! s:get_sid_prefix() abort
  return matchstr(expand('<sfile>'), 'function \zs<SNR>\d\+_\zeget_sid_prefix$')
endfunction
let s:sid_prefix = s:get_sid_prefix()
delfunction s:get_sid_prefix

let s:define = {
      \ 'name': 'exts',
      \ 'cmpl': s:sid_prefix . 'cmpl',
      \ 'prompt': s:sid_prefix . 'prompt',
      \ 'submitted': s:sid_prefix . 'submitted'
      \}
unlet s:sid_prefix

function! alti#exts#define() abort
  return s:define
endfunction


function! s:cmpl(context) abort
  return a:context.filtered(self.candidates)
endfunction

function! s:prompt(context) abort
  return 'exts> '
endfunction

function! s:submitted(context, line) abort
  if len(a:context.inputs) == 0
    echomsg a:context.selection
  else
    for input in a:context.inputs
      echomsg input
    endfor
  endif
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

alti.vimはctrlp.vimに不満があって作成されたプラグインだけあって,拡張の作成はctrlp.vimと似た雰囲気になります. しかし,ctrlp.vimよりパワフルな拡張を作ることが可能になっています. alti.vimは入力することに重きを置いたプラグインという雰囲気です.

上記コードにおいて,候補の取得とフィルタリングは s:cmpl() が,アクションは s:submitted() が担当しています. 他の候補絞り込み型プラグインと異なり,s:cmpl() で入力に対する候補の絞り込みを実装する必要があります. alti.vim本体には4つのフィルタリング関数が(引数 context の辞書関数として)用意されており,それぞれ

  • 前方一致:a:context.filtered()
  • 後方一致:a:context.backward_filtered()
  • 部分一致:a:context.partial_filtered()
  • 曖昧一致:a:context.fuzzy_filtered()

となっています. もちろん,自前でフィルタリングを実装してもよいですが,基本的には上記4つのいずれかを用いることで解決できるでしょう.

s:submitted() において,カーソル下にあった候補は, a:context.selection に格納され,入力された候補は a:context.inputs にリストとして格納されています. alti.vimでは,スペース区切りで候補の入力が認識されるようになっていて,複数の候補を選択する場合は,スペース区切りで入力することになります. <Tab> を入力することで,候補窓のカーソル下の候補が入力欄に挿入されるので,ユーザとしては,

  1. 絞り込みクエリを入力し,候補を絞り込む
  2. 候補窓でカーソル移動をして,候補を <Tab> で完全に入力欄に入力する
  3. <CR> で選択を終え,アクションを実行する

という流れになると思います. alti.vim的には,ユーザが <CR> でカーソル下の単語を選択してアクションを実行するのではなく, <CR> によって,入力された候補を送信することが想定されていると思われます. したがって, a:context.selection は基本的にアクション側で見る必要はないでしょう(ただし,受理できる入力が無かった場合は,カーソル下の候補に対してアクションを行うというのもアリかもしれません). なお,入力された候補は,候補窓からは除外されるようになっているので,重複選択の恐れはありません. 手動で重複した入力を与えたとしても,候補から除外されます.

入力欄に入力されたもののうち,元々の候補群になかったものは除外されて a:context.inputs に格納されます. つまり,ユーザがでたらめな入力をしたとしても,それらは全て除外されるということです.

なお,上記コード中の辞書 s:define の中で,値が関数名の文字列となっているものは,関数参照を取ることも可能です. 関数名を渡すより,関数参照を渡した方がコードとしてはシンプルになるのではないかと思います.

autoload/milqi/exts.vim : vim-milqiの拡張

let s:save_cpo = &cpo
set cpo&vim


let s:define = {'name': 'exts'}

function! milqi#exts#define() abort
  return s:define
endfunction

function! s:define.init(context) abort
  " let context.i = 0
  return ['apple', 'banana', 'cake']
endfunction

function! s:define.accept(context, candidate) abort
  call milqi#exit()
  echomsg a:candidate
endfunction

" function! s:define.lazy_init(context, query) abort
"   let a:context.i += 1
"   if query ==# ''
"     return {
"           \ 'reset': 0,
"           \ 'candidates': map(['lazy_apple', 'lazy_banana', 'lazy_cake'],
"           \   'v:val . " - " . a:context.i')
"           \}
"   else
"     return {
"           \ 'reset': 1,
"           \ 'candidates': ['apple_query', 'banana_query', 'cake_query']
"           \}
"   endif
" endfunction

" function! s:define.async_init(context) abort
"   let a:context.i += 1
"   return {
"         \ 'done': a:context.i < 6 ? 0 : 1,
"         \ 'candidates': map(['async_apple', 'async_banana', 'async_cake'],
"         \   'v:val . " - " . a:context.i')
"         \}
" endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

辞書関数ばかりとなっていますが,コードの雰囲気は,ctrlp.vimと似ていますね. 今回の例では活用していませんが(例が悪いのは申し訳ありません),vim-milqiの一番の特徴は, query-firstcandidate-first という2つのインタラクションモードです. 上記コード中では s:define.lazy_inits:define.async_init が,それらのインタラクションモードでのアクションに関係しています この2つのインタラクションモードは,vim-milqiのセールスポイントを語る上では欠かせない要素です. 以下,2015年3月29日のLingrの過去ログからの引用です.

query-firstは入力に応じて動的に内容が変化する系,candidate-firstは単なる非同期ctrlp系ですね candidate-firstだと,例えば時間のかかる処理で候補の取得に時間のかかるもの向けで考えていて query-firstだと,例えば過去の選択状態に応じて最初に一部の候補をキャッシュから出し,入力があれば入力に対応した候補を動的に生成して出すということができます candidate-firstはuniteやctrlpを考えていただければイメージは掴みやすいと思います query-firstは,こっちが私のやりたいメインですが,例えば初期表示でa, bを表示,hogeと入力したときにc, d, eを表示,hogeを消してfugaと入力したときにf, g, h, iを表示,ということができます

candidate-first

candidate-first は,unite.vimにもある「非同期の候補の取得ができるもの」といえるでしょう. candidate-firstモードとして利用する場合, milqi#candidate_first() に拡張の定義辞書を渡します. この辞書のキー async_init に関数参照が指定されていれば, updatetime ハックにより,定期的にその関数がコールされます. async_init に指定された関数は,donecandidates をキーに持つ辞書を返却する必要があります. キーの名前の通り, done は非同期処理が終了したかどうかを, candidates は非同期で取得した候補を表します. candidate に指定したリストは既にある候補群に追加されるので,拡張を作成する側が候補リストの要素に追加するといった処理は不要です. done1 を指定したとき,非同期処理は完了したものとして,関数を定期的なコールが終了します.

query-first

query-first は,入力に応じて,候補を動的に生成する,といったものらしいです. query-firstモードとして利用する場合, milqi#candidate_first() に拡張の定義辞書を渡します. この辞書のキー lazy_init に関数参照が指定されていれば, updatetime ハックにより,定期的にその関数がコールされます. ここまでは,candidate-firstモードと変わらないですが, lazy_init に指定した関数は第二引数に入力クエリを受け取ることができます. そして,返り値は resetcandidates をキーに持つ辞書を返却しなくてはなりません. candidates に指定したリストが候補群に追加されていくのは,candidate-firstモードの async_init と同様ですが, reset1 を指定することで,候補群を(ただの init で返却した候補リストも含めて)全て除去することができます. 入力クエリと reset を活用して,候補を動的に変更するのが,query-firstモードの使い方といえるでしょう.

単純な例の場合

今回のような単純な例(拡張辞書に lazy_initasync_init のどちらも持たない例)では,どちらのモードでも(すなわち, milqi#query_first()milqi#candidate_first のどちらに拡張の定義辞書を渡しても),動作に違いは無いでしょう.

なお,上記コード中のコメントアウト部分を解除し, plugin/exts.vimvim-milqiに関する部分を適切に変更することで,candidate-first , query-firstのそれぞれを体験することができるようにしています. candidate-firstの場合は,関数が5回コールされるまで,候補が増えるようにしています. query-first の場合,何か入力すると,候補が apple_querybanana_querycake_query の3つだけになるようにしています.

感想

この記事では5つの候補絞り込み型インターフェースを提供するプラグインについて,簡単に説明しました. どれもざっと見た限りでは「候補絞り込み型」という風にまとめられますが,それぞれが実現したいことや,目指しているものが少しずつ違っていて面白いですね.

unite.vimは初めから拡張を見越して作られた分,第三者にとってはかなり自由かつパワフルに拡張を作ることができるようになっています. また,作成したsourceの呼び出しは, :Unite のサブコマンド,すなわち第一引数にsource名を与える形になっているので,source作成者が plugin/ 下のファイルにコマンドを定義しなくてよいのも魅力です. :Unite のオプション引数である -auto-preview などは,uniteにしかできないことでしょう.

ctrlp.vimは元々がファイラであり,拡張が後付けのようなものであるため,作ることのできる拡張にやや不自由があるのは否めません. 例えば,複数選択がファイルを開くという操作に限定されるので,第三者にとっては利用しにくいものでしょう. しかし,コンパクトな実装である分,unite.vimより初回の起動時間が短い点は評価できるでしょう.

fzfは外部コマンドとして利用するので,一時的にVimから出る形になります. その点が他の絞り込み検索型のプラグインと比べると異質なところではあります. しかし,tmux上であれば,画面分割を利用し,候補を表示することができたり,neovimであればneovimのターミナルエミュレーター上で起動するようにしていたり,とても面白い機能が実装されています.

alti.vimはctrlp.vimにインスパイアされて開発されたプラグインだけあって,細かい部分での配慮が行きとどいていますね. 本体から前方一致,後方一致,部分一致,ファジーマッチという4つの絞り込み関数が提供されているのも,なかなかよいと感じました. ctrlp.vimはファジーマッチしかできないようになっており,僕としては多数の候補のフィルタリングに苦労することがありました.

vim-milqiは,unite souceから候補を取得できるようにしているのは画期的な機能だと思いました. また,非同期処理を主眼に置いており,他の絞り込み検索プラグインには無い特徴を感じ取ることができました.

おわりに

絞り込み検索型プラグインの拡張を作ることは難しくありません. この記事で紹介したような,たった十数行程度のコード(ほとんどが定型句のようなもので,本質的な部分は数行程度)を書くだけで,自作のプラグインから絞り込み検索型プラグインのUIを利用することができます. もし,これを読んでいるあなたがプラグイン作成の初心者であり,unite sourceやctrlp.vimの拡張を書いたことが無いのであれば,この機会に挑戦してみるのはいかがでしょうか?

なお,unite.vimは日本での知名度がとても高く,ググれば多数の情報が出てくると思います. unite.vimは高機能なため,この記事では紹介しきれていないので,unite.vimの本当の力を知るためにもググることをオススメします.

参考

Cygwinでclipboard機能を有効にしたVimをビルドする

はじめに

昔のCygwinVimではクリップボードが利用できず,Cygwinで用意されているクリップボードデバイス /dev/clipboard に対し,読み書きを行うことや, getclipputclip コマンドを利用することで,クリップボードとのやりとりを行っていた.

kanaさんのkana/vim-fakeclipというプラグインは,Cygwinや他の環境で +clipboard でないVimであっても,外部コマンドなどを用いることによって,クリップボードを何とか利用しようというものであり,かつてのCygwinVimユーザーは,よくこのプラグインを利用していたことだろう.

しかし,Vim 7.3.836以降,k_takataさんにより,CygwinVimでもクリップボード機能が利用できるようになった. 最近では,Cygwinにデフォルトで付属しているVimであっても,クリップボード機能を有効にしてビルドされており,利用することができる. だが,僕がCygwinVimを自前ビルドしたとき, +clipboard となっているのに,クリップボードが利用できなかった. この記事では,そのときにハマったことと,どう解決したかについて述べる.

長い前書きになったが,内容としては「こうやっただけ」というしょうもないものである. 結論を先に言うと, configure--enable-gui=no--without-x を指定しただけである.

CygwinVimをビルドする

最近では,VimソースコードGitHubで管理されているようになっているので,GitHubからソースを落としてくる.

$ git clone https://github.com/vim/vim.git

リポジトリのルートディレクトリに configure が用意されているので,多くのソフトウェアと同じく,

$ ./configure
$ make
$ make install

の順で実行するとよい.

configure については,適当に情報を収集し,以下のようにオプションを追加し,ビルドした.

$ ./configure \
  --prefix=/usr/local/ \
  --enable-fail-if-missing \
  --enable-gui=yes \
  --enable-multibyte=yes \
  --enable-perlinterp=yes \
  --enable-pythoninterp=yes \
  --enable-rubyinterp=yes \
  --enable-tclinterp=yes \
  --enable-luainterp=yes \
  --enable-cscope=yes \
  --enable-gpm \
  --enable-cscope \
  --enable-fontset \
  --with-features=huge \
  --with-x \
  --with-compiledby=koturn && \
  make -j5 && \
  make install

しかし,この設定では +clipboard となったが,クリップボードが利用できなかった. そのときに,ざっと調べたときに見た情報から, --with-x--enable-gui=yes が余計なのではないかと思った. そこで,--with-x の代わりに --without-x を, --enable-gui=yes の代わりに --enable-gui=no を指定して,再度 configure を行いビルドした(1度 configure を行った場合, rm src/auto/config.cache をしておく必要がある).

$ ./configure \
  --prefix=/usr/local/ \
  --enable-fail-if-missing \
  --enable-gui=no \
  --enable-multibyte=yes \
  --enable-perlinterp=yes \
  --enable-pythoninterp=yes \
  --enable-rubyinterp=yes \
  --enable-tclinterp=yes \
  --enable-luainterp=yes \
  --enable-cscope=yes \
  --enable-gpm \
  --enable-cscope \
  --enable-fontset \
  --with-features=huge \
  --without-x \
  --with-compiledby=koturn && \
  make -j5 && \
  make install

これで自前ビルドのCygwinVimであっても,クリップボード機能が利用可能となった. 明示的に --without-x--enable-gui=no を指定しなかった場合でも,クリップボードが有効にならなかったので,ちゃんと指定しなくてはならない.

余談

CFLAGSLDFLAGS に,自分なりの最適化オプションを追加してもビルドできるか?ということが気になったので,

$ ./configure \
  --prefix=/usr/local/ \
  --enable-fail-if-missing \
  --enable-gui=no \
  --enable-multibyte=yes \
  --enable-perlinterp=yes \
  --enable-pythoninterp=yes \
  --enable-rubyinterp=yes \
  --enable-tclinterp=yes \
  --enable-luainterp=yes \
  --enable-cscope=yes \
  --enable-gpm \
  --enable-cscope \
  --enable-fontset \
  --with-features=huge \
  --without-x \
  --with-compiledby=koturn \
  CFLAGS='-Ofast -m64 -march=native -mtune=native -flto -funroll-loops -DNDEBUG' \
  LDFLAGS='-flto' && \
  make -j5 && \
  make install

としてビルドしてみた. しかし,-flto が何かの悪さをしているらしく,出来上がったバイナリを実行すると,Vimが立ち上がった瞬間にエラーが出た. -flto を外すと,エラー無く,無事に実行できた.

まとめ

CygwinVimconfigure--enable-gui=no --without-x を指定しないと,クリップボードが有効にならない.

参考

Vimからfzfを利用する

はじめに

fzfとは,percolpecoと同様,絞り込みの検索を行うことのできるコマンドラインツールである. 日本では,percolやpecoが有名で,fzfはあまり有名ではないが,海外では有名であるらしい. fzfはVimから利用できるように,公式のリポジトリAPIを提供するVimプラグインが付属している. この記事では,fzfをVimから使う方法について述べる.

fzfの特徴

fzfはGoで実装されている. Goで実装されているから,マルチプラットフォームなのかと思ってしまうが,実は Windowsでは利用することができないCygwin上では利用可能).

fzfを利用したプラグインの作り方

正直,fzfのREADME.mdwikiのサンプルを見るのが早いのだが,それではこの記事の意味が無いので,ちゃんと書く. (日本語で書いておくと,日本人が読みやすいという利点もあるだろうし)

まず,fzfをVimから利用するためには,fzfが提供しているプラグインruntimepath を通す必要がある. これはfzfをクローンした場所にもよるが,

$ git clone https://github.com/junegunn/fzf.git ~/.fzf

としたならば,

set rtp+=~/.fzf

とするだけでよい. すなわち,fzfのルートディレクトリを runtimepath に追加するだけでよい. 上記の場合,fzfのVimプラグイン本体は ~/.fzf/plugin/fzf.vim にあるので,Vimからfzfの関数を利用できるようになるわけだ.

fzfをVimから利用するのは簡単で, fzf#run() に渡す適切な辞書を定義するだけだ. 後は,その辞書を第一引数とし, fzf#run() をコールするだけで,Vimからfzfを利用できる. 例えば,wikiには,

nnoremap <silent> <Leader>C :call fzf#run({
    \ 'source': map(split(globpath(&rtp, "colors/*.vim"), "\n"),
    \         "substitute(fnamemodify(v:val, ':t'), '\\..\\{-}$', '', '')"),
    \ 'sink': 'colo',
    \ 'options': '+m',
    \ 'left': 30
    \})<CR>

というfzfを用いるキーマッピングの例が紹介されている. これは,利用可能なcolorschemeを候補として表示し,選択したcolorschemeに変更するというものだ.

この fzf#run() に渡す辞書のキーと値には以下のようなものが有効である. 同じキーであっても,値の型によって挙動が異なる点に注意しよう.

  • source
    • 値の型が 文字列 のとき,その文字列を外部コマンドとして実行し,その結果をfzfに渡す
    • 値の型が リスト のとき,このリストをfzfに渡す
  • sink
    • 値の型が 文字列 のとき,選択した候補に対してその文字列をVimコマンドとして実行する
      • 候補は文字列の末尾に結合される(例:値が 'edit' ならば,実行されるコマンドは 'edit [選択したもの]'
    • 値の型が 関数参照 のとき,選択した候補を指定した関数(への参照)の第一引数として渡し,関数をコール
      • 指定する関数参照の引数は1つでなければならない
    • 複数の候補が選択されたとき,それぞれの候補に対して, sink で指定したアクションを1回1回実行する
  • sink*
    • 値の型は 関数参照 のみで,選択した候補を指定した関数(への参照)の第一引数として渡し,関数をコール
    • 複数選択された候補を リストとして扱う
      • すなわち,選択された候補はリスト化されて,関数の第一引数に渡される
  • options
    • 値の型は 文字列 のみで,fzfの起動オプションを指定する
  • dir
    • 値の型は 文字列 のみで,fzf起動時の作業ディレクトリを指定する
  • updownleftright
    • 値の型は 数値 または 文字列
      • 数値型では行数,または列数を指定できる( 20 など)
      • 文字列型では行,または列の占める割合を指定できる( '50%' など)
      • 文字列型の場合,末尾に % を付けないと,行数,または列数の指定になる( '50' では50行,または50列で50%でない )
    • tmux上でVimを起動している場合のみ
  • window
    • neovimのみ
    • 値の型は 文字列 のみで,fzfのウィンドウを開くコマンドを指定する(例: vertical aboveleft 30new
  • launcher
    • gvimのみ
    • 値の型が 文字列 のとき,fzfを起動する外部のターミナルエミュレータ名を指定する
    • 値の型が 関数参照 のとき,その関数の返り値をfzfを起動する外部のターミナルエミュレータとして用いる

キーとして必ず持つべきものは,候補に対するアクションを担当する sink または sink* である. 候補の取得を担当する source は,後述するが,ほぼ必須の項目である. なお, sinksink* の2つがある場合, sinksink* の順番に処理がなされる.

ちなみに, source を指定しない場合,デフォルトのfzfの動作(再帰的なファイル検索)になるので,単純にファイルを検索したいのであれば, source を敢えて指定しないというのも手だ. 単純なファイラとして用いるのであれば,以下のようにするとよいだろう.

call fzf#run({'sink': 'edit'})

sink も指定しなかった場合,候補を選択しても何のアクションも行われない.

" カレントディレクトリ以下のファイルが再帰的に表示されるが,
" 候補を選択しても,何のアクションも行われない
call fzf#run({})

プラグインに組み込む

僕の場合,fzfをプラグインに導入する場合,実装を別ファイルに分割している. unite.vimctrlp.vimに習い,分割したファイルは autoload/fzf/ に配置することにする.

以下は,カレントディレクトリのファイルを表示し,選択されたファイルの行数を echo するという単純なサンプルである.

  • plugin/sample.vim
if exists('g:loaded_sample')
  finish
endif
let g:loaded_sample = 1
let s:save_cpo = &cpo
set cpo&vim

command! FZFSample  call fzf#run(fzf#sample#option())

let &cpo = s:save_cpo
unlet s:save_cpo
  • autoload/fzf/sample.vim
let s:save_cpo = &cpo
set cpo&vim

function! fzf#sampleoption() abort
  return {
        \ 'down': 20,
        \ 'sink': function('s:sink'),
        \ 'source': s:gather_candidates()
        \}
endfunction

function! s:gather_candidates() abort
  return filter(split(globpath('.', '*', 1), "\n"), 'filereadable(v:val)')
endfunction

function! s:sink(candidate) abort
  echo len(readfile(a:candidate))
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

autoload/fzf/sample.vimfzf#sample#option() で返却する辞書の一部の項目は,いちいち新しく定義し直す必要は無いので,以下のように記述してもよいだろう.

let s:save_cpo = &cpo
set cpo&vim

let s:option = {
      \ 'down': 20
      \}
function! s:option.sink(candidate) abort
  echo len(readfile(a:candidate))
endfunction


function! fzf#sample#option() abort
  let s:option.source = s:gather_candidates()
  return s:option
endfunction


function! s:gather_candidates() abort
  return filter(split(globpath('.', '*', 1), "\n"), 'filereadable(v:val)')
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

この例では,候補の取得をVim Script側で行っているが,カレントディレクトリのファイルを検索するのなら,外部コマンドを用いてもよいだろう. fzfがコマンドラインツールであることを考えれば,そちらの方が好ましいかもしれない.

let s:save_cpo = &cpo
set cpo&vim

let s:option = {
      \ 'down': 20,
      \ 'source': 'find -maxdepth 1 -find f'
      \}
function! s:option.sink(candidate) abort
  echo len(readfile(a:candidate))
endfunction


function! fzf#sample#option() abort
  return s:option
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

複数選択したい場合

先述の例では触れていないが,fzfの拡張は複数選択できるようにすることが可能である. 複数選択はfzf自体の機能にあるので, options にfzfのオプション -m ,または --multi を指定するだけでよい. 先述の例であれば,以下のように s:option を変更するだけだ.

let s:option = {
      \ 'down': 20,
      \ 'options': '-m'
      \}
function! s:option.sink(candidate) abort
  echo len(readfile(a:candidate))
endfunction

このとき, sink に指定したアクションが,選択した各候補に対して実行される. もし,複数選択したものを,Vim Script側ではリストとして扱いたいならば, sink* を利用するとよい.

function! s:sink(candidates) abort
  for candidate in a:candidates
    echo len(readfile(candidate))
  endfor
endfunction

let s:option = {
      \ 'down': 20,
      \ 'options': '-m',
      \ 'sink*': function('s:sink')
      \}

感想

fzfはあくまで外部ツールであって,Vimからfzfに操作を移さなければならない点が残念に思えた. 僕としては,外部ツールはあくまでシームレスに用いることができるようにしてほしいと思っている. ただ,tmux上のVimで用いた場合に,候補選択用の新しいペインを作成する点や,neovimに対応している点など,なかなか面白い試みをしていると感じた.

また,複数選択に対応している点も魅力的だと感じた. 候補選択型インターフェースで,ユーザが作成する拡張で,複数選択ができるものを作ることができるのは,実質unite.vimとfzfぐらいのものではないかと思われる(ctrlp.vimは特殊な場合に限り,複数選択可能).

参考

ctrlp.vimのエクステンションの作り方

はじめに

この記事はctrlpvim/ctrlp.vimのエクステンションの作り方についての記事であり,ctrlpの公式リポジトリのextensionsブランチに書かれているエクステンションの作り方,および以下の3つの記事の内容に個人的な知見を追加したものである.

CtrlPとは

ctrlpvim/ctrlp.vimは候補絞り込み検索型のファイラである. Shougo/unite.vimと双璧を成すプラグインとして,とても有名である.

オリジナルはkien氏のものであるが,kien氏と連絡が取れないことから,それをforkしたctrlpvim/ctrlp.vimが誕生した.

ctrlp.vimは標準機能であるファイラだけではなく,候補絞り込み検索のインタフェースを利用して,ctagsのタグやundo履歴を候補として表示し,選択された場合にそれに応じたアクションが実行される拡張が本体に付属している.

このctrlp.vimの拡張は,第三者が作成することもできる. unite.vimの様々な拡張(unite source)が多数の人により開発され公開されているように,ctrlp.vimの拡張(以後,エクステンションと呼ぶ)も多数の人により開発され公開されている.

ctrlp.vimのエクステンションは,unite.vimのsourceほど自由に作ることはできないが,シンプルかつ簡単に作ることが可能な点が魅力であると思っている.

2015 / 12 / 01 追記

kien氏と連絡が取れ,2015年11月30日にkien氏のリポジトリではメンテナンスされていないということがオリジナルの方のREADME.mdに明記された

エクステンションの作り方

g:ctrlp_ext_vars というリストを保持するグローバル変数があり,このグローバル変数に作成するエクステンションに関する辞書を追加するだけで,エクステンションの作成は終わりである. そして,関数 ctrlp#init() に適切なインデックスを渡すことで,作成したエクステンションを利用することができる.

ただし,作成するエクステンションは,1つのファイルにし, autoload/ctrlp/sample.vim に置く. リスト g:ctrlp_extensions にエクステンションを登録してある場合,ctrlp.vim本体の読み込み時に,ctrlp.vim本体が runtime autoload/ctrlp/sample.vim として読み込みを行うので,エクステンションのファイルは autoload/ctrlp/ 以下に置かなければならない.

何はともあれ,実例を見てもらうとしよう. 以下は,カレントディレクトリのファイルを表示し,選択されたファイルの行数を echo するというシンプル(かつ何の役にも立たない)なエクステンションのサンプルである.

  • plugin/sample.vim
if exists('g:loaded_sample')
  finish
endif
let g:loaded_sample = 1
let s:save_cpo = &cpo
set cpo&vim

command! CtrlPSample  call ctrlp#init(ctrlp#sample#id())

let &cpo = s:save_cpo
unlet s:save_cpo
  • autoload/ctrlp/sample.vim
if exists('g:loaded_ctrlp_sample') && g:loaded_ctrlp_sample
  finish
endif
let g:loaded_ctrlp_sample = 1
let s:save_cpo = &cpo
set cpo&vim

let s:sample_var = {
      \ 'init': 'ctrlp#sample#init()',
      \ 'accept': 'ctrlp#sample#accept',
      \ 'lname': 'sample',
      \ 'sname': 'sample',
      \ 'type': 'path',
      \ 'nolim': 1
      \}
if exists('g:ctrlp_ext_vars') && !empty(g:ctrlp_ext_vars)
  let g:ctrlp_ext_vars = add(g:ctrlp_ext_vars, s:sample_var)
else
  let g:ctrlp_ext_vars = [s:sample_var]
endif
let s:id = g:ctrlp_builtins + len(g:ctrlp_ext_vars)


function! ctrlp#sample#id() abort
  return s:id
endfunction


function! ctrlp#sample#init() abort
  return filter(split(globpath('.', '*', 1), "\n"), 'filereadable(v:val)')
endfunction

function! ctrlp#sample#accept(mode, str) abort
  call ctrlp#exit()
  echo len(readfile(a:str))
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

先に述べたように,ctrlp.vim本体が runtime autoload/ctrlp/sample.vim として,作成したエクステンションを読みにいくので,autoloadファイルであっても,ファイルの先頭に二重読み込み防止の記述をしておく必要がある.

このサンプルは,koturn/ctrlp-sampleにあるので,インストールして動作を確認したり,改造したりして,ctrlp.vimのエクステンションの作り方の勉強に役立てていただきたい. なお,masterブランチのものは,後述する僕の好みに合わせてコードを洗練してあるので,上記のコード例そのままのものが欲しいなら,not-refiedブランチのものを参照していただきたい.

g:ctrlp_ext_vars に追加する辞書のそれぞれのキー

エクステンションの作成において,要となるのが, g:ctrlp_ext_vars に追加する辞書である. ここでは,その辞書が持つべきキーについて述べる.

必須

  • init : 文字列(evalすると関数呼び出しになる文字列)
    • 評価されるべき関数呼び出しの文字列( 'ctrlp#sample#init()' など)
    • 候補の取得に用い,関数から候補のリストを返却する
    • unite.vimでいうところの gather_candidates
    • CtrlPのバッファに移動した後に呼び出される
  • accept : 文字列(候補が選択されたときに呼び出される関数名)
    • 選択した候補に対するアクションを定義した関数の名前を渡す
    • initenterexit とは異なり, 関数呼び出しの括弧は不要
    • ここで指定した関数のAPIctrlp#sample#accept(mode, str) といった形にすること(仮引数名は何でもよいが,本体付属のエクステンションに従った)
      • mode : 候補の選択に入力したキーに応じた文字列が入っている.対応は以下の通り
        • <CR> : 'mode' は 'e'
        • <C-v> : 'mode' は 'v'
        • <C-t> : 'mode' は 't'
        • <C-x> : 'mode' は 'h'
      • str : 選択した候補
    • 通常,関数の先頭で ctrlp#exit() をコールするとよい
    • mode に応じて処理を変更しているエクステンションは少ない
  • lname : 文字列(省略無しのエクステンション名 - long name)
    • ステータスラインに表示される省略無しのエクステンション名
  • sname : 文字列(省略したエクステンション名 - short name)
    • ステータスラインに表示される省略されたエクステンション名
  • type : 文字列で 'line''path''tabs''tabe' のいずれか
    • 'line'
      • それぞれの候補に対し,末尾まで入力文字列とのマッチを行う
    • 'path'
      • それぞれのファイルパスのような形式の候補に対し,末尾まで入力文字列とのマッチを行う
      • それぞれの候補について,入力にマッチした部分がハイライト される
    • 'tabs' (tab start)
      • 最初のTAB文字までマッチする
      • 候補の文字列の最初のTAB以降を,ユーザに情報として提示したい場合に用いる(候補に関するヒントの提示)
    • 'tabe' (tab end)
      • 最後のタブ文字までマッチする
      • 用途は 'tabs' と同じで,どのTABまでをマッチ対象にするかが異なる

任意

  • enter : 文字列(evalすると関数呼び出しになる文字列)
    • CtrlPが起動する前に呼び出される処理
    • 元のバッファの filetype の取得することなどに用いる
  • exit : 文字列(evalすると関数呼び出しになる文字列)
    • CtrlPが終了する前に呼び出される処理
  • opts : 文字列(evalすると関数呼び出しになる文字列)
    • オプションの設定などに用いるらしい
    • enter で指定した関数より前に呼び出される
  • sort : 数値( 01
    • 候補をソートするか否か
    • 0 なら候補をソートしない(デフォルト), 1 なら候補をソートする
  • specinput : 数値( 01 ) - special input
    • ..@cd といった特殊な入力を可能にする
      • :h ctrlp-input-formats を参照
    • 0 なら特殊入力を不可に(デフォルト), 1 なら特殊入力を可能にする
    • ..../ と等価で,1つドットを増やす度にディレクトリ階層を1つ上にした入力として解釈され,リターンキーを押すと,ワーキングディレクトリを変更し,再度 init に指定した関数が呼び出され,候補が更新される
      • ... なら ../../.... なら ../../../ に移動する
    • @cd はワーキングディレクトリを変更する
      • 候補選択時に @cd ./plugin/ と入力して,リターンキーを押すと,ワーキングディレクトリを ./plugin/ に変更し,再度 init に指定した関数が呼び出され,候補が更新される
    • CtrlP起動中にワーキングディレクトリを変更したい場合に用いる
    • 候補の絞り込みに用いるのではない
    • ワーキングディレクトリが候補の取得に関わっているものに有効

ドキュメント化されていないもの(任意)

  • nolim : 数値( 01 ) - no limit
    • 0 なら制限あり(デフォルト), 1 なら無制限(スクロール可能)に
    • 日本語などの検索しづらい候補があるときに用いると便利かもしれない
    • mattnさんのctrlpのエクステンションに多く用いられているのを見掛ける
  • opmul : 数値( 01 ) - open multiple
    • 0 なら複数選択不可(デフォルト), 1 なら複数選択可能に
    • 掛け算記号のことではない
    • 複数選択は候補上で <C-z> と入力することで候補にマークを付け, <C-o> を入力することで行うことができる
    • ただし, 複数選択のアクションは "基本的に" 定義できず,ファイルを開くという動作のみ である
      • 存在するファイルのパスに限定されるが,autocmdBufReadCmd を用いることで強引にアクションを定義することは可能(危険)
    • 候補はファイルパスとして解釈され,存在しないファイルパスである候補は選択しても除外される
    • 基本的に定義しない.本体付属のプラグインに用いられている
  • wipe : 文字列(関数名)
    • <F7> で候補を除去するときのアクションを指定する
      • opmul1 のとき,<C-z> でマークを付け,<F7> でマークを付けた候補を指定した関数に渡す
      • マークが1つも付いていない場合,<F7> で全ての候補を除去するかどうかのメッセージが表示される
        • o を入力すると,指定した関数に空リストを渡してコールし,その後ctrlpバッファに戻る
        • c を入力すると,候補を除去せず,ctrlpバッファに戻る
    • initenterexit とは異なり, 関数呼び出しの括弧は不要
    • ここで指定した関数のAPIctrlp#sample#wipe(entries) といった形にすること(仮引数名は何でもよいが,本体付属のエクステンションに従った)
    • 新たな候補リスト (引数の除去対象が除去されたリストが想定されている)を関数から返却する
    • 基本的に定義しない.本体付属のプラグインに用いられている

opmulwipe を用い,指定した候補を除去したいのであれば,以下のようにするのがよいだろう.

function! ctrlp#sample#init() abort
  " 候補を他の関数で用いるために,スクリプトローカル変数で保持しておく
  " この s:candidates は ctrlp#sample#exit() でunletするとよい
  let s:candidates = filter(split(globpath('.', '*', 1), "\n"), 'filereadable(v:val)')
  return s:candidates
endfunction

function! ctrlp#sample#wipe(entries) abort
  return empty(a:entries) ? [] : filter(s:candidates, 'index(a:entries, v:val) == -1')
endfunction

上記の ctrlp#sample#wipe() が分かりにくいのであれば,以下のようにforを用いて書くとよい.

function! ctrlp#sample#wipe(entries) abort
  if empty(a:entries)
    return []
  endif
  for entry in a:entries
    let idx = index(s:candidates, entry)
    if idx != -1
      call remove(s:candidates, idx)
    endif
  endfor
  return s:candidates
endfunction

前者の filter() を用いたものと比べると, a:entriess:candidates の捉え方(ループの主体)が異なっているが,候補に重複が無ければ同一の結果が得られるはずだ. (前者は除去候補にマッチしたものを候補から全て取り除き,後者は候補のうち,最初にマッチしたもののみを取り除く)

Vim scriptのパースは時間がかかる(ループ毎にもパースを行っているはず)ため, s:candidates の数が多くないのであれば,前者の方が高速に動作すると思われる.

CtrlP起動時にワーキングディレクトリを変更する

ctrlp#init() に作成したエクステンションのIDを渡すことで,CtrlPを起動するようになっていることは最初に述べた. 実は, ctrlp#init() は,第二引数に辞書を取ることもでき,その辞書のキー dir の値に,ワーキングディレクトリを指定することで,CtrlP起動時のディレクトリを変更することができる. これは相対パスからファイルを簡単に辿りたい場合などに使用できる.

command! -nargs=1 -complete=dir CtrlPSample  call ctrlp#init(ctrlp#sample#id(), {'dir': <q-args>})

候補のシンタックスハイライト

本体付属のエクステンションやこの記事を参考にすると,CtrlPの候補にハイライトを適用する(すなわち,CtrlPバッファ内でハイライトを行う)には, ctrlp#sample#init() 内で,

function! ctrlp#sample#init() abort
  call ctrlp#hicheck('CtrlPSampleTabExtra', 'Comment')
  syntax match CtrlPSampleTabExtra '\zs\t.*$'
  return filter(split(globpath('.', '*', 1), "\n"), 'filereadable(v:val)')
endfunction

などのようにするとよい. 本体付属のエクステンションでは,シンタックスハイライト用の設定を行う部分を関数として切り出し,

function! ctrlp#sample#init() abort
  call s:syntax()
  return filter(split(globpath('.', '*', 1), "\n"), 'filereadable(v:val)')
endfunction

function! s:syntax() abort
  if ctrlp#nosy()
    return
  endif
  call ctrlp#hicheck('CtrlPSampleTabExtra', 'Comment')
  syntax match CtrlPSampleTabExtra '\zs\t.*$'
endfunction

としている( ctrlp#nosy() については後述する).

ctrlp#hicheck() はハイライトのリンクをチェックする関数であり,リンクが存在しないならば,リンクを行う. 実装は非常に単純で,

" ctrlpvim/ctrlp.vim の autoload/ctrlp.vim より引用
fu! ctrlp#hicheck(grp, defgrp)
    if !hlexists(a:grp)
        exe 'hi link' a:grp a:defgrp
    en
endf

となっている.

また, ctrlp#nosy() (no syntax) はシンタックスハイライトが利用できる環境であるかどうかを判別する関数である. シンタックス機能が利用できないVimや, :syntax enable をしていないVimならば 1 を返却し,シンタックスハイライトが可能ならば, 0 を返却する.

" ctrlpvim/ctrlp.vim の autoload/ctrlp.vim より引用
fu! ctrlp#nosy()
    retu !( has('syntax') && exists('g:syntax_on') )
endf

グローバル変数 g:syntax_on は, :syntax enable とコマンドを実行する(大抵,.vimrcに書かれているだろう)ことで定義される変数で, :syntax off とコマンドを実行すると unlet される. 大抵のユーザはシンタックス機能をオンにしているので,わざわざチェックする必要も無いが,本体付属のプラグインに沿った形に記述しておくと,雰囲気は良いだろう.

シンタックスハイライトの用途としては, typetabs を指定した場合,最初のタブ文字以降を目立たない色にするといったものが考えられる. 実は,先述のハイライトの例は,本体付属の typetabs であるエクステンションにも書かれている「タブ文字以降のハイライトをコメントと同じものにして,目立たなくする」ものである.

改良案

ここまではctrlp.vimのエクステンションの作り方について述べたが,ここからは「エクステンションのコードはこう書くのが好きだ」という個人の意見を書く.

ctrlp.vim本体のグローバル変数の参照

ctrlp.vimの拡張はプラグインとしてオプショナルであり,NeoBundle等で依存関係を明示したくない場合があるかもしれない(そんなユーザがいるのかは疑問であるが). このことについて考えているときに,以下の記事を見て,なるほど,と感じた.

ctrlp.vimのExtensionを書くときに:NeoBundleLazy autoloadを考慮してg:ctrlp_builtinsをそのまま使わない - cafegale

ctrlp.vimの拡張はあくまでオプショナルとしてプラグインに付属させており,NeoBundleに依存関係が記述されておらず,ctrlp.vimが遅延読み込みされるように設定してある場合,先述の記事にあるように,作成したプラグインを読み込みしている段階で g:ctrlp_builtins が定義されていないという問題が起こることもあるだろう. 従って,ctrlpが提供しているautoload関数 ctrlp#getvar() を用いて,ctrlp.vim本体の読み込みを発生させ,ctrlpのグローバル変数が読み込まれるようにした方が望ましいはずだ. そのことを踏まえ,作成するエクステンションのファイルの先頭に,

let s:ctrlp_builtins = ctrlp#getvar('g:ctrlp_builtins')

と記述し, g:ctrlp_builtins の代わりに, s:ctrlp_builtins を用いるようにするとよさそうだ.

ただ,プラグインマネージャである neobundle.vim のためだけの記述をするというのは,本質的ではなく,少し嫌な気持ちになる. この g:ctrlp_builtins の取得方法に関しては,個人の好みが大きいだろう. 正直,ユーザがNeoBundleの依存関係をキッチリ書けば問題の無い話でもある.

ちなみに, ctrlp#getvar() は以下のような実装になっている.

" ctrlpvim/ctrlp.vim の autoload/ctrlp.vim より引用
fu! ctrlp#getvar(var)
    retu {a:var}
endf

a:var は文字列と想定されるので,{a:var} は, eval(a:var) と読み替えてよく, a:val を評価した結果に置き換えられる. すなわち, ctrlp#getvar() は, autoload/ctrlp.vim 内で引数を評価した結果を返す関数であり,autoload/ctrlp.vimスクリプトローカル変数を取得することもできる.

g:ctrlp_ext_vars へのエクステンションの辞書の追加

Vimプラグインの拡張機能プラグインを作ってVimをさらに使いやすくしよう - 29th Sta. という記事の例では,

let s:sample_var = {
      \ 'init': 'ctrlp#sample#init()',
      \ 'accept': 'ctrlp#sample#accept',
      \ 'lname': 'sample extension',
      \ 'sname': 'sample',
      \ 'type': 'path',
      \ 'nolim': 1
      \}
if exists('g:ctrlp_ext_vars') && !empty(g:ctrlp_ext_vars)
  let g:ctrlp_ext_vars = add(g:ctrlp_ext_vars, s:sample_var)
else
  let g:ctrlp_ext_vars = [s:sample_var]
endif

としているが, add() は第一引数のリストに対して破壊的変更を加えるので,返却値を再び第一引数のリストに代入する必要はない(どちらが読みやすいかは置いておくとして). また,スクリプトローカル変数 s:sample_var を保持する必要もない. これらのことから,上記コードは以下のように記述することができる.

let s:sample_var = {
      \ 'init': 'ctrlp#sample#init()',
      \ 'accept': 'ctrlp#sample#accept',
      \ 'lname': 'sample extension',
      \ 'sname': 'sample',
      \ 'type': 'path',
      \ 'nolim': 1
      \}
if exists('g:ctrlp_ext_vars') && !empty(g:ctrlp_ext_vars)
  call add(g:ctrlp_ext_vars, s:sample_var)
else
  let g:ctrlp_ext_vars = [s:sample_var]
endif
unlet s:sample_var

個人的にはif文を用いずにシンプルに書きたいので,以下のように記述することにしている.

let g:ctrlp_ext_vars = add(get(g:, 'ctrlp_ext_vars', []), {
      \ 'init': 'ctrlp#sample#init()',
      \ 'accept': 'ctrlp#sample#accept',
      \ 'lname': 'sample extension',
      \ 'sname': 'sample',
      \ 'type': 'path',
      \ 'nolim': 1
      \})

add() の第一引数が即値 [] となることもあるので,この場合は add() の返却値を利用しなければならない.

可能な限り,グローバル関数を使用しないようにする

エクステンション側にグローバル関数を定義し,ctrlp.vimに渡す辞書にそのグローバル関数名を渡しているが,必ずしもそうする必要はない. スクリプトローカル関数であっても,スクリプト番号付きであれば,グローバルに参照することが可能だからだ.

function! s:hoge() abort
  echomsg 'Hello, World'
endfunction
" :call <SNR>18_hoge() などという形で,グローバルに呼び出すことが可能

このことを踏まえ, g:ctrlp_ext_vars に辞書を追加する記述を,以下のような記述に変更した.

function! s:get_sid_prefix() abort
  return matchstr(expand('<sfile>'), '\zs<SNR>\d\+_\zeget_sid_prefix$')
endfunction
let s:sid_prefix = s:get_sid_prefix()
delfunction s:get_sid_prefix

let g:ctrlp_ext_vars = add(get(g:, 'ctrlp_ext_vars', []), {
      \ 'init': s:sid_prefix . 'init()',
      \ 'accept': s:sid_prefix . 'accept',
      \ 'lname': 'sample extension',
      \ 'sname': 'sample',
      \ 'type': 'path',
      \ 'nolim': 1
      \})
let s:id = s:ctrlp_builtins + len(g:ctrlp_ext_vars)
unlet s:sid_prefix

僕は,不特定多数の相手(プラグインであったり,.vimrcであったり)にAPIを提供する場面では,グローバル関数を用い,今回のように作成したプラグインから 能動的に 他のプラグインに自身の関数を渡す場合では,スクリプトローカル関数を用いるとよいだろうと考えている. ただ,ここまでしてグローバル関数を嫌う必要があるのかと問われれば,どうなんだろうと思ってしまう. また,スクリプト番号の取得は,コードとしてややごちゃごちゃしており(5行は必要になる),グローバル関数を用いる方がスマートという気もするが,この場では気にしないことにする.

ここまでのことを踏まえ,最初に紹介したカレントディレクトリのファイルを表示し,選択されたファイルの行数を表示するエクステンションを以下のように書き直した.

if exists('g:loaded_ctrlp_sample') && g:loaded_ctrlp_sample
  finish
endif
let g:loaded_ctrlp_sample = 1
let s:save_cpo = &cpo
set cpo&vim

let s:ctrlp_builtins = ctrlp#getvar('g:ctrlp_builtins')

function! s:get_sid_prefix() abort
  return matchstr(expand('<sfile>'), '\zs<SNR>\d\+_\zeget_sid_prefix$')
endfunction
let s:sid_prefix = s:get_sid_prefix()
delfunction s:get_sid_prefix

let g:ctrlp_ext_var = add(get(g:, 'ctrlp_ext_vars', []), {
      \ 'init': s:sid_prefix . 'init()',
      \ 'accept': s:sid_prefix . 'accept',
      \ 'lname': 'sample',
      \ 'sname': 'sample',
      \ 'type': 'path',
      \ 'nolim': 1
      \})
let s:id = s:ctrlp_builtins + len(g:ctrlp_ext_vars)
unlet s:ctrlp_builtins s:sid_prefix


function! ctrlp#sample#id() abort
  return s:id
endfunction


function! s:init() abort
  return filter(split(globpath('.', '*', 1), "\n"), 'filereadable(v:val)')
endfunction

function! s:accept(mode, str) abort
  call ctrlp#exit()
  echomsg len(readfile(a:str))
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

ctrlp.vimのエクステンションのテンプレート

僕はctrlp.vimのエクステンションを作成する際に,以下のテンプレートを用いている. <+FILEBASE+> を適宜,エクステンションのファイル名からディレクトリ部と,拡張子を取り除いたものにすればよい( :%s/<+FILEBASE+>/\=fnamemodify(expand('%'), ':t:r') ). これを thinca/vim-template などのテンプレートプラグインと組み合わせると,サクッとエクステンションを作ることができるはずだ.

なお,以下のテンプレートには,CtrlPバッファでハイライトするコードは含まれない. 僕の場合,ハイライトが必要なエクステンションを書くことがあまり無いので,必要に応じて追加するようにしているためだ.

if exists('g:loaded_ctrlp_<+FILEBASE+>') && g:loaded_ctrlp_<+FILEBASE+>
  finish
endif
let g:loaded_ctrlp_<+FILEBASE+> = 1
let s:save_cpo = &cpo
set cpo&vim

let s:ctrlp_builtins = ctrlp#getvar('g:ctrlp_builtins')

function! s:get_sid_prefix() abort
  return matchstr(expand('<sfile>'), '\zs<SNR>\d\+_\zeget_sid_prefix$')
endfunction
let s:sid_prefix = s:get_sid_prefix()
delfunction s:get_sid_prefix

let g:ctrlp_ext_var = add(get(g:, 'ctrlp_ext_vars', []), {
      \ 'init': s:sid_prefix . 'init()',
      \ 'accept': s:sid_prefix . 'accept',
      \ 'lname': '<+FILEBASE+>',
      \ 'sname': '<+FILEBASE+>',
      \ 'type': 'line',
      \ 'enter': s:sid_prefix . 'enter()',
      \ 'exit': s:sid_prefix . 'exit()',
      \ 'opts': s:sid_prefix . 'opts()',
      \ 'sort': 0,
      \ 'specinput': 0,
      \ 'nolim': 1
      \})
let s:id = s:ctrlp_builtins + len(g:ctrlp_ext_vars)
unlet s:ctrlp_builtins s:sid_prefix


function! ctrlp#<+FILEBASE+>#id() abort
  return s:id
endfunction


function! s:init() abort
  " Gather candidates
  let candidates = []
  return candidates
endfunction

function! s:accept(mode, str) abort
  call ctrlp#exit()
  " Write actions
endfunction

function! s:enter() abort
  " Called before s:init()
  " For example: get filetype
endfunction

function! s:exit() abort
  " Called when exit ctrlp
endfunction

function! s:opts() abort
  " Called before s:enter()
  " Set options etc...
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

最小限の機能に絞ったテンプレートならば,以下のようになるだろう.

if exists('g:loaded_ctrlp_<+FILEBASE+>') && g:loaded_ctrlp_<+FILEBASE+>
  finish
endif
let g:loaded_ctrlp_<+FILEBASE+> = 1
let s:save_cpo = &cpo
set cpo&vim

let s:ctrlp_builtins = ctrlp#getvar('g:ctrlp_builtins')

function! s:get_sid_prefix() abort
  return matchstr(expand('<sfile>'), '\zs<SNR>\d\+_\zeget_sid_prefix$')
endfunction
let s:sid_prefix = s:get_sid_prefix()
delfunction s:get_sid_prefix

let g:ctrlp_ext_vars = add(get(g:, 'ctrlp_ext_vars', []), {
      \ 'init': s:sid_prefix . 'init()',
      \ 'accept': s:sid_prefix . 'accept',
      \ 'lname': '<+FILEBASE+>',
      \ 'sname': '<+FILEBASE+>',
      \ 'type': 'line',
      \ 'nolim': 1
      \})
let s:id = s:ctrlp_builtins + len(g:ctrlp_ext_vars)
unlet s:ctrlp_builtins s:sid_prefix


function! ctrlp#<+FILEBASE+>#id() abort
  return s:id
endfunction


function! s:init() abort
  " Gather candidates
  let candidates = []
  return candidates
endfunction

function! s:accept(mode, str) abort
  call ctrlp#exit()
  " Write actions
endfunction


let &cpo = s:save_cpo
unlet s:save_cpo

基本的にこれらのテンプレートの s:init()s:accept() を中心に実装するだけでよいはずだ.

最後に

ctrlp.vimのエクステンションを作るにあたって覚えなければならないことは,unite.vimより少なく,unite sourceより簡単に作ることができる. 日本人が作ったプラグインには,unite sourceが付属しているものが多いが,ctrlp.vimのエクステンションが付属しているプラグインは少ない. unite sourceを付属させるのであれば,是非ともctrlp.vimのエクステンションも付属させてみてはどうだろうか?

追記

2015 / 12 / 01

何とあのmattnさんにこの記事を取り上げていただいた!

当初は割とパパッと書いた記事で,恥ずかしい間違いなどもあったのだが,まさかmattnさんに紹介していただけると思っていなかった.

mattnさんはブログ中で, g:ctrlp_ext_vars に追加するときに if ~ else を用いるか,get() を用いるかについて堀下げて解説してくださっている. 特に,get() の第一引数に g: という一見変数っぽくないものを与えるあたりについて解説してくださっている.

let s:sample_var = {
      \ 'init': s:sid_prefix . 'init()',
      \ 'accept': s:sid_prefix . 'accept',
      \ 'lname': 'sample extension',
      \ 'sname': 'sample',
      \ 'type': 'path',
      \ 'nolim': 1
      \}
if exists('g:ctrlp_ext_vars') && !empty(g:ctrlp_ext_vars)
  call add(g:ctrlp_ext_vars, s:sample_var)
else
  let g:ctrlp_ext_vars = [s:sample_var]
endif
unlet s:sample_var

とするより,

let g:ctrlp_ext_vars = get(g:, 'ctrlp_ext_vars', []) + [{
      \ 'init': s:sid_prefix . 'init()',
      \ 'accept': s:sid_prefix . 'accept',
      \ 'lname': 'sample extension',
      \ 'sname': 'sample',
      \ 'type': 'path',
      \ 'nolim': 1
\}]

とした方が上級Vimmerに見られるようなので,みなさんも要チェックだ.

ちなみに,Vim scriptはパースがネックである部分もあるので,前者より後者の方が高速に動作するはずだ. カッコよさの他,マシンに優しくしたり,電力消費を僅かに抑えたいのであれば,後者を用いるとよいだろう. (1度しか実行されない処理に何を言っているんだ,というツッコミは無しでお願いしたい)

参考

Windowsでneovimを使う

注意(2016/10/18 追記)

この記事の情報は古くなっているので、新しい記事を参照してください.

前書き

僕自身,neovim自体にはあまり注目していなかった人間であるが,neovimが頑張ってるっぽく,かなり話題を耳にするようになったので,敢えてWindowsでneovimを試すことにした. Windowsで試すきっかけとなったのは,Osaka.vim #6にて,「Windows用のneovimのバイナリって無いんですかね?」と聞いたところ,公式で配布されており,Wikiにもそのことが書かれているという情報をいただいたからである. また,(ゴミ)プラグイン作成者としても,自分のプラグインがneovimで動作するかどうかには興味があった.

ダウンロード

公式でneovim本体のバイナリとQtで実装されたフロントエンドのバイナリが配布されているので,それらをダウンロードしてくる. 公式のWikiに書かれている通り,

  • neovimのAppVeyorからneovim本体のバイナリをダウンロードする(手順は以下とgifアニメを参照).
    • Environment: GENERATOR=Visual Studio 14 Win64, DEPS_PATH=deps64 というラベルをクリック.
    • Artifacts タブをクリック.
    • Neovim.zipをダウンロードし,解凍.
  • Releases · equalsraf/neovim-qtより,neovim-qt.zipをダウンロードし,解凍.
  • Neovim\nvim.exeneovim-qt\ に移動.
  • Neovim\share\nvim\runtime\ 以下にあるファイルを全て neovim-qt\ にぶちまける.

f:id:koturn:20151111194729g:plain

という手順を踏むとよい. あとは,nvim-qt.exe をダブルクリックすると,neovimが起動する. neovim本体である nvim.exe 単体ではneovimを起動できない(Windowsのコンソールをサポートしていない)ので注意すること.

自分でビルドしたい場合

AppVeyorのビルドログを参考にし,以下の順番でコマンドを実行するとよい. ただし,cmake,git,python2.7以上,Visual Studio 2015(2015以上であることが必須)などがインストールされており, cl.exe などのツールがコマンドプロンプトで使用できることが前提である.

:: MSVCでのビルド用のリポジトリ/ブランチを持ってくる
> git clone -q --branch=tb-mingw https://github.com/equalsraf/neovim.git
> git checkout -qf 27f0fa51e9ca886c87159ee1e2c394426d64d128
> cd neovim
:: 環境変数の設定
> set GYP_MSVS_VERSION=2015
> set GENERATOR=Visual Studio 14 Win64
> set DEPS_PATH=deps64
:: サードパーティライブラリのビルド
> mkdir %DEPS_PATH%
> cd %DEPS_PATH%
> cmake -G "%GENERATOR%" ..\third-party\
> set PATH=%PATH%;C:\cygwin64\bin\
> cmake --build .
> cd ..
:: neovimのビルド
> mkdir build
> cd build
> cmake -G "%GENERATOR%" -DCMAKE_BUILD_TYPE=Debug -DDEPS_PREFIX=..\%DEPS_PATH%\usr -DCMAKE_INSTALL_PREFIX=..\INSTALL ..
> cmake --build .

ある程度の開発環境が整っているならば,これでビルドすることができるはずである. 現在のところ,本家のリポジトリを持ってくるわけではないが,将来的には本家から持ってくるようにできるのではないだろうか?

なお,Cygwinバイナリがあるディレクトリのパスを通していると,思わぬところでつまづいたりするので注意すること. neovim-qtのビルドについてはやっていない.(ただのフロントエンドだろうし...)

neovimの設定ファイル

最近になって,Vim%HOME%\_vimrcLinux環境では ~/.vimrc)にあたるファイルは %HOME%\.nvimrc から %XDG_CONFIG_HOME%\nvim\init.vimに,%HOME%\vimfiles\Linux環境では ~/.vim/)にあたるディレクトリは %HOME%\.nvim\ から %XDG_CONFIG_HOME%\nvim\ に変更された.

Linux環境における $XDG_CONFIG_HOME のデフォルトディレクトリ( $XDG_CONFIG_HOME 環境変数が存在しない場合のディレクトリ)は, ~/.config/ であるが,Windowsでは, %AppData% がデフォルトディレクトリとなるらしい(参考). 僕は,Linuxと同様に %XDG_HOME_CONFIG%%AppData% ではなく, %HOME%\.config とした.

既存のVimが用いる .vimrcvimfiles\ をneovimの設定として扱いたい場合,以下のようにしてシンボリックリンクを作成するとよい. (ただし,コマンドプロンプトを管理者権限で起動する必要があるかもしれない. また,環境変数 %XDG_CONFIG_HOME%%HOME% がちゃんと設定され,共に存在することを確認しておこう.)

> mklink /D %XDG_CONFIG_HOME%\nvim %HOME%\vimfiles\
> mklink %HOME%\vimfiles\init.vim %HOME%\_vimrc

init.vimに関しては,シンボリックリンクを作成するのではなく,以下のように記述するのもよいだろう.

source ~/_vimrc

この場合, init.vim にneovimだけで用いる設定を記述することが可能である.

init.vim は新規に書くことが望ましいが,僕は4000行を超える .vimrc を記述しており,neovimの init.vim 用に .vimrc を書き直すようなことはしたくなかったので, .vimrc にneovimの設定も記述することにした. neovimの判定は has('nvim') という式を用いるとよい. neovimを利用している環境であるならば,この式は 1 と評価されるはずである.

注意点

僕が軽くWindowsのneovim + neovim-qtを触って遭遇した問題は,以下のようなものである.

  • フロントエンドであるneovim-qtの問題であると思われるが,文字コードutf-8以外のファイルを開くと文字化けする.
  • neovim側から新規ファイルを作成しようとすると(存在しないファイルを編集しようとすると),"Permission denided" となってしまい,強制的に保存しないといけない(:write! を用いる).
  • コンパイル時にいくつかのオプションを無効化しているので,既存の _vimrc を用いるとエラーが発生した.
    • 例えば,:language コマンドは使用できなかった.
  • neovim本体をフロントエンドであるneovim-qtを用いているので, has('gui_running')0 となることを期待していたが,起動時は 0.起動後は 1 を返却するようになっていた.
  • 当然のことであるが,一部のプラグインはうまく動作しない
    • bling/vim-airline を導入して, :split:vsplit などとして画面を分割すると,盛大にエラー吐いたなど ....

感想

neovimの公式が配布しているWindows向けのバイナリには難が多いと感じた. Linuxで自分でビルドした場合,エンコーディング等の問題もなく,快適に使うことができたので,現状ではWindows対応がややおろそかであると思われる. また, :terminal コマンドが現状では利用できなかったのが少し残念であった.

参考