2019 年振り返り

f:id:heavenshell:20191231171259p:plain

とにかく辛い一年だった。今までの一番の底。 春先に辛い出来事があって、今年はその状態から立て直そうと精一杯生きようとしてたら、夏に退職勧奨を受けたので職場を変えた。

悲しみのどん底にいるときに、色々察してくれて、そっと支えてくれた最高のチームで、 そのチームの仲間全員を卑怯な方法で追い出すとか、今でも意味がわからんし怒りしかない。

よくよく考えてみればチームごとのレイオフか。

悲しみの方はもうほんと時が経つのを待つしかない感じだが、受けた仕打ちに対しての怒りと恨み辛みは当分尽きそうにない。

という感じですっかりメンタルをやられた一年だった。

作ったもの

pocke.hatenablog.com

https://github.com/heavenshell/vim-genshijin
https://github.com/heavenshell/vim-recents
https://github.com/heavenshell/ts-react-hooks-redux
https://github.com/heavenshell/vim-eslint
https://github.com/heavenshell/vim-golinter
https://github.com/heavenshell/ts-react-redux-hooks
https://github.com/heavenshell/vim-inhibitor
https://github.com/heavenshell/rs-pluginbaby
https://github.com/heavenshell/nkgrnkgr.github.io
https://github.com/heavenshell/ts-react-static
https://github.com/heavenshell/ts-angular
https://github.com/heavenshell/ts-angular-tutorial

vim 関連は相変わらずネタプラグインと linter しか書いてない。 TypeScript がらみは React の素振りと Angular の素振り。

来年

ただただ平和に平穏に生きたい…。辛いことも悲しいことはもうたくさん。

Tsuquyomi で popup する

本記事は Vim advent calendar 21 日目の記事です。

例年は割と準備してたけど、もっとカジュアルに書いていい気がしたという完全な言い訳をして、自分の vimrc に書いてた設定をブログにコピペ。

その 1

TypeScript で変数とか関数のシグネチャーとかをポップアップで表示する。 組み込みの popup 関係の関数だと表示位置によって微妙にポップアップの位置をずらすとか微調整ができなかったので、自前で描画する場所を計算してやる。

let s:winid = -1
function! s:ts_hint_popup()
  if s:winid != -1
    call popup_close(s:winid)
    let s:winid = -1
  endif
  let l:content = tsuquyomi#hint()
  let l:contents = split(l:content, '\n')

  let l:border_size = 2 " both side of `|` and top and bottom `-`

  " col position
  let l:current_col = col('.')
  let l:max_width = strlen(sort(copy(l:contents), function('s:sort'))[0])

  let l:col = l:current_col
  if l:current_col + l:max_width > &columns
    " popup is overflowed
    if l:max_width > &columns
      let l:col = 0
    else
      " If popup is overflow from buffer window, popup like followings.
      "
      " +=========================+
      " |      +-----------------+|
      " |      |const foo: string||
      " |      +-----------------+|
      " |  { bar, baz, bazz, foo }|
      " |                    ^    |
      " |     cursor is here |    |
      " +=========================+
      "
      let l:col = l:current_col + (&columns - (l:current_col + l:max_width + l:border_size)) + 1
    endif
  endif

  " line position
  let l:current_line = winline()
  let l:popup_height = len(l:contents) + l:border_size

  " Calc popup overflow size
  for l:line in l:contents
    let l:width = strlen(l:line)
    if l:width + l:border_size >= &columns
      let l:popup_height += 1
    endif
  endfor

  let l:lnum = l:current_line < l:popup_height
    \ ? l:current_line + 1
    \ : l:current_line - l:popup_height

  let s:winid = popup_create(split(l:content, '\n'), {
    \ 'line': l:lnum,
    \ 'col': l:col,
    \ 'border': [1, 1, 1, 1],
    \ 'moved': 'WORD',
    \ })
  let bufnr = winbufnr(s:winid)
  call setbufvar(winbufnr(s:winid), '&filetype', &filetype)
  return s:winid
