252 lines
10 KiB
Text
252 lines
10 KiB
Text
import Lean.Data.Lsp
|
|
import Lean.Widget
|
|
open Lean
|
|
open Lean.Lsp
|
|
open Lean.JsonRpc
|
|
|
|
namespace Client
|
|
|
|
/- Client-side types for showing interactive goals. -/
|
|
|
|
structure SubexprInfo where
|
|
subexprPos : String
|
|
diffStatus? : Option String
|
|
deriving FromJson, Repr
|
|
|
|
structure Hyp where
|
|
type : Widget.TaggedText SubexprInfo
|
|
names : Array String
|
|
isInserted?: Option Bool
|
|
isRemoved?: Option Bool
|
|
deriving FromJson, Repr
|
|
|
|
structure InteractiveGoal where
|
|
type : Widget.TaggedText SubexprInfo
|
|
isInserted?: Option Bool := none
|
|
isRemoved?: Option Bool := none
|
|
hyps : Array Hyp
|
|
deriving FromJson, Repr
|
|
|
|
structure InteractiveGoals where
|
|
goals : Array InteractiveGoal
|
|
deriving FromJson, Repr
|
|
|
|
end Client
|
|
|
|
/-! Test-only instances -/
|
|
|
|
instance : FromJson Widget.PanelWidgetInstance where
|
|
fromJson? j := do
|
|
let id ← j.getObjValAs? Name "id"
|
|
let javascriptHash ← j.getObjValAs? UInt64 "javascriptHash"
|
|
let props ← j.getObjVal? "props"
|
|
let range? ← j.getObjValAs? (Option Lsp.Range) "range"
|
|
return { id, javascriptHash, props := pure props, range? }
|
|
|
|
deriving instance FromJson for Widget.GetWidgetsResponse
|
|
|
|
def Lean.Widget.GetWidgetsResponse.debugJson (r : Widget.GetWidgetsResponse) : Json :=
|
|
Json.mkObj [
|
|
("widgets", Json.arr (r.widgets.map fun w =>
|
|
Json.mkObj [
|
|
("id", toJson w.id),
|
|
("javascriptHash", toJson w.javascriptHash),
|
|
("props", w.props.run' {}),
|
|
("range", toJson w.range?),
|
|
])
|
|
)
|
|
]
|
|
|
|
open Std.Internal.Parsec in
|
|
open Std.Internal.Parsec.String in
|
|
def word : Parser String :=
|
|
many1Chars <| digit <|> asciiLetter <|> pchar '_'
|
|
|
|
open Std.Internal.Parsec in
|
|
open Std.Internal.Parsec.String in
|
|
def ident : Parser Name := do
|
|
let head ← word
|
|
let xs ← many1 (pchar '.' *> word)
|
|
return xs.foldl .str $ .mkSimple head
|
|
|
|
partial def main (args : List String) : IO Unit := do
|
|
let uri := s!"file:///{args.head!}"
|
|
-- We want `dbg_trace` tactics to write directly to stderr instead of being caught in reuse
|
|
Ipc.runWith (←IO.appPath) #["--server", "-DstderrAsMessages=false"] do
|
|
let capabilities := {
|
|
textDocument? := some {
|
|
completion? := some {
|
|
completionItem? := some {
|
|
insertReplaceSupport? := true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ipc.writeRequest ⟨0, "initialize", { capabilities : InitializeParams }⟩
|
|
let _ ← Ipc.readResponseAs 0 InitializeResult
|
|
Ipc.writeNotification ⟨"initialized", InitializedParams.mk⟩
|
|
|
|
let text ← IO.FS.readFile args.head!
|
|
let mut requestNo : Nat := 1
|
|
for text in text.splitOn "-- RESET" do
|
|
Ipc.writeNotification ⟨"textDocument/didOpen", {
|
|
textDocument := { uri := uri, languageId := "lean", version := 1, text := text } : DidOpenTextDocumentParams }⟩
|
|
let initialDiags ← Ipc.collectDiagnostics requestNo uri 1
|
|
requestNo := requestNo + 1
|
|
let initialRequestNo := requestNo
|
|
let mut lineNo := 0
|
|
let mut lastActualLineNo := 0
|
|
let mut versionNo : Nat := 2
|
|
let mut rpcSessionId : Option UInt64 := none
|
|
for line in text.splitOn "\n" do
|
|
match line.splitOn "--" with
|
|
| [ws, directive] =>
|
|
let line ← match directive.front with
|
|
| 'v' => pure <| lineNo + 1 -- TODO: support subsequent 'v'... or not
|
|
| '^' => pure <| lastActualLineNo
|
|
| _ =>
|
|
lastActualLineNo := lineNo
|
|
lineNo := lineNo + 1
|
|
continue
|
|
let directive := directive.drop 1
|
|
let colon := directive.posOf ':'
|
|
let method := directive.extract 0 colon |>.trim
|
|
-- TODO: correctly compute in presence of Unicode
|
|
let column := ws.endPos + "--"
|
|
let pos : Lsp.Position := { line := line, character := column.byteIdx }
|
|
let params := if colon < directive.endPos then directive.extract (colon + ':') directive.endPos |>.trim else "{}"
|
|
match method with
|
|
-- `delete: "foo"` deletes the given string's number of characters at the given position.
|
|
-- We do NOT check currently that the text at this position is indeed that string.
|
|
| "delete"
|
|
-- `insert: "foo"` inserts the given string at the given position.
|
|
| "insert"
|
|
-- `change: "foo" "bar"` is like `delete: "foo"` followed by `insert: "bar"` in one atomic step.
|
|
| "change" =>
|
|
let (delete, insert) ← match method with
|
|
| "delete" => pure (params, "\"\"")
|
|
| "insert" => pure ("\"\"", params)
|
|
| "change" =>
|
|
-- TODO: allow spaces in strings
|
|
let [delete, insert] := params.splitOn " "
|
|
| throw <| IO.userError s!"expected two arguments in {params}"
|
|
pure (delete, insert)
|
|
| _ => unreachable!
|
|
let some delete := Syntax.decodeStrLit delete
|
|
| throw <| IO.userError s!"failed to parse {delete}"
|
|
let some insert := Syntax.decodeStrLit insert
|
|
| throw <| IO.userError s!"failed to parse {insert}"
|
|
let params : DidChangeTextDocumentParams := {
|
|
textDocument := {
|
|
uri := uri
|
|
version? := versionNo
|
|
}
|
|
contentChanges := #[TextDocumentContentChangeEvent.rangeChange {
|
|
start := pos
|
|
«end» := { pos with character := pos.character + delete.length }
|
|
} insert]
|
|
}
|
|
let params := toJson params
|
|
Ipc.writeNotification ⟨"textDocument/didChange", params⟩
|
|
-- We don't want to wait for changes to be processed so we can test concurrency
|
|
--let _ ← Ipc.collectDiagnostics requestNo uri versionNo
|
|
requestNo := requestNo + 1
|
|
versionNo := versionNo + 1
|
|
| "collectDiagnostics" =>
|
|
if let some diags ←
|
|
if requestNo = initialRequestNo then pure initialDiags
|
|
else Ipc.collectDiagnostics requestNo uri (versionNo - 1) then
|
|
IO.eprintln (toJson diags.param)
|
|
requestNo := requestNo + 1
|
|
| "sync" => -- wait for processing but do not print diagnostics
|
|
let _ ← Ipc.collectDiagnostics requestNo uri (versionNo - 1)
|
|
requestNo := requestNo + 1
|
|
| "codeAction" =>
|
|
let params : CodeActionParams := {
|
|
textDocument := {uri := uri},
|
|
range := ⟨pos, pos⟩
|
|
}
|
|
Ipc.writeRequest ⟨requestNo, "textDocument/codeAction", params⟩
|
|
let r ← Ipc.readResponseAs requestNo (Array Json)
|
|
for x in r.result do
|
|
IO.eprintln x
|
|
requestNo := requestNo + 1
|
|
| "goals" =>
|
|
if rpcSessionId.isNone then
|
|
Ipc.writeRequest ⟨requestNo, "$/lean/rpc/connect", RpcConnectParams.mk uri⟩
|
|
let r ← Ipc.readResponseAs requestNo RpcConnected
|
|
rpcSessionId := some r.result.sessionId
|
|
requestNo := requestNo + 1
|
|
let params : Lsp.PlainGoalParams := {
|
|
textDocument := { uri }
|
|
position := pos,
|
|
}
|
|
let ps : RpcCallParams := {
|
|
params := toJson params
|
|
textDocument := { uri }
|
|
position := pos,
|
|
sessionId := rpcSessionId.get!,
|
|
method := `Lean.Widget.getInteractiveGoals
|
|
}
|
|
Ipc.writeRequest ⟨requestNo, "$/lean/rpc/call", ps⟩
|
|
let response ← Ipc.readResponseAs requestNo Client.InteractiveGoals
|
|
requestNo := requestNo + 1
|
|
IO.eprintln (repr response.result)
|
|
IO.eprintln ""
|
|
| "widgets" =>
|
|
if rpcSessionId.isNone then
|
|
Ipc.writeRequest ⟨requestNo, "$/lean/rpc/connect", RpcConnectParams.mk uri⟩
|
|
let r ← Ipc.readResponseAs requestNo RpcConnected
|
|
rpcSessionId := some r.result.sessionId
|
|
requestNo := requestNo + 1
|
|
let ps : RpcCallParams := {
|
|
textDocument := {uri := uri},
|
|
position := pos,
|
|
sessionId := rpcSessionId.get!,
|
|
method := `Lean.Widget.getWidgets,
|
|
params := toJson pos,
|
|
}
|
|
Ipc.writeRequest ⟨requestNo, "$/lean/rpc/call", ps⟩
|
|
let response ← Ipc.readResponseAs requestNo Lean.Widget.GetWidgetsResponse
|
|
requestNo := requestNo + 1
|
|
IO.eprintln response.result.debugJson
|
|
for w in response.result.widgets do
|
|
let params : Lean.Widget.GetWidgetSourceParams := { pos, hash := w.javascriptHash }
|
|
let ps : RpcCallParams := {
|
|
ps with
|
|
method := `Lean.Widget.getWidgetSource,
|
|
params := toJson params,
|
|
}
|
|
Ipc.writeRequest ⟨requestNo, "$/lean/rpc/call", ps⟩
|
|
let resp ← Ipc.readResponseAs requestNo Lean.Widget.WidgetSource
|
|
IO.eprintln (toJson resp.result)
|
|
requestNo := requestNo + 1
|
|
| _ =>
|
|
let Except.ok params ← pure <| Json.parse params
|
|
| throw <| IO.userError s!"failed to parse {params}"
|
|
let params := params.setObjVal! "textDocument" (toJson { uri := uri : TextDocumentIdentifier })
|
|
-- TODO: correctly compute in presence of Unicode
|
|
let params := params.setObjVal! "position" (toJson pos)
|
|
IO.eprintln params
|
|
Ipc.writeRequest ⟨requestNo, method, params⟩
|
|
let rec readFirstResponse := do
|
|
match ← Ipc.readMessage with
|
|
| Message.response id r =>
|
|
assert! id == requestNo
|
|
return r
|
|
| Message.notification .. => readFirstResponse
|
|
| Message.request .. => readFirstResponse
|
|
| msg => throw <| IO.userError s!"unexpected message {toJson msg}"
|
|
let resp ← readFirstResponse
|
|
IO.eprintln resp
|
|
requestNo := requestNo + 1
|
|
| _ =>
|
|
lastActualLineNo := lineNo
|
|
lineNo := lineNo + 1
|
|
|
|
let _ ← Ipc.collectDiagnostics requestNo uri (versionNo - 1)
|
|
Ipc.writeNotification ⟨"textDocument/didClose", {
|
|
textDocument := { uri } : DidCloseTextDocumentParams }⟩
|
|
|
|
Ipc.shutdown requestNo
|
|
discard <| Ipc.waitForExit
|