実行結果の出力先をモードレスウィンドウとして起動したVimにする

![modeless_demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51728/c1705652-0aaa-f96e-7a9b-295f4a79d4eb.gif)

# はじめに
Vim上で起動した実行結果は、基本的にはウィンドウ分割された別のバッファに出力されるのですが、

- ソースコード用の画面が狭くなる
- サブモニタに結果を出力したい

という個人的な不満がありました。

そこで、あーIDEのようにfloating windowに結果を出せたらなあ、という事をふと考え、実装してみる事にしました。
どんな感じになるかは冒頭のデモ画面の通りです、Rustを[QuickRun](https://github.com/thinca/vim-quickrun)で実行し、本来バッファに出力される実行結果をPythonサーバー経由で別のVimに送信し出力しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51728/0f941864-4970-768b-cfa0-c8726c194bf0.png)

Vimから直接他のVimプロセスと通信出来れば、例えばメインエディタ側をサーバーに出来ればPythonサーバーは省けるのですが、あまり詳しく調べていないのでVimにもサンプルとして同封されているPythonサーバーを改造しソケット通信で実行結果を渡す方法をとりました。

これらの実装一式はプラグインとしてまとめています、QuickRunは実利用の一例であり、このプラグインでは渡したい文字列をソケット通信で送受信する処理一式を提供しています。

[wordijp/vim-modeless-window](https://github.com/wordijp/vim-modeless-window)

# QuickRunの設定と仕組みについて

冒頭のデモをどのように実装しているかを解説します。
*※詳細は折り畳み*

## 実行結果の送信

QuickRunはそのままでは実行結果がoutputterに出力されてしまいますので、hook機能を使い出力前に中身をキャッシュに退避させちゃいます、そして実行が終った時にPythonサーバーに結果を送信します。
この時に`outputter/buffer/close_on_empty`を1にしてメインエディタ側で出力用の分割ウィンドウが開かないようにしておきます。

<details>
<summary>.vimrcのQuickRun設定</summary><div>

```vim
" .vimrc

let g:quickrun_config = {
\  'rust-run-modeless': {
\    'command' : 'cargo',
\    'cmdopt': 'run',
\    'args': '--quiet',
\    'exec' : '%c %o %a',
\    'outputter': 'buffer',
\    'outputter/buffer/close_on_empty': 1,
\  },
\ }

let s:hook = {
\ 'name': 'modeless',
\ 'kind': 'hook',
\ 'output_data': '',
\ }

function! s:hook.on_output(data, session)
  let self.output_data .= a:session.data
  let a:session.data = '' " empty output
endfunction

function! s:hook.on_exit(session, context)

  " 実行結果の送信
  let l:txt = split(self.output_data, '\r\n\|\n')
  call modeless_window#send(l:txt)

  echohl MoreMsg
  echomsg 'output: to modeless'
  echohl NONE
endfunction

call quickrun#module#register(s:hook, 1)
unlet s:hook
```
</div></details>

## 送受信時のソケット通信

ソケット通信はVimのchannelを利用します、`ch_open`でport番号決め打ちで送信用、受信用二つを開きますが、メインエディタ側では送信用のみを開き、`ch_sendexpr`で送信します。

<details>
<summary>送信処理</summary><div>

```vim
function! modeless_window#send(txt)
  let l:ch = ch_open('127.0.0.1:'.s:send_port)
  let l:st = ch_status(l:ch)
  if l:st ==# 'fail' || l:st ==# 'closed'
  else
    call ch_sendexpr(l:ch, a:txt)
  endif
endfunction
```

</div></details>

対して受信側のモードレスウィンドウとして起動したVimでは、非同期で実行する為にこれまた`ch_sendexpr`を使いサーバーからの返事を待ちます。

<details>
<summary>受信処理</summary><div>

```vim
function! modeless_window#run() abort
  " メインエディタ側から呼ばれ、起動する
  call vimproc#system_bg(
    \ 'gvim' .
    \ ' +"set columns=80"' .
    \ ' +"set lines=20"' .
    \ ' +"set titlestring=[MODELESS_WINDOW]"' .
    \ ' +"set bufhidden=hide buftype=nofile noswapfile nobuflisted"' .
    \ ' +"call modeless_window#_recv()"')
endfunction

let s:ch = v:null

function! modeless_window#_recv()
  if s:ch == v:null
    let s:ch = ch_open('127.0.0.1:'.s:recv_port)
  endif
  let l:st = ch_status(s:ch)
  if l:st ==# 'fail' || l:st ==# 'closed'
    :q!
    return
  endif
  
  call ch_sendexpr(s:ch, 'message', {'callback': 'modeless_window#_recv_handler'})
endfunction


function! modeless_window#_recv_handler(handle, msg)
  if a:handle ==# 'fail' || a:handle ==# 'closed'
    :q!
    return
  endif

  " バッファ内容を文字列引数へと置き換えるヘルパ関数
  call modeless_window#utils#settext(a:msg)
  
  " 次へ
  call modeless_window#_recv()
endfunction
```

</div></details>


## サーバーの送受信処理

サーバーといっても二つのソケットを開き、送信内容をそのまま受け流すだけですのでシンプルな作りです、VimにはPython製のソケット通信サンプル(`tools/demoserver.py`)が付属してますのでこれを改造しています。

<details>
<summary>送受信処理(全掲載)(受信用にバグがあります)</summary><div>

```python
#!/usr/bin/python
# vim-modeless-window/tools/modeless_server.py
#
# from) $VIM/tools/demoserver.pyを拝借
#
# This requires Python 2.6 or later.

from __future__ import print_function
import json
import socket
import sys
import threading

import time

try:
    # Python 3
    import socketserver
except ImportError:
    # Python 2
    import SocketServer as socketserver


shared_message = None

lock = None

# メッセージ送信用
class ThreadedTCPSendHandler(socketserver.BaseRequestHandler):

    def handle(self):
        print("=== [S] socket opened ===")
        global shared_message
        global lock
        while True:
            try:
                data = self.request.recv(40960).decode('utf-8') # NOTE: 適当なサイズ
            except socket.error:
                print("=== [S] socket error ===")
                break
            except IOError:
                print("=== [S] socket closed ===")
                break
            if data == '':
                print("=== [S] socket closed ===")
                break
            # print("received: {0}".format(data))
            try:
                decoded = json.loads(data)
            except ValueError:
                print("json decoding failed")
                decoded = [-1, '']

            # 本分の取得
            response = ''
            if decoded[0] >= 0:
                with lock:
                    shared_message = decoded[1]

                response = 'sended!'
            else:
                response = 'what?'

            encoded = json.dumps([decoded[0], response])
            print("sending {0}".format(encoded))
            self.request.sendall(encoded.encode('utf-8'))


# メッセージ受信用
class ThreadedTCPReceiveHandler(socketserver.BaseRequestHandler):

    def handle(self):
        print("=== [R] socket opened ===")
        global shared_message
        global lock
        while True:
            try:
                data = self.request.recv(4096).decode('utf-8')
            except socket.error:
                print("=== [R] socket error ===")
                break
            except IOError:
                print("=== [R] socket closed ===")
                break
            if data == '':
                print("=== [R] socket closed ===")
                break
            print("received: {0}".format(data))
            try:
                decoded = json.loads(data)
            except ValueError:
                print("json decoding failed")
                decoded = [-1, '']

            if decoded[0] >= 0:
                if decoded[1] == 'message':
                    # メッセージをひたすら待つ
                    # FIXME: 受信するまで終われない
                    while True:
                        message = None
                        with lock:
                            if shared_message != None:
                                message = shared_message
                                shared_message = None

                        if message != None:
                            encoded = json.dumps([decoded[0], message])
                            print('sending message text')
                            self.request.sendall(encoded.encode('utf-8'))
                            break

                        time.sleep(0.1)
                else:
                    # 知らないリクエスト
                    print('unknown request {0}'.format(decoded[1]))
                    pass

class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
    pass

def send_server():
    HOST, PORT = "localhost", 13578
    return server(HOST, PORT, ThreadedTCPSendHandler)

def recv_server():
    HOST, PORT = "localhost", 13579
    return server(HOST, PORT, ThreadedTCPReceiveHandler)

def server(host, port, tcpHandler):
    server = ThreadedTCPServer((host, port), tcpHandler)
    ip, port = server.server_address

    # Start a thread with the server -- that thread will then start one
    # more thread for each request
    server_thread = threading.Thread(target=server.serve_forever)

    # Exit the server thread when the main thread terminates
    server_thread.daemon = True
    server_thread.start()
    print("Server loop running in thread: ", server_thread.name)

    print("Listening on port {0}".format(port))

    return server


if __name__ == "__main__":
    lock = threading.RLock()

    send_server = send_server()
    recv_server = recv_server()

    while True:
        typed = sys.stdin.readline()
        if "quit" in typed:
            print("Goodbye!")
            break

    send_server.shutdown()
    send_server.server_close()
    recv_server.shutdown()
    recv_server.server_close()
```

</div></details>