はじめに
Vim上で起動した実行結果は、基本的にはウィンドウ分割された別のバッファに出力されるのですが、
- ソースコード用の画面が狭くなる
- サブモニタに結果を出力したい
という個人的な不満がありました。
そこで、あーIDEのようにfloating windowに結果を出せたらなあ、という事をふと考え、実装してみる事にしました。
どんな感じになるかは冒頭のデモ画面の通りです、RustをQuickRunで実行し、本来バッファに出力される実行結果をPythonサーバー経由で別のVimに送信し出力しています。
Vimから直接他のVimプロセスと通信出来れば、例えばメインエディタ側をサーバーに出来ればPythonサーバーは省けるのですが、あまり詳しく調べていないのでVimにもサンプルとして同封されているPythonサーバーを改造しソケット通信で実行結果を渡す方法をとりました。
これらの実装一式はプラグインとしてまとめています、QuickRunは実利用の一例であり、このプラグインでは渡したい文字列をソケット通信で送受信する処理一式を提供しています。
QuickRunの設定と仕組みについて
冒頭のデモをどのように実装しているかを解説します。
※詳細は折り畳み
実行結果の送信
QuickRunはそのままでは実行結果がoutputterに出力されてしまいますので、hook機能を使い出力前に中身をキャッシュに退避させちゃいます、そして実行が終った時にPythonサーバーに結果を送信します。
この時にoutputter/buffer/close_on_empty
を1にしてメインエディタ側で出力用の分割ウィンドウが開かないようにしておきます。
.vimrcのQuickRun設定
" .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
送受信時のソケット通信
ソケット通信はVimのchannelを利用します、ch_open
でport番号決め打ちで送信用、受信用二つを開きますが、メインエディタ側では送信用のみを開き、ch_sendexpr
で送信します。
送信処理
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
対して受信側のモードレスウィンドウとして起動したVimでは、非同期で実行する為にこれまたch_sendexpr
を使いサーバーからの返事を待ちます。
受信処理
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
サーバーの送受信処理
サーバーといっても二つのソケットを開き、送信内容をそのまま受け流すだけですのでシンプルな作りです、VimにはPython製のソケット通信サンプル(tools/demoserver.py
)が付属してますのでこれを改造しています。
送受信処理(全掲載)(受信用にバグがあります)
#!/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()