endfunction

command! TsHintPopup :call s:ts_hint_popup()
noremap <silent> <buffer> <Plug>(TsuHintPopup) :<c-u>TsuHintPopup<CR>

Tsuquyomi には変数とかシグネチャーを取得する関数があるので、それを利用して、Vim のポップアップに流し込んだだけ。

その 2

Angular を書いてて、html でも補完とか、定義ジャンプを使いたい(こっちがメイン)。 Angular は LSP を提供してるので、それを使う。 VSCode で Angular Language Service(angular.ng-template-0.802.3) の拡張を入れる(angular.ng-template-0.900.3 はうまく動いてなかったので要調査)。

function! s:is_angular()
  let current = expand('%:p:h')
  let path = findfile('angular.json', current . ';')
  if path == ''
    return 0
  endif
  return 1
endfunction

augroup Angular
  let s:server = expand('~/.vscode/extensions/angular.ng-template-0.802.3/server/server.js')
  let g:lsp_async_completion = 1
  autocmd User lsp_setup call lsp#register_server({
    \ 'name': 'Angular Language Service',
    \ 'cmd': {server_info -> [&shell, &shellcmdflag, printf('node %s --stdio', s:server)]},
    \ 'root_uri':{server_info -> lsp#utils#path_to_uri(lsp#utils#find_nearest_parent_file_directory(lsp#utils#get_buffer_path(), 'angular.json'))},
    \ 'whitelist': ['html'],
    \ })
augroup END
autocmd FileType html if s:is_angular() | setlocal omnifunc=lsp#complete | endif

なお Angular の TS なファイルは普通に Tsuquyomi で何の問題もなく動いている。

追記

最新 の angular.ng-template-0.900.3 の場合。

let base = expand(printf('~/.vscode/extensions/%s', 'angular.ng-template-0.900.3'))                  
let node_modules = printf('%s/node_modules/', base)
let ng = expand(printf('--ngProbeLocations %s/server', base))
let ts = expand(printf('--tsProbeLocations %s', node_modules))
let s:server = printf('%s/server %s %s', base, ng, ts)

とすればうまくいく。

また angular.ng-template-0.802.3 は適当に GitHub からダウンロードして、

yarn
yarn compile

して出来上がった /angular.ng-template-0.802.3/server/out/server.jslet s:server に指定すれば OK。

2018 年振り返り


今年もほぼお仕事のコミット。
py-pixela を作ったくらい。あとは Vim script。

健康問題

年末(先週くらい)から風邪をひいている。体が弱いくせに風邪は滅多にひかないと言う特殊体質なので、咳が止まらなくて寒気でからが震え、頭痛がする。
加齢とともに回復力が落ちてきた気はする。


2017 年は肩とか耳とかが酷かったけど、今年はマシになった。
肩は上がるようになったが、痛みがないわけではないので、うまく付き合っていくしかなさそう。

体重

去年の年末に少し減らしたけど、正月明けたら見事にリバウンドして上昇。
夏に健康診断時に体重の数値がひどいことになっていたので、痩せる決意をする。
4 ヶ月で大体 8kg 減。来年もこのままこのペースを続けていきたい。

買ってよかったもの

割と機材にお金を突っ込んだ気がする。

  • Shure BETA 58A
  • マイクスタンド
  • Mackie Onyx Black Jack
    • もう絶版で最後に残ってたのがあったからそれ
    • 仕事用のデスクは奥行きが狭く、小さいものがよかった


ビデオ会議で声が聞きやすくなったと言われた(自分ではわからない)。
58A はダイナミックマイクなので、口元につけるオンマイク状態じゃないと厳しいことがわかった(油断すると聞こえないと言われる)。
同僚が導入した M-Audio uber はとても聞き取りやすかったので、次はコンデンサーマイク(BETA 87Aあたり)かなぁと思っている。
コンデンサーマイクになると保管問題が出てくるし、そうなると防湿庫も欲しいと思ってる(カメラも入れたいし)。

