Without this, it would not easy but perhaps be feasible to break incrementality when editing command prefixes such as `set_option ... in theorem` or also `theorem namesp.name ...` (which is a macro), especially if at some later point we support incrementality in input shifted by an edit. Explicit, sound support for these common cases will be brought back soon.
248 lines
10 KiB
Text
248 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?),
|
|
])
|
|
)
|
|
]
|
|
|
|
def word : Parsec String :=
|
|
Parsec.many1Chars <| Parsec.digit <|> Parsec.asciiLetter <|> Parsec.pchar '_'
|
|
|
|
def ident : Parsec Name := do
|
|
let head ← word
|
|
let xs ← Parsec.many1 (Parsec.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
|