LSP: spec and implementation - scmlab/gcl-all GitHub Wiki
現實
LSP protocol 在 client server 端各有 framework
我們先接受 LSP 規範的限制
也要接受 VS Code core API / vscode-languageclient / Haskell LSP server 實作的限制
我們 client 端用的是 TypeScript 的 vscode-languageclient 9.0.1
Server 端用的是 Haskell 的 lsp-2.7.0.1
內容狀態的維護
Server 端會維護一個在 memory 中的 VFS
使用者即便在 editor 中還沒有 save to disk
VFS 中也會有 editor 中的資料
這是怎麼做到的呢?
Client 會先發送 didOpen notification 並包含著檔案的完整內容
此時宣告 client 控制著這個檔案
server 不該從 disk 讀取
再來由 client 發送 textDocument/didChange 給 server
Server framework 用此來更新 VFS 的內容
我們這邊選用的是 incremental update
LSP 保障 didChange 的版本號必定增加, 但不保證一次只會加一:
interface VersionedTextDocumentIdentifier extends TextDocumentIdentifier {
/**
* The version number of this document.
*
* The version number of a document will increase after each change,
* including undo/redo. The number doesn't need to be consecutive.
*/
version: integer;
}
Thread
如果 server 端我們沒有另外建立 thread
則 lsp-2.7.0.1 只會有一個 thread
所有 client 來的 message 將會循序執行
也因此不用考慮 server 本身的 concurrent issues
如果任何 handler 正在執行的時候 didChange 來了
則在 thread 離開 handler 前, VFS 的內容是不會被更新的
因為 didChange 會 queue 住等待 thread 來處理
順序
LSP spec 沒有完全保證 request / response / notification 之間的順序
實作也沒有
(我們還沒有探索如何在 client middleware 製造 message order 的作法)
從 spec 可得出, client 會送出版本一直增加的 didChange notification
我們可以以此為主軸 如同 clock 一般的存在
其餘的相對順序則需要從因果關係來推導
Server edit request / edit response / didChange
Server 送出 workspace/applyEdit request 其 response 和 didChange notification 間沒有順序 也就是說 可能是這樣
<- edit request
-> edit response *
-> didChange v10
-> didChange v20
也可能是這樣
<- edit request
-> didChange v10
-> edit response *
-> didChange v20
甚至可能是這樣
<- edit request
-> didChange v10
-> didChange v20
-> edit response *
如果 edit response 晚發生, 在那個時間點做處理會比較難以想像.
Server edit 的 property
我們推論系統有以下的 property:
如果 edit request 指定版本, 且 edit 成功, 則下一個處理到的 didChange 就是(*)屬於這個 edit 的.
假設現在 server VFS 的 version 是 v10.
下一個 didChange 的 version 不會小於 10, 因為
- 要處理完 v10 的 message VFS 才會是 v10
- didChange 的 version 是嚴格遞增的
下一個 didChange 不會是別的 edit 造成的, 因為
- client 端版本先改才會送 didChange (從實作看)
- workspace/applyEdit 指定版號, client 會先檢查版本相等才修改 (從實作看, spec 也是)
- 如果編輯要能成功, 必須要在版本修改前發生
此處我們假設:
- 假設檢查版本和發出修改之間沒有別人發出修改 (實作限制)
- 發出修改 / 版本號增加 / 發出 didChange 的順序和沒有人插隊 (沒有想更仔細)
藉由這個 property, 我們可以儘早結束一個 editing cycle, 讓後面的 response / didChange 邏輯可以確定地處理.
response 先來: 很快知道成敗(並紀錄)
didChange 先來: 比對改過的 VFS 和我們預先計算的檔案內容來判斷成敗
也就是我們不用擔心 response 很晚來. 我們建立了很快就知道成敗的方法.
Optimistic
對於 load 來說, 把 ? edit 成 [! !] 後再拿著 source load 就好, 沒有什麼處理一半的 state 要保留到修改後的 load.
對於 refine 來說, 中斷要再接下去做比較麻煩.
一種作法是:
server 端直接假設 edit 成功
用成功的路線繼續算出新的 FileState
但另存在 memory
原來的 state 先不動
等到緊接的 edit response 或 update 來知道真的成功時再取代 或知道失敗則拋棄
這個等待時間應該非常短
且不依賴使用者的下一次 input
onDidChange
魔王還是 didChange 的邏輯
檔案本來有 ABC 三塊
A
B
C
從 B 換成 B' 時
A 裡面的東西不動
C 裡面的東西的位置都要調整
而和 B 交疊的東西都被丟掉不能再用
那這和 Load / Refine 怎麼搭配呢?
新的想法:
Load / Refine 會根據編輯成功的樣子算出一整份新的 FileState 先放在旁邊
如果確知編輯失敗就丟掉
如果確知編輯成功 就在屬於自己的那個 didChange 中把 FileState 拿起來 並且不做 didChange
所以不再需要 editVersion
因為 editVersion 只是用來判斷收到 didChange 時要不要調整
(或者本來也只需要 isNew 一個 flag 貼在上面就好 不用到 integer?)