来年

マイペースにいきたい。
健康はなんとかせんといかんなぁ。

tsuquyomi を魔改造している

https://qiita.com/advent-calendar/2018/vim 22 日目の記事です。

tl;dr

tsuquyomi でエラーチェックが同期で走って Vim の UI をブロックしストレスがたまるから、魔改造して非同期で動く仕組みを作った。
https://github.com/heavenshell/tsuquyomi/tree/feature/async

はじまり

今年はお仕事で TypeScript を本格的に書くようになった。
2016 年に TypeScript を少し書いた時も tsuquyomi を使っていた。
今回も tsuquyomi を入れて、自作の Tslint チェックをする Vim plugin を作って開発をしている。
保存時に tsuquyomi が TSServer にチェックを行っていた。
(Tslint は完全に非同期になっているので、InsertLeave 時にチェック)


最初はプロジェクトのソースコードのファイル数や依存ライブラリ数が少ないので特に速度に問題を感じなかったが、ある程度の規模になってきてから保存しチェックが終わるまで、体感的に数秒かかり Vim の UI をブロックする場合があり、ストレスになってきた。
特に起動直後の :TsuGeterr が遅い。

シンタックスエラーがあるファイルを開き :TsuGeterr をすると、Vim の UI がブロックされていることがわかると思う。

なおこの問題は tsuquyomi の Issue にもある。
https://github.com/Quramy/tsuquyomi/issues/241

ale.vim

そんな時に同僚に ale.vim がエラーチェック、補完とともによくできていることを教えてもらった。
ale.vim のエラーチェックが非同期で動作するのは勿論知っていたが、lsp による補完までサポートしているのは知らなかった。
少し使ってみたが、現状ストレスに感じているエラーチェックも完全にバックグランドで動作し、Vim の UI をブロックすることないのが素晴らしかった。
一方で非同期で補完しているため、補完のポップアップがちらつくのが少し気になった。


目に付いた欠点はその程度(あとは TypeScript + Prettier + tslint --fix との連携がうまくいかなかったが、おそらくこれは自分の設定が悪そう)だが、ale.vim は非常によくできていて、なぜか悔しかった。


また vim-lsp は試していない(mattn さんがごくごく最近メンテナの一人になられたので、今後は安心して使えるとは思う)が、おそらく非同期でいい感じで動くのだろう。
その他の最近の TypeScript な Vim プラグインも試していないが、おそらく似たり寄ったりだろう。

tsuquyomi はなぜ UI をブロックするのか

tsuquyomi も TSServer を使っているし、Vim8 の Job / Channel を使っているので、ale.vim と同様のことができるはずと仮説を立てて、tsuquyomi のコードを読んだ。


Vim8 だと TSServer には `ch_sendraw()` でリクエストしている。
https://github.com/Quramy/tsuquyomi/blob/master/autoload/tsuquyomi/tsClient.vim#L154


問題はレスポンスの箇所で、指定してたレスポンス長を超えるまでループで待っている。
https://github.com/Quramy/tsuquyomi/blob/master/autoload/tsuquyomi/tsClient.vim#L160
https://github.com/Quramy/tsuquyomi/blob/master/autoload/tsuquyomi/tsClient.vim#L178

結果的に TSServer がレスポンスを返して QuickFix に追加するまでが同期として動いており、UI をブロックしている。
特に起動直後は TSServer からのレスポンスが遅い(これは ale.vim でも同様で、TSServer 上の仕様であると思う)。
そのため上記のスクリーンキャストのように :TsuGeterr を実行すると、遅い場合は数秒固まったようになる。


tsuquyomi の関数のヘッダーコメントに `pseudo synchronously` と書いてあるので意図した動きだろうが、エラーがあれば QuickFix に入れて、ユーザーに通知するだけで良いので、レスポンスをわざわざ待つ必要はない。
ちなみに tsuquyomi が作られた頃には Vim8 の Job/Channel なんてなかったのだから、そういう実装になっているのは理解できる。

魔改造の方針

元からある TsuGeterr を改良するのではなく、TsuAsyncGeterr というように新しくインターフェイスを加える方針にする。
現状はエラーチェック(TsuGeterr)だけだが、他の機能も容易に非同期化できる作りにしたい。
なお本魔改造は Vim8 の Job/Channel に全力で依存しているので、NeoVim は一切考慮していない。
(元々 tsuquyomi も NeoVim は考慮していない)

魔改造した箇所

まずは Vim8 の Job を開始しているところにコールバックを仕込む。


https://github.com/heavenshell/tsuquyomi/blob/feature/async/autoload/tsuquyomi/tsClient.vim#L87
これで TSServer が STDOUT に出力があるたびにコールバックが反応する。
今回はエラーチェックに関するものだけを受け取りたい。


コールバックを受け取り応答メッセージを解析しエラーがあれば QuickFix に入れ、ユーザーに通知するコールバックを呼び出すという設計にした。


https://github.com/heavenshell/tsuquyomi/blob/feature/async/autoload/tsuquyomi/config.vim#L313
tsuquyomi が起動した際にここで、エラーチェックに関する関数を登録する。
TSServer からの応答メッセージにエラーがあればそれを抽出し、requestCompleted を受けたら、QuickFix に設定するリストを作成し、通知用のコールバック関数に送信するようにした。


これにより Vim 起動直後にエラーチェックを行っても UI をブロックすることがなくなった。


上記は :TsuGeterr 時と同様の条件で行った様子。:TsuAsyncGeterr 実行時にすぐに Vim の UI が操作できるのがわかると思う。


また TextChange, InsertLeave, BufWritePost イベントを登録することによって、シームレスにエラーを通知できるようになった。


ただし :TsuAsyncGeterr は :TsuGeterr に挙動を合わせているため、エラーがあれば、QuickFix を自動で開き、エラーがなければ、自動で QuickFix を閉じる。
そのため TextChange や InsertLeave に紐付けていると、QuickFix を自動で開くのが UI の操作をブロックして鬱陶しい。
エラーの通知は ale.vim 同様そっとユーザーに知らせ、ユーザーが自分で QuickFix を開けば Vim の操作をブロックすることがなくなる。


下記のように自分独自の通知先関数を .vimrc で登録すると、QuickFix の制御をできるようにした。

let s:ts_notify = 0
function! s:ts_callback(qflist)
  if s:ts_notify == 1
    call setqflist(a:qflist)
    let cnt = len(getqflist())
    if cnt > 0
      echomsg printf('[TypeScript] %s errors', cnt)
    else
      echomsg '[TypeScript] No error'
    endif
  endif
  let s:ts_notify = 0
endfunction

function! s:ts_quickfix()
  let s:ts_notify = 1
  if g:tsuquyomi_is_available == 1
    call tsuquyomi#registerNotify(function('s:ts_callback'), 'diagnostics')
    call tsuquyomi#asyncCreateFixlist()
  endif
endfunction
autocmd InsertLeave,TextChanged,BufWritePost *.ts,*.tsx silent! call s:ts_quickfix()

tsuquyomi#registerNotify() は登録したコールバック関数の引数に QuickFix に入れるリストを詰めて呼び出すので、それを受けて、自分で setqflist() を呼び、エラーがあればエコーメッセージを出し、ユーザーに通知をする。

上記は、間違った型を設定しようとして、tsuquyomi がエラーを通知を受けた様子。
(エラーハイライトは別のプラグインを使っている)

拡張性

今はエラーチェックのみだが、例えば今後補完を非同期で実装したいとなると、エラーチェックと同様に専用の関数を作成し、 https://github.com/heavenshell/tsuquyomi/blob/feature/async/autoload/tsuquyomi/config.vim#L293 ここで登録し、応答メッセージをパースし、補完候補を構築し、Vim の complete() を呼べば良い。

おまけ

tsuquyomi で関数などのシグネチャがどう定義されているかを知りたい場合などが多々ある。
ale では :ALEHover を利用すれば良い。
同様のことを tsuquyomi でするには以下のように .vimrc に登録すればできる。

function! s:ts_hint()
  let content = tsuquyomi#hint()
  silent pedit __TsuquyomiScratch__
  silent wincmd P
  setlocal modifiable noreadonly
  setlocal nobuflisted buftype=nofile bufhidden=wipe ft=typescript
  put =content
  0d_
  setlocal nomodifiable readonly
  silent wincmd p
endfunction

command! TsHint silent! :call s:ts_hint()
noremap <silent> <buffer> <Plug>(TsuHint) :TsuHint<CR>
autocmd FileType typescript nnoremap <buffer> <leader>h :TsHint<CR>

まとめ & お願い

Vim7 時代の少し前に作られたプラグインを非同期化を試みた。
色々お世話になったプラグインなので、改良して貢献をしたい。


もし tsquyomi を使っていらっしゃる方がおり、エラーチェックで UI がブロックされて辛いという方は是非 https://github.com/heavenshell/tsuquyomi/tree/feature/async を試して頂いて、フィードバックを頂きたい。
自分で仕事でも使っていて、今の所それほど怪しい動きはしていないのでそのまま PR しようかと思ったが、先にご意見などを受けてから PR したい(集まらなければそのまま PR 出そうと思う)。
Twitter で @heavenshell あたりにメンションいただければとてもありがたいです。

謝辞

Vim アドベントカレンダーに登録したものの、今回のネタどうしようと悩んでた時に TypeScript で ale.vim がいけているというのを教えてくれた同僚に感謝します。

Python Pixela client 作った

ちょっと Python 書いてないかったからリハビリ兼ねて。

py-pixela


一応現時点で全ての API には対応してる。
お仕事の Django アプリケーションは mixin がたくさんあり、また今まであんまり mixin しなかったけど練習がてら。

Jest の create() がエラーになるお話

忘備録。
Jest と react-test-renderer の create() を使って snapshot の比較のテストが落ちるようになった。

TypeError: Cannot read property 'addEventListener' of null 

さんざか調べまくった結果 react-test-renderer では ref を使ってコールバックを仕込んでるとサポート外で落ちる模様。
https://github.com/facebook/jest/issues/5462


createNodeMock を使うか Enzyme 使えと Issue に有った。
ライブラリの深いところで ref を使ってたので、createNodeMock は厳しそうだったので、やむをえなく Enzyme 使った。


react-router-redux とか使ってるので、単純に mount() したものを enzyme-to-json で toJson() しただけでは snapshot が意図したものと違うものが出来上がるので、toJson() の第二引数の { mode: 'deep' } をするようにした。

react-router-redux でハマった

忘備録。


react-router-redux v5.0.0-alpha.9 で表示している URL と match.params が一致していないことが起きた。
症状的には https://s8a.jp/react-router-redux-5-does-not-work-correctly#%E7%8F%BE%E8%B1%A1 こちらと同じ。
ブラウザの戻る/進むボタンだが、発生したのは history.push() で URL を書き換えた際に URL と match.params に入ってる値が違う。


どうも仕様みたい。
https://github.com/ReactTraining/react-router/pull/4668#issuecomment-285260711


example にも同様のコードがある。
https://github.com/ReactTraining/react-router/blob/master/packages/react-router-redux/examples/BasicExample.js
おそらく Store にある location が同期が取れてないので、ルーティングが始まる前に connect で Store にある location の情報を最新の location で書き換える。