diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 1d7c9e81..0494a8ba 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -40,6 +40,8 @@ jobs:
ocaml: 5.0.x
- os: ubuntu-latest
ocaml: 5.1.x
+ - os: ubuntu-latest
+ ocaml: 5.2.x
- os: macos-latest
ocaml: 4.14.x
- name: Windows Latest
@@ -79,6 +81,39 @@ jobs:
- name: 🐛 Test fcc
run: opam exec -- make test-compiler
+ build-opam:
+ name: Opam dev install
+ strategy:
+ fail-fast: false
+ runs-on: ubuntu-latest
+ steps:
+ - name: 🔭 Checkout code
+ uses: actions/checkout@v3
+ with:
+ submodules: recursive
+
+ - name: 🐫 Setup OCaml
+ uses: ocaml/setup-ocaml@v2
+ with:
+ ocaml-compiler: 4.14.x
+ dune-cache: true
+
+ - name: Install Coq and SerAPI into OPAM switch
+ run: |
+ opam install lwt logs # Also build pet-server
+ opam install memprof-limits # We need to do this to avoid coq-lsp rebuilding Coq below due to deptops
+ opam install vendor/coq/{coq-core,coq-stdlib,coqide-server,coq}.opam
+ opam install vendor/coq-serapi/coq-serapi.opam
+
+ - name: Install `coq-lsp` into OPAM switch
+ run: opam install .
+
+ - name: Test `coq-lsp` in installed switch
+ run: opam exec -- fcc examples/Demo.v
+
+ - name: Test `pet-server` is built
+ run: opam exec -- which pet-server
+
client-compile:
runs-on: ubuntu-latest
defaults:
diff --git a/.ocamlformat b/.ocamlformat
index 16289641..b2257f5a 100644
--- a/.ocamlformat
+++ b/.ocamlformat
@@ -1,4 +1,4 @@
-version=0.26.0
+version=0.26.1
profile=conventional
ocaml-version=4.08.0
break-separators=before
diff --git a/CHANGES.md b/CHANGES.md
index dac06c04..9bed0d3f 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,5 +1,5 @@
-# unreleased
-------------
+# coq-lsp 0.1.9: Hasta el 40 de Mayo...
+---------------------------------------
- new option `show_loc_info_on_hover` that will display parsing debug
information on hover; previous flag was fixed in code, which is way
@@ -65,11 +65,19 @@
#348)
- fix Coq performance view display (@ejgallego, #663, regression in
#513)
+ - Added new heatmap feature allowing timing data to be seen in the
+ editor. Can be enabled with the `Coq LSP: Toggle heatmap`
+ command. Can be configured to show memory usage. Colors and
+ granularity are configurable. (@Alizter and @ejgallego, #686,
+ grants #681).
- allow more than one input position in `selectionRange` LSP call
(@ejgallego, #667, fixes #663)
- new VSCode commands to allow to move one sentence backwards /
forward, this is particularly useful when combined with lazy
checking mode (@ejgallego, #671, fixes #263, fixes #580)
+ - VSCode commands `coq-lsp.sentenceNext` / `coq-lsp.sentencePrevious`
+ are now bound by default to `Alt + N` / `Alt + P` keybindings
+ (@ejgallego, #718)
- change diagnostic `extra` field to `data`, so we now conform to the
LSP spec, include the data only when the `send_diags_extra_data`
server-side option is enabled (@ejgallego, #670)
@@ -98,6 +106,81 @@
- Update `package-lock.json` for latest bugfixes (@ejgallego, #687)
- Update Nix flake enviroment (@Alizter, #684 #688)
- Update `prettier` (@Alizter @ejgallego, #684 #688)
+ - Store original performance data in the cache, so we now display the
+ original timing and memory data even for cached commands (@ejgallego, #693)
+ - Fix type errors in the Performance Data Notifications (@ejgallego,
+ @Alizter, #689, #693)
+ - Send performance performance data for the full document
+ (@ejgallego, @Alizter, #689, #693)
+ - Better types `coq/perfData` call (@ejgallego @Alizter, #689)
+ - New server option to enable / disable `coq/perfData` (@ejgallego, #689)
+ - New client option to enable / disable `coq/perfData` (@ejgallego, #717)
+ - The `coq-lsp.document` VSCode command will now display the returned
+ JSON data in a new editor (@ejgallego, #701)
+ - Update server settings on the fly when tweaking them in VSCode.
+ Implement `workspace/didChangeConfiguration` (@ejgallego, #702)
+ - [Coq API] Add functions to retrieve list of declarations done in
+ .vo files (@ejallego, @eytans, #704)
+ - New `petanque` API to interact directly with Coq's proof
+ engine. (@ejgallego, @gbdrt, Laetitia Teodorescu #703, thanks to
+ Alex Sanchez-Stern for many insightful feedback and testing)
+ - New `petanque` JSON-RPC `pet.exe`, which can be used à la SerAPI
+ to perform proof search and more (@ejgallego, @gbdrt, #705)
+ - New `pet-server.exe` TCP server for keep-alive sessions (@gbdrt,
+ #697)
+ - Always dispose UI elements. This should improve some strange
+ behaviors on extension restart (@ejgallego, #708)
+ - [code] Added new heatmap feature allowing timing data to be seen in
+ the editor. Can be enabled with the `Coq LSP: Toggle heatmap`
+ comamnd. Can be configured to show memory usage. Colors and
+ granularity are configurable. (@Alizter and @ejgallego, #686,
+ grants #681).
+ - [server] Support Coq meta-commands (Reset, Reset Initial, Back)
+ They are actually pretty useful to hint the incremental engine to
+ ignore changes in some part of the document (@ejgallego, #709)
+ - JSON-RPC library now supports all kind of incoming messages
+ (@ejgallego, #713)
+ - [code/server] New `coq/viewRange` notification, from client to
+ server, than hints the scheduler for the visible area of the
+ document; combined with the new lazy checking mode, this provides
+ checking on scroll, a feature inspired from Isabelle IDE
+ (@ejgallego, #717)
+ - [code] Have VSCode wait for full LSP client shutdown on server
+ restart. This fixes some bugs on extension restart (finally!)
+ (@ejgallego, #719)
+ - [code] Center the view if cursor goes out of scope in
+ `sentenceNext/sentencePrevious` (@ejgallego, #718)
+ - Switch Flèche range encoding to protocol native, this means UTF-16
+ code points for now (Léo Stefanesco, @ejgallego, #624, fixes #620,
+ #621)
+ - Give `Goals` panel focus back if it has lost it (in case of
+ multiple panels in the second viewColumn of Vscode) whenever
+ user navigates proofs (@Alidra @ejgallego, #722, #725)
+ - `fcc`: new option `--diags_level` to control whether Coq's notice
+ and info messages appear as diagnostics
+ - [code] Display the continous/on-request checking mode in the status bar,
+ allow to change it by clicking on it (@ejgallego, #721)
+ - Add an example of multiple workspaces (@ejgallego, @Blaisorblade,
+ #611)
+ - Don't show types of un-expanded goals. We should add an option for
+ this, but we don't have the cycles (@ejgallego, #730, workarounds
+ #525 #652)
+ - Support for `.lv / .v.tex` TeX files with embedded Coq code
+ (@ejgallego, #727)
+ - Don't expand bullet goals at previous levels by default
+ (@ejgallego, @Alizter, #731 cc #525)
+ - [petanque] Return basic goal information after `run_tac`, so we
+ avoid a `goals` round-trip for each tactic (@gbdrt, @ejgallego,
+ #733)
+ - [coq] Add support for reading glob files metadata (@ejgallego,
+ #735)
+ - [petanque] Return extra premise information: file name, position,
+ raw_text, using the above support for reading .glob files
+ (@ejgallego, #735)
+ - [code] Display server status using the `LanguageStatusItem`
+ facility, for now we display version and checking status
+ information (moved from #721), and we also allow to toggle the
+ checking mode from there (@ejgallego, #728)
# coq-lsp 0.1.8.1: Spring fix
-----------------------------
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 96a07cdb..b4abacf5 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -3,17 +3,23 @@ Contributing to Coq LSP
Thank you very much for willing to contribute to coq-lsp!
-`coq-lsp` has two components:
+The `coq-lsp` repository contains several tightly coupled components
+in a single repository, also known as a monorepo, in particular:
-- a LSP server for Coq project written in OCaml.
-- a `coq-lsp` VS Code extension written in TypeScript and React, in
- the `editor/code` directory.
+- Flèche: an incremental document engine for Coq supporting literate
+ programming and programability, written in OCaml
+- `fcc`: an extensible command line compiler built on top of Flèche
+- `petanque`: direct access to Coq's proof engine
+- `coq-lsp`a LSP server for the Coq project, written in OCaml on top of Flèche
+- a `coq-lsp/VSCode` extension written in TypeScript and React, in
+ the `editor/code` directory
-Read coq-lsp [FAQ](etc/FAQ.md) for an explanation on what the above mean.
+Read coq-lsp [FAQ](etc/FAQ.md) to learn more about LSP and
+server/client roles.
-It is possible to hack only in the server, on the client, or on both at the same
-time. We have thus structured this guide in 3 sections: general guidelines,
-server, and VS Code client.
+It is possible to hack only in the server, on the client, or on both
+at the same time. We have thus structured this guide in 3 sections:
+general guidelines, server, and VS Code client.
## General guidelines
@@ -184,14 +190,17 @@ coq-lsp.packages.${system}.default
The `coq-lsp` server consists of several components, we present them bottom-up
+- `vendor/coq`: [vendored] Coq version to build coq-lsp against
- `vendor/coq-serapi`: [vendored] improved utility functions to handle Coq AST
- `coq`: Utility library / abstracted Coq API. This is the main entry point for
communication with Coq, and it reifies Coq calls as to present a purely
functional interface to Coq.
+- `lang`: base language definitions for Flèche
- `fleche`: incremental document processing backend. Exposes a generic API, but
closely modelled to match LSP
- `lsp`: small generic LSP utilities, at some point to be replaced by a generic
library
+- `petanque`: low-level access to Coq's API
- `controller`: LSP controller, a thin layer translating LSP transport layer to
`flèche` surface API, plus some custom event queues for Coq.
- `controller-js`: LSP controller for Javascript, used for `vscode.dev` and
@@ -209,6 +218,29 @@ Some tips:
[ocamlformat]: https://github.com/ocaml-ppx/ocamlformat
+### Unicode setup
+
+Flèche stores locations in the protocol-native format. This has the
+advantage that conversion is needed only at range creation point
+(where we usually have access to the document to perform the
+conversion).
+
+This way, we can send ranges to the client without conversion.
+
+Request that work on the raw `Contents.t` buffer must do the inverse
+conversion, but we handle this via a proper text API that is aware of
+the conversion.
+
+For now, the setup for Coq is:
+
+- protocol-level (and Flèche) encoding: UTF-16 (due to LSP)
+- `Contents.t`: UTF-8 (sent to Coq)
+
+It would be very easy to set this parameters at initialization time,
+ideal would be for LSP clients to catch up and allow UTF-8 encodings
+(so no conversion is needed, at least for Coq), but it seems that we
+will take a while to get to this point.
+
## Client guide (VS Code Extension)
The VS Code extension is setup as a standard `npm` Typescript + React package
diff --git a/README.md b/README.md
index 19ec0065..e9162961 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,8 @@ document checking, advanced error recovery, hybrid Coq/markdown document
support, multiple workspace support, positional goals and information panel,
performance data, extensible command-line compiler, plugin system, and more.
+See the [coq-lsp User Manual](./etc/doc/USER_MANUAL.md) for more information.
+
`coq-lsp` aims to provide a seamless, modern interactive theorem proving
experience, as well as to serve as a maintainable platform for research and UI
integration with other projects.
@@ -37,6 +39,7 @@ and web native usage, providing quite a few extra features from vanilla Coq.
- [🎁 Features](#-features)
- [⏩ Incremental Compilation and Continuous Document Checking](#-incremental-compilation-and-continuous-document-checking)
+ - [👁 On-demand, Follow The Viewport Document Checking](#-on-demand-follow-the-viewport-document-checking)
- [🧠 Smart, Cache-Aware Error Recovery](#-smart-cache-aware-error-recovery)
- [🥅 Whole-Document Goal Display](#-whole-document-goal-display)
- [🗒️ Markdown Support](#️-markdown-support)
@@ -58,6 +61,7 @@ and web native usage, providing quite a few extra features from vanilla Coq.
- [✅ Vim](#-vim)
- [🩱 Neovim](#-neovim)
- [🐍 Python](#-python)
+- [⇨ `coq-lsp` users and extensions](#-coq-lsp-users-and-extensions)
- [🗣️ Discussion Channel](#️-discussion-channel)
- [☎ Weekly Calls](#-weekly-calls)
- [❓FAQ](#faq)
@@ -87,6 +91,14 @@ restart your proof session where you left it at the last time.
Incremental support is undergoing refinement, if `coq-lsp` rechecks when it
should not, please file a bug!
+### 👁 On-demand, Follow The Viewport Document Checking
+
+`coq-lsp` does also support on-demand checking. Two modes are available: follow
+the cursor, or follow the viewport; the modes can be toggled using the Language
+Status Item in Code's bottom right corner:
+
+
+
### 🧠 Smart, Cache-Aware Error Recovery
`coq-lsp` won't stop checking on errors, but supports (and encourages) working
@@ -182,6 +194,12 @@ fully-fledged LSP client.
add your pre/post processing passes, for example to analyze or serialize parts
of Coq files.
+### 🪄 Advanced APIs for Coq Interaction
+
+Thanks to Flèche, we provide some APIs on top of it that allow advanced use
+cases with Coq. In particular, we provide direct, low-overhead access to Coq's
+proof engine using [petanque](./petanque).
+
### ♻️ Reusability, Standards, Modularity
The incremental document checking library of `coq-lsp` has been designed to be
@@ -301,6 +319,17 @@ guide](./CONTRIBUTING.md)
- Interact programmatically with Coq files by using the [Python `coq-lsp` client](https://github.com/sr-lab/coq-lsp-pyclient)
by Pedro Carrott and Nuno Saavedra.
+## ⇨ `coq-lsp` users and extensions
+
+The below projects are using `coq-lsp`, we recommend you try them!
+
+- [CoqPilot uses Large Language Models to generate multiple potential proofs and then uses coq-lsp to typecheck them](https://github.com/JetBrains-Research/coqpilot).
+- [jsCoq: use Coq from your browser](https://github.com/jscoq/jscoq)
+- [Pytanque: a Python library implementing RL Environments](https://github.com/LLM4Coq/pytanque)
+- [ViZX: A Visualizer for the ZX Calculus](https://github.com/inQWIRE/ViZX).
+- [The Waterproof vscode extension helps students learn how to write mathematical proofs](https://github.com/impermeable/waterproof-vscode).
+- [Yade: Support for the YADE diagram editor in VSCode](https://github.com/amblafont/vscode-yade-example).
+
## 🗣️ Discussion Channel
`coq-lsp` discussion channel it at [Coq's
@@ -326,7 +355,7 @@ recommend that if you are installing via opam, you use the following branches
that have some fixes backported:
- For 8.20: No known problems
-- For 8.19: No known problems
+- For 8.19: `opam pin add coq-core https://github.com/ejgallego/coq.git#v8.19+lsp`
- For 8.18: `opam pin add coq-core https://github.com/ejgallego/coq.git#v8.18+lsp`
- For 8.17: `opam pin add coq-core https://github.com/ejgallego/coq.git#v8.17+lsp`
- For 8.16: `opam pin add coq https://github.com/ejgallego/coq.git#v8.16+lsp`
diff --git a/compiler/args.ml b/compiler/args.ml
index ede34c06..b6de9e98 100644
--- a/compiler/args.ml
+++ b/compiler/args.ml
@@ -14,6 +14,8 @@ type t =
; plugins : string list (** Flèche plugins to load *)
; max_errors : int option
(** Maximum erros before aborting the compilation *)
+ ; coq_diags_level : int
+ (** Whether to include feedback messages in the diagnostics *)
}
let compute_default_plugins ~no_vo ~plugins =
diff --git a/compiler/compile.ml b/compiler/compile.ml
index 059b05ac..263817d0 100644
--- a/compiler/compile.ml
+++ b/compiler/compile.ml
@@ -22,7 +22,7 @@ let save_diags_file ~(doc : Fleche.Doc.t) =
let file = Lang.LUri.File.to_string_file doc.uri in
let file = Filename.remove_extension file ^ ".diags" in
let diags = Fleche.Doc.diags doc in
- Util.format_to_file ~file ~f:Output.pp_diags diags
+ Coq.Compat.format_to_file ~file ~f:Output.pp_diags diags
(** Return: exit status for file:
@@ -47,7 +47,7 @@ let compile_file ~cc file : int =
let workspace = workspace_of_uri ~io ~workspaces ~uri ~default in
let files = Coq.Files.make () in
let env = Doc.Env.make ~init:root_state ~workspace ~files in
- let raw = Util.input_all file in
+ let raw = Coq.Compat.Ocaml_414.In_channel.(with_open_bin file input_all) in
let () = Theory.create ~io ~token ~env ~uri ~raw ~version:1 in
match Theory.Check.maybe_check ~io ~token with
| None -> 102
diff --git a/compiler/driver.ml b/compiler/driver.ml
index e8d0d94f..20814cc6 100644
--- a/compiler/driver.ml
+++ b/compiler/driver.ml
@@ -36,18 +36,26 @@ let apply_config ~max_errors =
max_errors
let go ~int_backend args =
- let { Args.cmdline; roots; display; debug; files; plugins; max_errors } =
+ let { Args.cmdline
+ ; roots
+ ; display
+ ; debug
+ ; files
+ ; plugins
+ ; max_errors
+ ; coq_diags_level
+ } =
args
in
(* Initialize event callbacks, in testing don't do perfData *)
let perfData = Option.is_empty fcc_test in
- let io = Output.init display ~perfData in
+ let io = Output.init ~display ~perfData ~coq_diags_level in
(* Initialize Coq *)
let debug = debug || Fleche.Debug.backtraces || !Fleche.Config.v.debug in
let root_state = coq_init ~debug in
let roots = if List.length roots < 1 then [ Sys.getcwd () ] else roots in
let default = Coq.Workspace.default ~debug ~cmdline in
- let () = Coq.Limits.select int_backend in
+ let () = Coq.Limits.select_best int_backend in
let () = Coq.Limits.start () in
let token = Coq.Limits.Token.create () in
let workspaces =
diff --git a/compiler/fcc.ml b/compiler/fcc.ml
index 8dc8d41d..dd990ad3 100644
--- a/compiler/fcc.ml
+++ b/compiler/fcc.ml
@@ -3,7 +3,8 @@ open Cmdliner
open Fcc_lib
let fcc_main int_backend roots display debug plugins files coqlib coqcorelib
- ocamlpath rload_path load_path require_libraries no_vo max_errors =
+ ocamlpath rload_path load_path require_libraries no_vo max_errors
+ coq_diags_level =
let vo_load_path = rload_path @ load_path in
let ml_include_path = [] in
let args = [] in
@@ -19,16 +20,21 @@ let fcc_main int_backend roots display debug plugins files coqlib coqcorelib
in
let plugins = Args.compute_default_plugins ~no_vo ~plugins in
let args =
- Args.{ cmdline; roots; display; files; debug; plugins; max_errors }
+ Args.
+ { cmdline
+ ; roots
+ ; display
+ ; files
+ ; debug
+ ; plugins
+ ; max_errors
+ ; coq_diags_level
+ }
in
Driver.go ~int_backend args
(****************************************************************************)
(* Specific to fcc *)
-let roots : string list Term.t =
- let doc = "Workspace(s) root(s)" in
- Arg.(value & opt_all string [] & info [ "root" ] ~docv:"ROOTS" ~doc)
-
let display : Args.Display.t Term.t =
let doc = "Verbosity display settings" in
let dparse =
@@ -95,7 +101,7 @@ let fcc_cmd : int Cmd.t =
Term.(
const fcc_main $ int_backend $ roots $ display $ debug $ plugins $ file
$ coqlib $ coqcorelib $ ocamlpath $ rload_paths $ qload_paths $ ri_from
- $ no_vo $ max_errors)
+ $ no_vo $ max_errors $ coq_diags_level)
in
let exits = Exit_codes.[ fatal; stopped; scheduled; uri_failed ] in
Cmd.(v (Cmd.info "fcc" ~exits ~version ~doc ~man) fcc_term)
diff --git a/compiler/output.ml b/compiler/output.ml
index f9f13f4d..bbf66a2a 100644
--- a/compiler/output.ml
+++ b/compiler/output.ml
@@ -9,7 +9,6 @@ let pp_diags fmt dl =
(* We will use this when we set eager diagnotics to true *)
let diagnostics ~uri:_ ~version:_ _diags = ()
let fileProgress ~uri:_ ~version:_ _progress = ()
-let perfData ~uri:_ ~version:_ _perf = ()
(* We print trace and messages, and perfData summary *)
module Fcc_verbose = struct
@@ -24,26 +23,30 @@ module Fcc_verbose = struct
let perfData ~uri:_ ~version:_ { Fleche.Perf.summary; _ } =
Format.(eprintf "[perfdata]@\n@[%s@]@\n%!" summary)
+ let serverVersion _ = ()
+ let serverStatus _ = ()
+
let cb =
- Fleche.Io.CallBack.{ trace; message; diagnostics; fileProgress; perfData }
+ Fleche.Io.CallBack.
+ { trace
+ ; message
+ ; diagnostics
+ ; fileProgress
+ ; perfData
+ ; serverVersion
+ ; serverStatus
+ }
end
(* We print trace, messages *)
module Fcc_normal = struct
let trace _ ?extra:_ _ = ()
- let message = Fcc_verbose.message
- let perfData = Fcc_verbose.perfData
-
- let cb =
- Fleche.Io.CallBack.{ trace; message; diagnostics; fileProgress; perfData }
+ let cb = { Fcc_verbose.cb with trace }
end
module Fcc_quiet = struct
- let trace _ ?extra:_ _ = ()
let message ~lvl:_ ~message:_ = ()
-
- let cb =
- Fleche.Io.CallBack.{ trace; message; diagnostics; fileProgress; perfData }
+ let cb = { Fcc_normal.cb with message }
end
let set_callbacks (display : Args.Display.t) =
@@ -56,16 +59,18 @@ let set_callbacks (display : Args.Display.t) =
Fleche.Io.CallBack.set cb;
cb
-let set_config ~perfData =
+let set_config ~perfData ~coq_diags_level =
+ let show_coq_info_messages = coq_diags_level > 1 in
+ let show_notices_as_diagnostics = coq_diags_level > 0 in
Fleche.Config.(
v :=
{ !v with
send_perf_data = perfData
; eager_diagnostics = false
- ; show_coq_info_messages = true
- ; show_notices_as_diagnostics = true
+ ; show_coq_info_messages
+ ; show_notices_as_diagnostics
})
-let init display ~perfData =
- set_config ~perfData;
+let init ~display ~coq_diags_level ~perfData =
+ set_config ~perfData ~coq_diags_level;
set_callbacks display
diff --git a/compiler/output.mli b/compiler/output.mli
index 7e643bc0..a4699ef7 100644
--- a/compiler/output.mli
+++ b/compiler/output.mli
@@ -1,5 +1,9 @@
(** Initialize Console Output System *)
-val init : Args.Display.t -> perfData:bool -> Fleche.Io.CallBack.t
+val init :
+ display:Args.Display.t
+ -> coq_diags_level:int
+ -> perfData:bool
+ -> Fleche.Io.CallBack.t
(** Report progress on file compilation *)
(* val report : unit -> unit *)
diff --git a/compiler/util.mli b/compiler/util.mli
deleted file mode 100644
index b9eaaf94..00000000
--- a/compiler/util.mli
+++ /dev/null
@@ -1,4 +0,0 @@
-val input_all : string -> string
-
-val format_to_file :
- file:string -> f:(Format.formatter -> 'a -> unit) -> 'a -> unit
diff --git a/controller/coq_lsp.ml b/controller/coq_lsp.ml
index 2af1fbc9..25caf87d 100644
--- a/controller/coq_lsp.ml
+++ b/controller/coq_lsp.ml
@@ -49,35 +49,52 @@ let rec process_queue ~delay ~io ~ofn ~state : unit =
| Some (Cont state) -> process_queue ~delay ~io ~ofn ~state
let concise_cb ofn =
+ let send_notification nt =
+ Lsp.Base.Message.(Notification nt |> to_yojson) |> ofn
+ in
+ let diagnostics ~uri ~version diags =
+ if List.length diags > 0 then
+ Lsp.JLang.mk_diagnostics ~uri ~version diags |> send_notification
+ in
Fleche.Io.CallBack.
{ trace = (fun _hdr ?extra:_ _msg -> ())
; message = (fun ~lvl:_ ~message:_ -> ())
- ; diagnostics =
- (fun ~uri ~version diags ->
- if List.length diags > 0 then
- Lsp.JLang.mk_diagnostics ~uri ~version diags |> ofn)
+ ; diagnostics
; fileProgress = (fun ~uri:_ ~version:_ _progress -> ())
; perfData = (fun ~uri:_ ~version:_ _perf -> ())
+ ; serverVersion = (fun _ -> ())
+ ; serverStatus = (fun _ -> ())
}
(* Main loop *)
let lsp_cb ofn =
+ let send_notification nt =
+ Lsp.Base.Message.(Notification nt |> to_yojson) |> ofn
+ in
+ let trace = LIO.trace in
let message ~lvl ~message =
let lvl = Fleche.Io.Level.to_int lvl in
LIO.logMessageInt ~lvl ~message
in
+ let diagnostics ~uri ~version diags =
+ Lsp.JLang.mk_diagnostics ~uri ~version diags |> send_notification
+ in
+ let fileProgress ~uri ~version progress =
+ Lsp.JFleche.mk_progress ~uri ~version progress |> send_notification
+ in
+ let perfData ~uri ~version perf =
+ Lsp.JFleche.mk_perf ~uri ~version perf |> send_notification
+ in
+ let serverVersion vi = Lsp.JFleche.mk_serverVersion vi |> send_notification in
+ let serverStatus st = Lsp.JFleche.mk_serverStatus st |> send_notification in
Fleche.Io.CallBack.
- { trace = LIO.trace
+ { trace
; message
- ; diagnostics =
- (fun ~uri ~version diags ->
- Lsp.JLang.mk_diagnostics ~uri ~version diags |> ofn)
- ; fileProgress =
- (fun ~uri ~version progress ->
- Lsp.JFleche.mk_progress ~uri ~version progress |> ofn)
- ; perfData =
- (fun ~uri ~version perf ->
- Lsp.JFleche.mk_perf ~uri ~version perf |> ofn)
+ ; diagnostics
+ ; fileProgress
+ ; perfData
+ ; serverVersion
+ ; serverStatus
}
let coq_init ~debug =
@@ -91,15 +108,18 @@ let exit_notification =
let rec lsp_init_loop ~ifn ~ofn ~cmdline ~debug =
match ifn () with
| None -> raise Lsp_exit
- | Some msg -> (
+ | Some (Ok msg) -> (
match lsp_init_process ~ofn ~cmdline ~debug msg with
| Init_effect.Exit -> raise Lsp_exit
| Init_effect.Loop -> lsp_init_loop ~ifn ~ofn ~cmdline ~debug
| Init_effect.Success w -> w)
+ | Some (Error err) ->
+ Lsp.Io.trace "read_request" ("error: " ^ err);
+ lsp_init_loop ~ifn ~ofn ~cmdline ~debug
let lsp_main bt coqcorelib coqlib ocamlpath vo_load_path ml_include_path
require_libraries delay int_backend =
- Coq.Limits.select int_backend;
+ Coq.Limits.select_best int_backend;
Coq.Limits.start ();
(* Try to be sane w.r.t. \r\n in Windows *)
@@ -107,12 +127,19 @@ let lsp_main bt coqcorelib coqlib ocamlpath vo_load_path ml_include_path
Stdlib.set_binary_mode_out stdout true;
(* We output to stdout *)
- let ifn () = LIO.read_request stdin in
+ let ifn () = LIO.read_message stdin in
+
(* Set log channels *)
- let ofn = LIO.send_json Format.std_formatter in
- LIO.set_log_fn ofn;
+ let json_fn = LIO.send_json Format.std_formatter in
+
+ let ofn response =
+ let response = Lsp.Base.Message.to_yojson response in
+ LIO.send_json Format.std_formatter response
+ in
- let io = lsp_cb ofn in
+ LIO.set_log_fn json_fn;
+
+ let io = lsp_cb json_fn in
Fleche.Io.CallBack.set io;
(* IMPORTANT: LSP spec forbids any message from server to client before
@@ -138,9 +165,12 @@ let lsp_main bt coqcorelib coqlib ocamlpath vo_load_path ml_include_path
| None ->
(* EOF, push an exit notication to the queue *)
enqueue_message exit_notification
- | Some msg ->
+ | Some (Ok msg) ->
enqueue_message msg;
read_loop ()
+ | Some (Error err) ->
+ Lsp.Io.trace "read_request" ("error: " ^ err);
+ read_loop ()
in
(* Input/output will happen now *)
@@ -152,7 +182,7 @@ let lsp_main bt coqcorelib coqlib ocamlpath vo_load_path ml_include_path
Fleche.Config.(
v := { !v with send_diags = false; send_perf_data = false });
LIO.set_log_fn (fun _obj -> ());
- let io = concise_cb ofn in
+ let io = concise_cb json_fn in
Fleche.Io.CallBack.set io;
io)
else io
diff --git a/controller/lsp_core.ml b/controller/lsp_core.ml
index c9c36765..abc0c48c 100644
--- a/controller/lsp_core.ml
+++ b/controller/lsp_core.ml
@@ -164,23 +164,28 @@ module Rq : sig
end
val serve :
- ofn:(J.t -> unit) -> token:Coq.Limits.Token.t -> id:int -> Action.t -> unit
+ ofn_rq:(LSP.Response.t -> unit)
+ -> token:Coq.Limits.Token.t
+ -> id:int
+ -> Action.t
+ -> unit
- val cancel : ofn:(J.t -> unit) -> code:int -> message:string -> int -> unit
+ val cancel :
+ ofn_rq:(LSP.Response.t -> unit) -> code:int -> message:string -> int -> unit
val serve_postponed :
- ofn:(J.t -> unit)
+ ofn_rq:(LSP.Response.t -> unit)
-> token:Coq.Limits.Token.t
-> doc:Fleche.Doc.t
-> Int.Set.t
-> unit
end = struct
(* Answer a request, private *)
- let answer ~ofn ~id result =
+ let answer ~ofn_rq ~id result =
(match result with
- | Result.Ok result -> LSP.mk_reply ~id ~result
- | Error (code, message) -> LSP.mk_request_error ~id ~code ~message)
- |> ofn
+ | Result.Ok result -> LSP.Response.mk_ok ~id ~result
+ | Error (code, message) -> LSP.Response.mk_error ~id ~code ~message)
+ |> ofn_rq
(* private to the Rq module, just used not to retrigger canceled requests *)
let _rtable : (int, Request.Data.t) Hashtbl.t = Hashtbl.create 673
@@ -191,16 +196,16 @@ end = struct
Hashtbl.add _rtable id pr
(* Consumes a request, if alive, it answers mandatorily *)
- let consume_ ~ofn ~f id =
+ let consume_ ~ofn_rq ~f id =
match Hashtbl.find_opt _rtable id with
| Some pr ->
Hashtbl.remove _rtable id;
- f pr |> answer ~ofn ~id
+ f pr |> answer ~ofn_rq ~id
| None ->
LIO.trace "can't consume cancelled request: " (string_of_int id);
()
- let cancel ~ofn ~code ~message id : unit =
+ let cancel ~ofn_rq ~code ~message id : unit =
(* fail the request, do cleanup first *)
let f pr =
let () =
@@ -209,30 +214,30 @@ end = struct
in
Error (code, message)
in
- consume_ ~ofn ~f id
+ consume_ ~ofn_rq ~f id
let debug_serve id pr =
if Fleche.Debug.request_delay then
LIO.trace "serving"
(Format.asprintf "rq: %d | %a" id Request.Data.data pr)
- let serve_postponed ~ofn ~token ~doc id =
+ let serve_postponed ~ofn_rq ~token ~doc id =
let f pr =
debug_serve id pr;
Request.Data.serve ~token ~doc pr
in
- consume_ ~ofn ~f id
+ consume_ ~ofn_rq ~f id
- let query ~ofn ~token ~id (pr : Request.Data.t) =
+ let query ~ofn_rq ~token ~id (pr : Request.Data.t) =
let uri, postpone, request = Request.Data.dm_request pr in
match Fleche.Theory.Request.add { id; uri; postpone; request } with
| Cancel ->
let code = -32802 in
let message = "Document is not ready" in
- Error (code, message) |> answer ~ofn ~id
+ Error (code, message) |> answer ~ofn_rq ~id
| Now doc ->
debug_serve id pr;
- Request.Data.serve ~token ~doc pr |> answer ~ofn ~id
+ Request.Data.serve ~token ~doc pr |> answer ~ofn_rq ~id
| Postpone -> postpone_ ~id pr
module Action = struct
@@ -244,13 +249,13 @@ end = struct
let error (code, msg) = now (Error (code, msg))
end
- let serve ~ofn ~token ~id action =
+ let serve ~ofn_rq ~token ~id action =
match action with
- | Action.Immediate r -> answer ~ofn ~id r
- | Action.Data p -> query ~ofn ~token ~id p
+ | Action.Immediate r -> answer ~ofn_rq ~id r
+ | Action.Data p -> query ~ofn_rq ~token ~id p
- let serve_postponed ~ofn ~token ~doc rl =
- Int.Set.iter (serve_postponed ~ofn ~token ~doc) rl
+ let serve_postponed ~ofn_rq ~token ~doc rl =
+ Int.Set.iter (serve_postponed ~ofn_rq ~token ~doc) rl
end
(***********************************************************************)
@@ -267,7 +272,7 @@ let do_open ~io ~token ~(state : State.t) params =
let env = Fleche.Doc.Env.make ~init ~workspace ~files in
Fleche.Theory.create ~io ~token ~env ~uri ~raw:text ~version
-let do_change ~ofn ~io ~token params =
+let do_change ~ofn_rq ~io ~token params =
let uri, version = Helpers.get_uri_version params in
let changes = List.map U.to_assoc @@ list_field "contentChanges" params in
match changes with
@@ -283,7 +288,7 @@ let do_change ~ofn ~io ~token params =
let invalid_rq = Fleche.Theory.change ~io ~token ~uri ~version ~raw in
let code = -32802 in
let message = "Request got old in server" in
- Int.Set.iter (Rq.cancel ~ofn ~code ~message) invalid_rq
+ Int.Set.iter (Rq.cancel ~ofn_rq ~code ~message) invalid_rq
let do_close ~ofn:_ params =
let uri = Helpers.get_uri params in
@@ -398,14 +403,32 @@ let do_document = do_document_request_maybe ~handler:Rq_document.request
let do_save_vo = do_document_request_maybe ~handler:Rq_save.request
let do_lens = do_document_request_maybe ~handler:Rq_lens.request
-let do_cancel ~ofn ~params =
+let do_cancel ~ofn_rq ~params =
let id = int_field "id" params in
let code = -32800 in
let message = "Cancelled by client" in
- Rq.cancel ~ofn ~code ~message id
+ Rq.cancel ~ofn_rq ~code ~message id
let do_cache_trim () = Nt_cache_trim.notification ()
+let do_viewRange params =
+ match List.assoc "range" params |> Lsp.JLang.Diagnostic.Range.of_yojson with
+ | Ok range ->
+ let { Lsp.JLang.Diagnostic.Range.end_ = { line; character }; _ } = range in
+ let message = Format.asprintf "l: %d c:%d" line character in
+ LIO.trace "viewRange" message;
+ let uri = Helpers.get_uri params in
+ Fleche.Theory.Check.set_scheduler_hint ~uri ~point:(line, character);
+ ()
+ | Error err -> LIO.trace "viewRange" ("error in parsing notification: " ^ err)
+
+let do_changeConfiguration params =
+ let message = "didChangeReceived" in
+ let () = LIO.(logMessage ~lvl:Lvl.Info ~message) in
+ let settings = field "settings" params |> U.to_assoc in
+ Rq_init.do_settings settings;
+ ()
+
(***********************************************************************)
(** LSP Init routine *)
@@ -433,6 +456,7 @@ module Init_effect = struct
end
let lsp_init_process ~ofn ~cmdline ~debug msg : Init_effect.t =
+ let ofn_rq r = Lsp.Base.Message.response r |> ofn in
match msg with
| LSP.Message.Request { method_ = "initialize"; id; params } ->
(* At this point logging is allowed per LSP spec *)
@@ -440,11 +464,21 @@ let lsp_init_process ~ofn ~cmdline ~debug msg : Init_effect.t =
Format.asprintf "Initializing coq-lsp server %s" (version ())
in
LIO.logMessage ~lvl:Info ~message;
- let result, dirs = Rq_init.do_initialize ~params in
- (* We don't need to interrupt this *)
let token = Coq.Limits.Token.create () in
- Rq.Action.now (Ok result) |> Rq.serve ~ofn ~token ~id;
- LIO.logMessage ~lvl:Info ~message:"Server initialized";
+ let result, dirs = Rq_init.do_initialize ~params in
+ Rq.Action.now (Ok result) |> Rq.serve ~ofn_rq ~token ~id;
+ let vi =
+ let coq = Coq_config.version in
+ let ocaml = Sys.ocaml_version in
+ let coq_lsp = Fleche.Version.server in
+ Fleche.ServerInfo.Version.{ coq; ocaml; coq_lsp }
+ in
+ Lsp.JFleche.mk_serverVersion vi |> Lsp.Base.Message.notification |> ofn;
+ let message =
+ Format.asprintf "Server initializing (int_backend: %s)"
+ (Coq.Limits.name ())
+ in
+ LIO.logMessage ~lvl:Info ~message;
(* Workspace initialization *)
let debug = debug || !Fleche.Config.v.debug in
let workspaces =
@@ -456,30 +490,36 @@ let lsp_init_process ~ofn ~cmdline ~debug msg : Init_effect.t =
Success workspaces
| LSP.Message.Request { id; _ } ->
(* per spec *)
- LSP.mk_request_error ~id ~code:(-32002) ~message:"server not initialized"
- |> ofn;
+ LSP.Response.mk_error ~id ~code:(-32002) ~message:"server not initialized"
+ |> ofn_rq;
Loop
| LSP.Message.Notification { method_ = "exit"; params = _ } -> Exit
| LSP.Message.Notification _ ->
(* We can't log before getting the initialize message *)
Loop
+ | LSP.Message.Response _ ->
+ (* O_O *)
+ Loop
(** Dispatching *)
let dispatch_notification ~io ~ofn ~token ~state ~method_ ~params : unit =
+ let ofn_rq r = Lsp.Base.Message.response r |> ofn in
match method_ with
(* Lifecycle *)
| "exit" -> raise Lsp_exit
- (* setTrace *)
+ (* setTrace and settings *)
| "$/setTrace" -> do_trace params
+ | "workspace/didChangeConfiguration" -> do_changeConfiguration params
(* Document lifetime *)
| "textDocument/didOpen" -> do_open ~io ~token ~state params
- | "textDocument/didChange" -> do_change ~io ~ofn ~token params
+ | "textDocument/didChange" -> do_change ~io ~ofn_rq ~token params
| "textDocument/didClose" -> do_close ~ofn params
| "textDocument/didSave" -> Cache.save_to_disk ()
(* Specific to coq-lsp *)
+ | "coq/viewRange" -> do_viewRange params
| "coq/trimCaches" -> do_cache_trim ()
(* Cancel Request *)
- | "$/cancelRequest" -> do_cancel ~ofn ~params
+ | "$/cancelRequest" -> do_cancel ~ofn_rq ~params
(* NOOPs *)
| "initialized" -> ()
(* Generic handler *)
@@ -521,15 +561,22 @@ let dispatch_request ~method_ ~params : Rq.Action.t =
LIO.trace "no_handler" msg;
Rq.Action.error (-32601, "method not found")
-let dispatch_request ~ofn ~token ~id ~method_ ~params =
- dispatch_request ~method_ ~params |> Rq.serve ~ofn ~token ~id
+let dispatch_request ~ofn_rq ~token ~id ~method_ ~params =
+ dispatch_request ~method_ ~params |> Rq.serve ~ofn_rq ~token ~id
let dispatch_message ~io ~ofn ~token ~state (com : LSP.Message.t) : State.t =
+ let ofn_rq r = Lsp.Base.Message.response r |> ofn in
match com with
| Notification { method_; params } ->
+ LIO.trace "process_queue" ("Serving notification: " ^ method_);
dispatch_state_notification ~io ~ofn ~token ~state ~method_ ~params
| Request { id; method_; params } ->
- dispatch_request ~ofn ~token ~id ~method_ ~params;
+ LIO.trace "process_queue" ("Serving Request: " ^ method_);
+ dispatch_request ~ofn_rq ~token ~id ~method_ ~params;
+ state
+ | Response r ->
+ LIO.trace "process_queue"
+ ("Serving response for: " ^ string_of_int (Lsp.Base.Response.id r));
state
(* Queue handling *)
@@ -560,10 +607,11 @@ type 'a cont =
| Yield of 'a
let check_or_yield ~io ~ofn ~token ~state =
+ let ofn_rq r = Lsp.Base.Message.response r |> ofn in
match Fleche.Theory.Check.maybe_check ~io ~token with
| None -> Yield state
| Some (ready, doc) ->
- let () = Rq.serve_postponed ~ofn ~token ~doc ready in
+ let () = Rq.serve_postponed ~ofn_rq ~token ~doc ready in
Cont state
module LspQueue : sig
@@ -603,7 +651,6 @@ let dispatch_or_resume_check ~io ~ofn ~state =
let token = token_factory () in
check_or_yield ~io ~ofn ~token ~state
| Some com ->
- LIO.trace "process_queue" ("Serving Request: " ^ LSP.Message.method_ com);
(* We let Coq work normally now *)
let token = token_factory () in
Cont (dispatch_message ~io ~ofn ~token ~state com)
diff --git a/controller/lsp_core.mli b/controller/lsp_core.mli
index 14bb111f..8581e189 100644
--- a/controller/lsp_core.mli
+++ b/controller/lsp_core.mli
@@ -41,7 +41,7 @@ module Init_effect : sig
end
val lsp_init_process :
- ofn:(Yojson.Safe.t -> unit)
+ ofn:(Lsp.Base.Message.t -> unit)
-> cmdline:Coq.Workspace.CmdLine.t
-> debug:bool
-> Lsp.Base.Message.t
@@ -56,7 +56,7 @@ type 'a cont =
wake up pending requests *)
val dispatch_or_resume_check :
io:Fleche.Io.CallBack.t
- -> ofn:(Yojson.Safe.t -> unit)
+ -> ofn:(Lsp.Base.Message.t -> unit)
-> state:State.t
-> State.t cont option
diff --git a/controller/rq_common.ml b/controller/rq_common.ml
index d1a25be0..2a1627af 100644
--- a/controller/rq_common.ml
+++ b/controller/rq_common.ml
@@ -44,9 +44,33 @@ let get_id_at_point ~contents ~point =
let { Fleche.Contents.lines; _ } = contents in
if line <= Array.length lines then
let line = Array.get lines line in
- (* XXX UTF this will fail on unicode chars that differ among UTF-8/16 (cc
- #531) *)
- match Coq.Utf8.index_of_char ~line ~char:character with
- | None -> None
- | Some character -> find_id line character
+ let character =
+ Lang.Utf.utf8_offset_of_utf16_offset ~line ~offset:character
+ in
+ find_id line character
else None
+
+let validate_line ~(contents : Fleche.Contents.t) ~line =
+ if Array.length contents.lines > line then
+ Some (Array.get contents.lines line)
+ else None
+
+let validate_column char line =
+ let length = Lang.Utf.length_utf16 line in
+ if char < length then
+ let char = Lang.Utf.utf8_offset_of_utf16_offset ~line ~offset:char in
+ Some (String.get line char)
+ else None
+
+(* This returns a byte-based char offset for the line *)
+let validate_position ~contents ~point =
+ let line, char = point in
+ validate_line ~contents ~line |> fun l -> Option.bind l (validate_column char)
+
+let get_char_at_point ~contents ~point =
+ let line, char = point in
+ if char >= 1 then
+ let point = (line, char - 1) in
+ validate_position ~contents ~point
+ else (* Can't get previous char *)
+ None
diff --git a/controller/rq_common.mli b/controller/rq_common.mli
index 80ab0dde..a7f7bf68 100644
--- a/controller/rq_common.mli
+++ b/controller/rq_common.mli
@@ -5,5 +5,11 @@
(* Written by: Emilio J. Gallego Arias *)
(************************************************************************)
+(* Contents utils, should be moved to Contents.t , they mainly handle character
+ enconding conversiong between protocol and prover positions, if needed *)
+
val get_id_at_point :
contents:Fleche.Contents.t -> point:int * int -> string option
+
+val get_char_at_point :
+ contents:Fleche.Contents.t -> point:int * int -> char option
diff --git a/controller/rq_completion.ml b/controller/rq_completion.ml
index dd7e1243..62a8519e 100644
--- a/controller/rq_completion.ml
+++ b/controller/rq_completion.ml
@@ -70,31 +70,11 @@ let unicode_list point : Yojson.Safe.t list =
(* Coq's CList.map is tail-recursive *)
CList.map (mk_unicode_completion_item point) ulist
-let validate_line ~(doc : Fleche.Doc.t) ~line =
- if Array.length doc.contents.lines > line then
- Some (Array.get doc.contents.lines line)
- else None
-
-(* This returns a byte-based char offset for the line *)
-let validate_position ~doc ~point =
- let line, char = point in
- Option.bind (validate_line ~doc ~line) (fun line ->
- let char = Coq.Utf8.get_byte_offset_from_utf16_pos line char in
- Option.bind char (fun index -> Some (String.get line index)))
-
-let get_char_at_point ~(doc : Fleche.Doc.t) ~point =
- let line, char = point in
- if char >= 1 then
- let point = (line, char - 1) in
- validate_position ~doc ~point
- else (* Can't get previous char *)
- None
-
-(* point is a utf char! *)
-let completion ~token:_ ~doc ~point =
+let completion ~token:_ ~(doc : Fleche.Doc.t) ~point =
(* Instead of get_char_at_point we should have a CompletionContext.t, to be
addressed in further completion PRs *)
- (match get_char_at_point ~doc ~point with
+ let contents = doc.contents in
+ (match Rq_common.get_char_at_point ~contents ~point with
| None ->
let incomplete = true in
let items = [] in
diff --git a/controller/rq_definition.ml b/controller/rq_definition.ml
index 8b29e668..fe7d28cc 100644
--- a/controller/rq_definition.ml
+++ b/controller/rq_definition.ml
@@ -11,8 +11,9 @@ let request ~token:_ ~(doc : Fleche.Doc.t) ~point =
(fun id_at_point ->
let { Fleche.Doc.toc; _ } = doc in
match CString.Map.find_opt id_at_point toc with
- | Some range ->
+ | Some node ->
let uri = doc.uri in
+ let range = node.range in
Lsp.Core.Location.({ uri; range } |> to_yojson)
| None -> `Null)
`Null
diff --git a/controller/rq_hover.ml b/controller/rq_hover.ml
index 174a89fd..28b4fd9a 100644
--- a/controller/rq_hover.ml
+++ b/controller/rq_hover.ml
@@ -238,7 +238,7 @@ end
(* Register in-file hover plugins *)
let () = List.iter Register.add [ Loc_info.h; Stats.h; Type.h; Notation.h ]
-let hover ~token ~doc ~point =
+let hover ~token ~(doc : Fleche.Doc.t) ~point =
let node = Info.LC.node ~doc ~point Exact in
let range = Option.map Doc.Node.range node in
let hovers = Register.fire ~token ~doc ~point ~node in
diff --git a/controller/rq_init.ml b/controller/rq_init.ml
index d0f5f606..d99a4db6 100644
--- a/controller/rq_init.ml
+++ b/controller/rq_init.ml
@@ -24,9 +24,9 @@ let odict_field name dict =
[]
(* Request Handling: The client expects a reply *)
-let do_client_options coq_lsp_options : unit =
- LIO.trace "init" "custom client options:";
- LIO.trace_object "init" (`Assoc coq_lsp_options);
+let do_settings coq_lsp_options : unit =
+ LIO.trace "settings" "setting server options:";
+ LIO.trace_object "settings" (`Assoc coq_lsp_options);
match Lsp.JFleche.Config.of_yojson (`Assoc coq_lsp_options) with
| Ok v -> Fleche.Config.v := v
| Error msg -> LIO.trace "CoqLspOption.of_yojson error: " msg
@@ -109,8 +109,8 @@ let do_initialize ~params =
let dir = determine_workspace_root ~params in
let trace = get_trace ~params in
LIO.set_trace_value trace;
- let coq_lsp_options = odict_field "initializationOptions" params in
- do_client_options coq_lsp_options;
+ let coq_lsp_settings = odict_field "initializationOptions" params in
+ do_settings coq_lsp_settings;
check_client_version !Fleche.Config.v.client_version;
let client_capabilities = odict_field "capabilities" params in
if Fleche.Debug.lsp_init then (
diff --git a/controller/rq_init.mli b/controller/rq_init.mli
index 39909178..c4a424c5 100644
--- a/controller/rq_init.mli
+++ b/controller/rq_init.mli
@@ -5,6 +5,9 @@
(* Written by: Emilio J. Gallego Arias *)
(************************************************************************)
+(** Setups the server configuration, takes the list of settings as a JSON dict *)
+val do_settings : (string * Yojson.Safe.t) list -> unit
+
(** Returns answer request + workspace root directory *)
val do_initialize :
params:(string * Yojson.Safe.t) list -> Yojson.Safe.t * string list
diff --git a/coq-lsp.opam b/coq-lsp.opam
index bd34db8c..80b6ea0f 100644
--- a/coq-lsp.opam
+++ b/coq-lsp.opam
@@ -40,6 +40,8 @@ depends: [
"coq-serapi" { >= "8.17.0+0.17.2" < "8.18" }
]
+depopts: ["lwt" "logs"]
+
build: [
[ "rm" "-rf" "vendor" ]
[ "dune" "build" "-p" name "-j" jobs ]
diff --git a/coq/args.ml b/coq/args.ml
index 2eec75ac..d9d5a56a 100644
--- a/coq/args.ml
+++ b/coq/args.ml
@@ -84,8 +84,22 @@ let ri_from : (string option * string) list Term.t =
& info [ "rifrom"; "require-import-from" ] ~docv:"FROM,LIBRARY" ~doc))
let int_backend =
- let doc = "Select Interruption Backend" in
+ let doc =
+ "Select Interruption Backend, if absent, the best available for your OCaml \
+ version will be selected"
+ in
Arg.(
value
- & opt (enum [ ("Coq", Limits.Coq); ("Mp", Limits.Mp) ]) Limits.Coq
+ & opt (enum [ ("Coq", Some Limits.Coq); ("Mp", Some Limits.Mp) ]) None
& info [ "int_backend" ] ~docv:"INT_BACKEND" ~doc)
+
+let roots : string list Term.t =
+ let doc = "Workspace(s) root(s)" in
+ Arg.(value & opt_all string [] & info [ "root" ] ~docv:"ROOTS" ~doc)
+
+let coq_diags_level : int Term.t =
+ let doc =
+ "Controsl whether Coq Info and Notice message appear in diagnostics.\n\
+ \ 0 = None; 1 = Notices, 2 = Notices and Info"
+ in
+ Arg.(value & opt int 0 & info [ "diags_level" ] ~docv:"DIAGS_LEVEL" ~doc)
diff --git a/coq/args.mli b/coq/args.mli
index 58d68a63..0181d2e8 100644
--- a/coq/args.mli
+++ b/coq/args.mli
@@ -16,4 +16,6 @@ val debug : Bool.t Term.t
val bt : Bool.t Term.t
val ml_include_path : string list Term.t
val ri_from : (string option * string) list Term.t
-val int_backend : Limits.backend Term.t
+val int_backend : Limits.backend option Term.t
+val roots : string list Term.t
+val coq_diags_level : int Term.t
diff --git a/coq/ast.ml b/coq/ast.ml
index 7491e6ec..ab8f8254 100644
--- a/coq/ast.ml
+++ b/coq/ast.ml
@@ -61,6 +61,51 @@ module Require = struct
| _ -> None
end
+module Meta = struct
+ type ast = t
+
+ open Ppx_hash_lib.Std.Hash.Builtin
+ open Ppx_compare_lib.Builtin
+ module Loc = Serlib.Ser_loc
+ module Names = Serlib.Ser_names
+ module Attributes = Serlib.Ser_attributes
+ module Vernacexpr = Serlib.Ser_vernacexpr
+
+ module Command = struct
+ type t =
+ | Back of int
+ | ResetName of Names.lident
+ | ResetInitial
+ [@@deriving hash, compare]
+ end
+
+ type t =
+ { command : Command.t
+ ; loc : Loc.t option
+ ; attrs : Attributes.vernac_flag list
+ ; control : Vernacexpr.control_flag list
+ }
+ [@@deriving hash, compare]
+
+ (* EJGA: Coq classification puts these commands as pure? Seems like yet
+ another bug... *)
+ let extract : ast -> t option =
+ CAst.with_loc_val (fun ?loc -> function
+ | { Vernacexpr.expr = Vernacexpr.(VernacResetName id)
+ ; control
+ ; attrs
+ } ->
+ let command = Command.ResetName id in
+ Some { command; loc; attrs; control }
+ | { expr = VernacResetInitial; control; attrs } ->
+ let command = Command.ResetInitial in
+ Some { command; loc; attrs; control }
+ | { expr = (VernacBack num); control; attrs } ->
+ let command = Command.Back num in
+ Some { command; loc; attrs; control }
+ | _ -> None)
+end
+
module Kinds = struct
(* LSP kinds *)
let _file = 1
diff --git a/coq/ast.mli b/coq/ast.mli
index 9a79710a..412d4721 100644
--- a/coq/ast.mli
+++ b/coq/ast.mli
@@ -32,6 +32,28 @@ module Require : sig
val extract : ast -> t option
end
+module Meta : sig
+ type ast = t
+
+ module Command : sig
+ type t =
+ | Back of int
+ | ResetName of Names.lident
+ | ResetInitial
+ end
+
+ type t =
+ { command : Command.t
+ ; loc : Loc.t option
+ ; attrs : Attributes.vernac_flag list
+ ; control : Vernacexpr.control_flag list
+ }
+ [@@deriving hash, compare]
+
+ (** Determine if we are under a meta-command *)
+ val extract : ast -> t option
+end
+
(** [make_info ~st ast] Compute info about a possible definition in [ast], we
need [~st] to compute the type. *)
val make_info :
diff --git a/compiler/util.ml b/coq/compat.ml
similarity index 87%
rename from compiler/util.ml
rename to coq/compat.ml
index 31745eb1..ea4561c3 100644
--- a/compiler/util.ml
+++ b/coq/compat.ml
@@ -1,3 +1,5 @@
+(* Compatibility file *)
+
module Ocaml_414 = struct
module In_channel = struct
(* 4.14 can do this: In_channel.with_open_bin file In_channel.input_all, so
@@ -8,6 +10,7 @@ module Ocaml_414 = struct
Fun.protect ~finally:(fun () -> Stdlib.close_in_noerr ic) (fun () -> f ic)
let with_open_bin s f = with_open Stdlib.open_in_bin s f
+ let with_open_text s f = with_open Stdlib.open_in s f
(* Read up to [len] bytes into [buf], starting at [ofs]. Return total bytes
read. *)
@@ -95,9 +98,22 @@ module Ocaml_414 = struct
end
end
-let input_all file =
- let open Ocaml_414 in
- In_channel.with_open_bin file In_channel.input_all
+module Result = struct
+ include Result
+
+ module O = struct
+ let ( let+ ) r f = map f r
+ let ( let* ) r f = bind r f
+ end
+
+ let split = function
+ | Ok (x1, x2) -> (Ok x1, Ok x2)
+ | Error err -> (Error err, Error err)
+
+ let pp pp_r pp_e fmt = function
+ | Ok r -> Format.fprintf fmt "@[Ok: @[%a@]@]" pp_r r
+ | Error e -> Format.fprintf fmt "@[Error: @[%a@]@]" pp_e e
+end
let format_to_file ~file ~f x =
let open Ocaml_414 in
diff --git a/coq/compat.mli b/coq/compat.mli
new file mode 100644
index 00000000..fc56a00c
--- /dev/null
+++ b/coq/compat.mli
@@ -0,0 +1,37 @@
+(* Compatiblity and general utils *)
+
+(* We should at some point remove all of this file in favor of a standard
+ library that suits our needs *)
+module Ocaml_414 : sig
+ module In_channel : sig
+ val with_open_bin : string -> (in_channel -> 'a) -> 'a
+ val with_open_text : string -> (in_channel -> 'a) -> 'a
+ val input_all : in_channel -> string
+ end
+
+ module Out_channel : sig
+ val with_open : ('a -> out_channel) -> 'a -> (out_channel -> 'b) -> 'b
+ val with_open_bin : string -> (out_channel -> 'a) -> 'a
+ end
+end
+
+val format_to_file :
+ file:string -> f:(Format.formatter -> 'a -> unit) -> 'a -> unit
+
+module Result : sig
+ include module type of Result
+
+ module O : sig
+ val ( let+ ) : ('a, 'l) t -> ('a -> 'b) -> ('b, 'l) t
+ val ( let* ) : ('a, 'l) t -> ('a -> ('b, 'l) t) -> ('b, 'l) t
+ end
+
+ val split : ('a * 'b, 'e) t -> ('a, 'e) t * ('b, 'e) t
+
+ val pp :
+ (Format.formatter -> 'r -> unit)
+ -> (Format.formatter -> 'e -> unit)
+ -> Format.formatter
+ -> ('r, 'e) Result.t
+ -> unit
+end
diff --git a/coq/dune b/coq/dune
index 15e19a97..7d954f4c 100644
--- a/coq/dune
+++ b/coq/dune
@@ -1,11 +1,8 @@
(library
(name coq)
(public_name coq-lsp.coq)
- ; Unfortunate we have to link the STM due to the LTAC plugin
- ; depending on it, we should fix this upstream
- (inline_tests)
(preprocess
- (pps ppx_compare ppx_hash ppx_inline_test))
+ (pps ppx_compare ppx_hash))
(libraries
(select
limits_mp_impl.ml
diff --git a/coq/glob.ml b/coq/glob.ml
new file mode 100644
index 00000000..6c9eb714
--- /dev/null
+++ b/coq/glob.ml
@@ -0,0 +1,190 @@
+(************************************************************************)
+(* * The Coq Proof Assistant / The Coq Development Team *)
+(* v * INRIA, CNRS and contributors - Copyright 1999-2018 *)
+(* "Ill-formed file: " ^ s
+ | Outdated -> "Outdated .glob file"
+end
+
+module Info = struct
+ type t =
+ { kind : string
+ ; offset : int * int
+ }
+end
+
+(* This is taken from coqdoc/glob_file.ml , we should share this code, but no
+ cycles ATM *)
+module Coq = struct
+ module Entry_type = struct
+ type t =
+ | Library
+ | Module
+ | Definition
+ | Inductive
+ | Constructor
+ | Lemma
+ | Record
+ | Projection
+ | Instance
+ | Class
+ | Method
+ | Variable
+ | Axiom
+ | TacticDefinition
+ | Abbreviation
+ | Notation
+ | Section
+ | Binder
+
+ let of_string = function
+ | "def"
+ | "coe"
+ | "subclass"
+ | "canonstruc"
+ | "fix"
+ | "cofix"
+ | "ex"
+ | "scheme" -> Definition
+ | "prf" | "thm" -> Lemma
+ | "ind" | "variant" | "coind" -> Inductive
+ | "constr" -> Constructor
+ | "indrec" | "rec" | "corec" -> Record
+ | "proj" -> Projection
+ | "class" -> Class
+ | "meth" -> Method
+ | "inst" -> Instance
+ | "var" -> Variable
+ | "defax" | "prfax" | "ax" -> Axiom
+ | "abbrev" | "syndef" -> Abbreviation
+ | "not" -> Notation
+ | "lib" -> Library
+ | "mod" | "modtype" -> Module
+ | "tac" -> TacticDefinition
+ | "sec" -> Section
+ | "binder" -> Binder
+ | s -> invalid_arg ("type_of_string:" ^ s)
+ end
+
+ let read_dp ic =
+ let line = input_line ic in
+ let n = String.length line in
+ match line.[0] with
+ | 'F' ->
+ let lib_name = String.sub line 1 (n - 1) in
+ Ok lib_name
+ | _ -> Error (Error.Ill_formed "lib name not found in header")
+
+ let correct_file vfile ic =
+ let s = input_line ic in
+ if String.length s < 7 || String.sub s 0 7 <> "DIGEST " then
+ Error (Error.Ill_formed "DIGEST ill-formed")
+ else
+ let s = String.sub s 7 (String.length s - 7) in
+ match (vfile, s) with
+ | None, "NO" -> read_dp ic
+ | Some _, "NO" -> Error (Ill_formed "coming from .v file but no digest")
+ | None, _ -> Error (Ill_formed "digest but .v source file not available")
+ | Some vfile, s ->
+ if s = Digest.to_hex (Digest.file vfile) then
+ (* XXX Read F line *)
+ read_dp ic
+ else Error Outdated
+
+ let parse_ref line =
+ try
+ (* Disable for now *)
+ if false then
+ let add_ref _ _ _ _ _ = () in
+ Scanf.sscanf line "R%d:%d %s %s %s %s" (fun loc1 loc2 lib_dp sp id ty ->
+ for loc = loc1 to loc2 do
+ add_ref loc lib_dp sp id (Entry_type.of_string ty);
+ (* Also add an entry for each module mentioned in [lib_dp], * to
+ use in interpolation. *)
+ ignore
+ (List.fold_right
+ (fun thisPiece priorPieces ->
+ let newPieces =
+ match priorPieces with
+ | "" -> thisPiece
+ | _ -> thisPiece ^ "." ^ priorPieces
+ in
+ add_ref loc "" "" newPieces Entry_type.Library;
+ newPieces)
+ (Str.split (Str.regexp_string ".") lib_dp)
+ "")
+ done)
+ with _ -> ()
+
+ let parse_def line : _ Result.t =
+ try
+ Scanf.sscanf line "%s %d:%d %s %s"
+ (fun kind loc1 loc2 section_path name ->
+ Ok (name, section_path, kind, (loc1, loc2)))
+ with Scanf.Scan_failure err -> Error err
+
+ let process_line dp map line =
+ match line.[0] with
+ | 'R' ->
+ let _reference = parse_ref line in
+ map
+ | _ -> (
+ match parse_def line with
+ | Error _ -> map
+ | Ok (name, section_path, kind, offset) ->
+ let section_path =
+ if String.equal "<>" section_path then "" else section_path ^ "."
+ in
+ let name = dp ^ "." ^ section_path ^ name in
+ let info = { Info.kind; offset } in
+ DefMap.add name info map)
+
+ let read_glob vfile inc =
+ match correct_file vfile inc with
+ | Error e -> Error (Error.to_string e)
+ | Ok dp -> (
+ let map = ref DefMap.empty in
+ try
+ while true do
+ let line = input_line inc in
+ let n = String.length line in
+ if n > 0 then map := process_line dp !map line
+ done;
+ assert false
+ with End_of_file -> Ok !map)
+end
+
+(* Glob file that was read and parsed successfully *)
+type t = Info.t DefMap.t
+
+let open_file file =
+ if Sys.file_exists file then
+ let vfile = Filename.remove_extension file ^ ".v" in
+ Compat.Ocaml_414.In_channel.with_open_text file (Coq.read_glob (Some vfile))
+ else Error (Format.asprintf "Cannot open file: %s" file)
+
+let get_info map name =
+ match DefMap.find_opt name map with
+ | Some info -> Ok info
+ | None -> Error (Format.asprintf "definition %s not found in glob table" name)
diff --git a/coq/glob.mli b/coq/glob.mli
new file mode 100644
index 00000000..83376443
--- /dev/null
+++ b/coq/glob.mli
@@ -0,0 +1,30 @@
+(************************************************************************)
+(* * The Coq Proof Assistant / The Coq Development Team *)
+(* v * INRIA, CNRS and contributors - Copyright 1999-2018 *)
+(* (t, string) Result.t
+
+module Info : sig
+ type t =
+ { kind : string
+ ; offset : int * int
+ }
+end
+
+val get_info : t -> string -> (Info.t, string) Result.t
diff --git a/coq/goals.ml b/coq/goals.ml
index f0855ad8..9b6b7b4e 100644
--- a/coq/goals.ml
+++ b/coq/goals.ml
@@ -50,9 +50,10 @@ type ('a, 'pp) goals =
; given_up : 'a list
}
-let map_goals ~f { goals; stack; bullet; shelf; given_up } =
+let map_goals ~f ~g { goals; stack; bullet; shelf; given_up } =
let goals = List.map f goals in
let stack = List.map (fun (s, r) -> (List.map f s, List.map f r)) stack in
+ let bullet = Option.map g bullet in
let shelf = List.map f shelf in
let given_up = List.map f given_up in
{ goals; stack; bullet; shelf; given_up }
diff --git a/coq/goals.mli b/coq/goals.mli
index 8614594c..5dc5e616 100644
--- a/coq/goals.mli
+++ b/coq/goals.mli
@@ -42,7 +42,8 @@ type ('a, 'pp) goals =
; given_up : 'a list
}
-val map_goals : f:('a -> 'b) -> ('a, 'pp) goals -> ('b, 'pp) goals
+val map_goals :
+ f:('a -> 'b) -> g:('pp -> 'pp') -> ('a, 'pp) goals -> ('b, 'pp') goals
type 'pp reified_pp = ('pp reified_goal, 'pp) goals
diff --git a/coq/library_file.ml b/coq/library_file.ml
new file mode 100644
index 00000000..b0c8c347
--- /dev/null
+++ b/coq/library_file.ml
@@ -0,0 +1,139 @@
+type t = Names.DirPath.t
+
+let name n = n
+let loaded ~token ~st = State.in_state ~token ~st ~f:Library.loaded_libraries ()
+
+(* Function to extract definitions and lemmas from the environment *)
+module DynHandle = Libobject.Dyn.Map (struct
+ type 'a t = 'a -> unit
+end)
+
+(* Prefix is the module / section things are defined *)
+let const_handler
+ (fn : Names.Constant.t -> Decls.logical_kind -> Constr.t -> unit) prefix
+ (id, obj) =
+ let open Names in
+ let kn = KerName.make prefix.Nametab.obj_mp (Label.of_id id) in
+ let cst = Global.constant_of_delta_kn kn in
+ let gr = GlobRef.ConstRef cst in
+ let env = Global.env () in
+ let typ, _ = Typeops.type_of_global_in_context env gr in
+ let kind = Declare.Internal.Constant.kind obj in
+ fn cst kind typ
+
+let iter_constructors indsp u fn env nconstr =
+ for i = 1 to nconstr do
+ let typ =
+ Inductive.type_of_constructor
+ ((indsp, i), u)
+ (Inductive.lookup_mind_specif env indsp)
+ in
+ fn (Names.GlobRef.ConstructRef (indsp, i)) typ
+ done
+
+let ind_handler fn prefix (id, (_obj : DeclareInd.Internal.inductive_obj)) =
+ let open Names in
+ let kn = KerName.make prefix.Nametab.obj_mp (Label.of_id id) in
+ let mind = Global.mind_of_delta_kn kn in
+ let mib = Global.lookup_mind mind in
+ let env = Global.env () in
+ let iter_packet i mip =
+ let ind = (mind, i) in
+ let u =
+ Univ.make_abstract_instance (Declareops.inductive_polymorphic_context mib)
+ in
+ let typ =
+ Inductive.type_of_inductive (Inductive.lookup_mind_specif env ind, u)
+ in
+ let () = fn (GlobRef.IndRef ind) typ in
+ let len = Array.length mip.Declarations.mind_user_lc in
+ iter_constructors ind u fn env len
+ in
+ Array.iteri iter_packet mib.mind_packets
+
+let handler fn_c fn_i prefix =
+ DynHandle.add Declare.Internal.Constant.tag (const_handler fn_c prefix)
+ @@ DynHandle.add DeclareInd.Internal.objInductive (ind_handler fn_i prefix)
+ @@ DynHandle.empty
+
+let handle h (Libobject.Dyn.Dyn (tag, o)) =
+ match DynHandle.find tag h with
+ | f -> f o
+ | exception Stdlib.Not_found -> ()
+
+let obj_action fn_c fn_i prefix lobj =
+ match lobj with
+ | Libobject.AtomicObject o -> handle (handler fn_c fn_i prefix) o
+ | _ -> ()
+
+let constructor_name c idx =
+ let cname =
+ Nametab.shortest_qualid_of_global Names.Id.Set.empty
+ (Names.GlobRef.ConstructRef (c, idx))
+ in
+ Libnames.string_of_qualid cname
+
+let constructor_info (gref : Names.GlobRef.t) =
+ match gref with
+ | ConstructRef (ind, idx) ->
+ let ind_dp = Names.(MutInd.modpath (fst ind) |> ModPath.dp) in
+ Some (ind_dp, constructor_name ind idx)
+ | VarRef _ | ConstRef _ | IndRef _ -> None
+
+let belongs_to_lib dps dp =
+ List.exists (fun p -> Libnames.is_dirpath_prefix_of p dp) dps
+
+module Entry = struct
+ type t =
+ { name : string
+ ; typ : Constr.t
+ ; file : string
+ }
+end
+
+let to_result ~f x =
+ try Ok (f x)
+ with exn when CErrors.noncritical exn ->
+ let iexn = Exninfo.capture exn in
+ Error iexn
+
+let try_locate_absolute_library dir =
+ let f = Loadpath.try_locate_absolute_library in
+ to_result ~f dir
+
+let find_v_file dir =
+ match try_locate_absolute_library dir with
+ (* EJGA: we want to improve this as to pass the error to the client *)
+ | Error _ -> "error when trying to locate the .v file"
+ | Ok file -> file
+
+let toc dps : Entry.t list =
+ let res : Entry.t list ref = ref [] in
+ let obj_action =
+ let fn_c (cst : Names.Constant.t) (_ : Decls.logical_kind) (typ : Constr.t)
+ =
+ let cst_dp = Names.(Constant.modpath cst |> ModPath.dp) in
+ if belongs_to_lib dps cst_dp then
+ (* let () = F.eprintf "cst found: %s@\n%!" (Names.Constant.to_string
+ cst) in *)
+ let name = Names.Constant.to_string cst in
+ let file = find_v_file cst_dp in
+ res := { name; typ; file } :: !res
+ else ()
+ in
+ (* We do nothing for inductives, note this is called both on constructors
+ and inductives, with the name and type *)
+ let fn_i (gref : Names.GlobRef.t) (typ : Constr.t) =
+ match constructor_info gref with
+ | None -> ()
+ | Some (ind_dp, name) ->
+ if belongs_to_lib dps ind_dp then
+ let file = find_v_file ind_dp in
+ res := { name; typ; file } :: !res
+ in
+ obj_action fn_c fn_i
+ in
+ let () = Declaremods.iter_all_segments obj_action in
+ List.rev !res
+
+let toc ~token ~st dps = State.in_state ~token ~st ~f:toc dps
diff --git a/coq/library_file.mli b/coq/library_file.mli
new file mode 100644
index 00000000..a93fd482
--- /dev/null
+++ b/coq/library_file.mli
@@ -0,0 +1,38 @@
+(* API to handle vo libraries, we cannot use Library as module name due to Coq's
+ libs not being wrapped... *)
+
+(* Library stored in a .vo file, it can contain several modules *)
+type t
+
+(** Logical path of the library *)
+val name : t -> Names.DirPath.t
+
+module Entry : sig
+ type t =
+ { name : string
+ ; typ : Constr.t
+ ; file : string
+ }
+end
+
+(** [toc libs] Returns the list of constants and inductives found on .vo
+ libraries [libs], as pairs of [name, typ]. Note that the constants are
+ returned in the order they appear on the file.
+
+ NOTE that (unfortunately) this is a very expensive process, similary to
+ Coq's Search, as this routine will have to traverse all the library objects
+ in scope.
+
+ Hence, we provide a slightly more efficient version that can do multiple
+ libraries but with the same complexity.
+
+ There have been several upstream Coq PRs trying to improve this situation,
+ but so far they didn't make enough progress. *)
+val toc :
+ token:Limits.Token.t
+ -> st:State.t
+ -> t list
+ -> (Entry.t list, Loc.t) Protect.E.t
+
+(** Recovers the list of loaded libraries for state [st] *)
+val loaded : token:Limits.Token.t -> st:State.t -> (t list, Loc.t) Protect.E.t
diff --git a/coq/limits.ml b/coq/limits.ml
index a6855c6d..847962ba 100644
--- a/coq/limits.ml
+++ b/coq/limits.ml
@@ -10,7 +10,7 @@ module type Intf = sig
val start : unit -> unit
val limit : token:Token.t -> f:('a -> 'b) -> 'a -> ('b, exn) Result.t
- val name : string
+ val name : unit -> string
val available : bool
end
@@ -41,7 +41,7 @@ module Coq : Intf = struct
let () = Control.interrupt := false in
try Ok (f x) with Sys.Break -> Error Sys.Break
- let name = "Control.interrupt"
+ let name () = "Control.interrupt"
let available = true
end
@@ -57,6 +57,18 @@ let select = function
| Coq -> backend := (module Coq)
| Mp -> backend := (module Mp)
+(* Set this to false for 8.19 and lower *)
+let sane_coq_base_version = true
+
+let sane_coq_branch =
+ CString.string_contains ~where:Coq_config.version ~what:"+lsp"
+
+let safe_coq = sane_coq_base_version || sane_coq_branch
+
+let select_best = function
+ | None -> if Mp.available && safe_coq then select Mp else select Coq
+ | Some backend -> select backend
+
module Token = struct
type t =
| C of Coq.Token.t
@@ -65,7 +77,7 @@ module Token = struct
let create () =
let module M = (val !backend) in
- match M.name with
+ match M.name () with
| "memprof-limits" -> M (Mp.Token.create ())
| "Control.interrupt" | _ -> C (Coq.Token.create ())
@@ -95,5 +107,8 @@ let limit ~token ~f x =
let () = Control.interrupt := false in
Ok (f x)
-let name = "select backend"
+let name () =
+ let module M = (val !backend) in
+ M.name ()
+
let available = true
diff --git a/coq/limits.mli b/coq/limits.mli
index 631b4c3a..b5886c4c 100644
--- a/coq/limits.mli
+++ b/coq/limits.mli
@@ -10,7 +10,7 @@ module type Intf = sig
val start : unit -> unit
val limit : token:Token.t -> f:('a -> 'b) -> 'a -> ('b, exn) Result.t
- val name : string
+ val name : unit -> string
val available : bool
end
@@ -25,4 +25,7 @@ type backend =
(** *Must* be called *only* once *)
val select : backend -> unit
+(** If [None] will pick the best backend, must be called only once *)
+val select_best : backend option -> unit
+
val create_atomic : unit -> Token.t
diff --git a/coq/limits_mp_impl.fake.ml b/coq/limits_mp_impl.fake.ml
index 7baeca46..386991fb 100644
--- a/coq/limits_mp_impl.fake.ml
+++ b/coq/limits_mp_impl.fake.ml
@@ -9,5 +9,5 @@ end
let start () = ()
let limit ~token:_ ~f x = Result.Ok (f x)
-let name = "memprof-limits"
+let name () = "memprof-limits (fake)"
let available = false
diff --git a/coq/limits_mp_impl.real.ml b/coq/limits_mp_impl.real.ml
index d5c4801f..a3a162ea 100644
--- a/coq/limits_mp_impl.real.ml
+++ b/coq/limits_mp_impl.real.ml
@@ -7,5 +7,5 @@ let limit ~token ~f x =
let f () = f x in
Memprof_limits.limit_with_token ~token f
-let name = "memprof-limits"
+let name () = "memprof-limits"
let available = true
diff --git a/coq/loader.ml b/coq/loader.ml
index e9af4464..9dc1503e 100644
--- a/coq/loader.ml
+++ b/coq/loader.ml
@@ -38,6 +38,7 @@ let check_package_exists fl_pkg =
let map_serlib fl_pkg =
let supported = match fl_pkg with
(* Supported by serlib *) (* directory *)
+ | "coq-core.plugins.btauto" (* btauto *)
| "coq-core.plugins.cc" (* cc *)
| "coq-core.plugins.extraction" (* extraction *)
| "coq-core.plugins.firstorder" (* firstorder *)
@@ -45,6 +46,7 @@ let map_serlib fl_pkg =
| "coq-core.plugins.ltac" (* ltac *)
| "coq-core.plugins.ltac2" (* ltac2 *)
| "coq-core.plugins.micromega" (* micromega *)
+ | "coq-core.plugins.micromega_core" (* micromega_core *)
| "coq-core.plugins.ring" (* ring *)
| "coq-core.plugins.ssreflect" (* ssreflect *)
| "coq-core.plugins.ssrmatching" (* ssrmatching *)
diff --git a/coq/protect.ml b/coq/protect.ml
index 284ce801..75313251 100644
--- a/coq/protect.ml
+++ b/coq/protect.ml
@@ -82,6 +82,7 @@ module E = struct
{ r; feedback = feedback @ fb2 }
let ok v = { r = Completed (Ok v); feedback = [] }
+ let error err = { r = R.error err; feedback = [] }
module O = struct
let ( let+ ) x f = map ~f x
diff --git a/coq/protect.mli b/coq/protect.mli
index ee262dfe..c0c2fe73 100644
--- a/coq/protect.mli
+++ b/coq/protect.mli
@@ -39,6 +39,7 @@ module E : sig
val map_loc : f:('l -> 'm) -> ('a, 'l) t -> ('a, 'm) t
val bind : f:('a -> ('b, 'l) t) -> ('a, 'l) t -> ('b, 'l) t
val ok : 'a -> ('a, 'l) t
+ val error : Pp.t -> ('a, 'l) t
module O : sig
val ( let+ ) : ('a, 'l) t -> ('a -> 'b) -> ('b, 'l) t
diff --git a/coq/utf8.mli b/coq/utf8.mli
deleted file mode 100644
index 2c66e66e..00000000
--- a/coq/utf8.mli
+++ /dev/null
@@ -1,52 +0,0 @@
-(* Copyright (C) 2002, 2003 Yamagata Yoriyuki. *)
-
-(* This library is free software; you can redistribute it and/or *)
-(* modify it under the terms of the GNU Lesser General Public License *)
-(* as published by the Free Software Foundation; either version 2 of *)
-(* the License, or (at your option) any later version. *)
-
-(* As a special exception to the GNU Library General Public License, you *)
-(* may link, statically or dynamically, a "work that uses this library" *)
-(* with a publicly distributed version of this library to produce an *)
-(* executable file containing portions of this library, and distribute *)
-(* that executable file under terms of your choice, without any of the *)
-(* additional requirements listed in clause 6 of the GNU Library General *)
-(* Public License. By "a publicly distributed version of this library", *)
-(* we mean either the unmodified Library as distributed by the authors, *)
-(* or a modified version of this library that is distributed under the *)
-(* conditions defined in clause 3 of the GNU Library General Public *)
-(* License. This exception does not however invalidate any other reasons *)
-(* why the executable file might be covered by the GNU Library General *)
-(* Public License . *)
-
-(* This library is distributed in the hope that it will be useful, *)
-(* but WITHOUT ANY WARRANTY; without even the implied warranty of *)
-(* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *)
-(* Lesser General Public License for more details. *)
-
-(* You should have received a copy of the GNU Lesser General Public *)
-(* License along with this library; if not, write to the Free Software *)
-(* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *)
-(* USA *)
-
-(* Taken from camomille *)
-
-(* utf8 utils, both Coq and Camomile have similar implementations, at some point
- we should remove this but for now we keep it internal. For now we use the
- Camomille functions *)
-
-type char = int
-type index = int
-
-(** Byte index to UTF-8 character position *)
-val char_of_index : line:string -> byte:index -> char option
-
-(** UTF-8 Char to byte index position *)
-val index_of_char : line:string -> char:char -> index option
-
-(** Lenght in utf-8 chars *)
-val length : string -> char
-
-(** Get the byte potition of a code point indexed in UTF-16 code units in a
- UTF-8 encoded string. *)
-val get_byte_offset_from_utf16_pos : string -> int -> int option
diff --git a/coq/utils.ml b/coq/utils.ml
index 53331257..12caf7c3 100644
--- a/coq/utils.ml
+++ b/coq/utils.ml
@@ -16,12 +16,10 @@
(* We convert in case of failure to some default values *)
-let char_of_index ~lines ~line ~byte =
+let utf16_offset_of_utf8_offset ~lines ~line ~byte =
if line < Array.length lines then
let line = Array.get lines line in
- match Utf8.char_of_index ~line ~byte with
- | Some char -> char
- | None -> Utf8.length line
+ Lang.Utf.utf16_offset_of_utf8_offset ~line ~offset:byte
else 0
let to_range ~lines (p : Loc.t) : Lang.Range.t =
@@ -34,8 +32,12 @@ let to_range ~lines (p : Loc.t) : Lang.Range.t =
let start_col = bp - bol_pos in
let end_col = ep - bol_pos_last in
- let start_col = char_of_index ~lines ~line:start_line ~byte:start_col in
- let end_col = char_of_index ~lines ~line:end_line ~byte:end_col in
+ let start_col =
+ utf16_offset_of_utf8_offset ~lines ~line:start_line ~byte:start_col
+ in
+ let end_col =
+ utf16_offset_of_utf8_offset ~lines ~line:end_line ~byte:end_col
+ in
Lang.Range.
{ start = { line = start_line; character = start_col; offset = bp }
; end_ = { line = end_line; character = end_col; offset = ep }
diff --git a/coq/workspace.ml b/coq/workspace.ml
index e9aca9f4..649f8502 100644
--- a/coq/workspace.ml
+++ b/coq/workspace.ml
@@ -272,6 +272,11 @@ let load_objs libs =
in
List.(iter rq_file (rev libs))
+let fleche_chop_extension basename =
+ match Filename.chop_suffix_opt ~suffix:".v.tex" basename with
+ | Some file -> file
+ | None -> Filename.chop_extension basename
+
(* We need to compute this with the right load path *)
let dirpath_of_uri ~uri =
let f = Lang.LUri.File.to_string_file uri in
@@ -282,7 +287,7 @@ let dirpath_of_uri ~uri =
with Not_found -> Libnames.default_root_prefix
in
let f =
- try Filename.chop_extension (Filename.basename f)
+ try fleche_chop_extension (Filename.basename f)
with Invalid_argument _ -> f
in
let id = Names.Id.of_string f in
diff --git a/editor/code/CHANGELOG.md b/editor/code/CHANGELOG.md
index b5eee4a5..1396f16b 100644
--- a/editor/code/CHANGELOG.md
+++ b/editor/code/CHANGELOG.md
@@ -1,3 +1,87 @@
+# coq-lsp 0.1.9: Hasta el 40 de Mayo...
+---------------------------------------
+
+ - new configuration value `check_only_on_request` which will delay
+ checking the document until a request has been made. This means
+ users can now switch between continuous and on-demand modes. (#629,
+ cc: #24, @ejgallego)
+ - display the continous/on-request checking mode in the status bar,
+ allow to change it by clicking on it (@ejgallego, #721)
+ - new Coq Language Status Item that display server status, version,
+ and memory use. We recommend the users to pin it.
+ - new heatmap feature allowing timing data to be seen in the
+ editor. use the `Coq LSP: Toggle heatmap` command. Colors and
+ granularity are configurable, see settings (@Alizter, #686)
+ - new VSCode commands to allow to move one sentence backwards /
+ forward, this is particularly useful when combined with lazy
+ checking mode (@ejgallego, #671, fixes #263, fixes #580)
+ - VSCode commands `coq-lsp.sentenceNext` / `coq-lsp.sentencePrevious`
+ are now bound by default to `Alt + N` / `Alt + P` keybindings
+ (@ejgallego, #718)
+ - new option `show_loc_info_on_hover` that will display parsing debug
+ information on hover
+ - fix activation bug that prevented extension activation for `.mv`
+ files (@ejgallego @r3m0t, #598, cc #596, reported by Théo Zimmerman)
+ - require VSCode >= 1.82 in package.json. (@ejgallego, #599,
+ thanks to Théo Zimmerman for the report)
+ - update progress indicator correctly on End Of File (@ejgallego,
+ #605, fixes #445)
+ - switch default of `goal_after_tactic` to `true` (@Alizter,
+ @ejgallego, cc: #614)
+ - error recovery: Recognize `Defined` and `Admitted` in lex recovery
+ (@ejgallego, #616)
+ - don't trigger the goals window in general markdown buffer
+ (@ejgallego, #625, reported by Théo Zimmerman)
+ - fix typo on package.json configuration section (@ejgallego, #645)
+ - support for Coq 8.16 has been abandoned due to lack of dev
+ resources (@ejgallego, #649)
+ - new "Coq LSP: Free Memory" command to liberate space used in the
+ cache (@ejgallego, #662, fixes #367 cc: #253 #236 #348)
+ - fix Coq performance view display panel (@ejgallego, #663,
+ regression in #513)
+ - new public VSCode extension API so other extensions can perform
+ actions when the user request the goals (@ejgallego, @bhaktishh,
+ #672, fixes #538)
+ - Support Visual Studio Live Share URIs better (`vsls://`), in
+ particular don't try to display goals if the URI is VSLS one
+ (@ejgallego, #676)
+ - Send performance performance data for the full document
+ (@ejgallego, @Alizter, #689, #693)
+ - New client option to enable / disable `coq/perfData` (@ejgallego, #717)
+ - The `coq-lsp.document` VSCode command will now display the returned
+ JSON data in a new editor (@ejgallego, #701)
+ - Update server settings on the fly when tweaking them in VSCode.
+ Implement `workspace/didChangeConfiguration` (@ejgallego, #702)
+ - Always dispose UI elements. This should improve some strange
+ behaviors on extension restart (@ejgallego, #708)
+ - Support Coq meta-commands (Reset, Reset Initial, Back) They are
+ actually pretty useful to hint the incremental engine to ignore
+ changes in some part of the document (@ejgallego, #709)
+ - New `coq/viewRange` notification, from client to server, than hints
+ the scheduler for the visible area of the document; combined with
+ the new lazy checking mode, this provides checking on scroll, a
+ feature inspired from Isabelle IDE (@ejgallego, #717)
+ - Have VSCode wait for full LSP client shutdown on server
+ restart. This fixes some bugs on extension restart (finally!)
+ (@ejgallego, #719)
+ - Center the view if cursor goes out of scope in
+ `sentenceNext/sentencePrevious` (@ejgallego, #718)
+ - Switch Flèche range encoding to protocol native, this means UTF-16
+ code points for now (Léo Stefanesco, @ejgallego, #624, fixes #620,
+ #621)
+ - Give `Goals` panel focus back if it has lost it (in case of
+ multiple panels in the second viewColumn of Vscode) whenever
+ user navigates proofs (@Alidra @ejgallego, #722, #725)
+ - Add an example of multiple workspaces (@ejgallego, @Blaisorblade,
+ #611)
+ - Don't show types of un-expanded goals. We should add an option for
+ this, but we don't have the cycles (@ejgallego, #730, workarounds
+ #525 #652)
+ - Support for `.lv / .v.tex` TeX files with embedded Coq code
+ (@ejgallego, #727)
+ - Don't expand bullet goals at previous levels by default
+ (@ejgallego, @Alizter, #731 cc #525)
+
# coq-lsp 0.1.8: Trick-or-treat
-------------------------------
diff --git a/editor/code/lib/types.ts b/editor/code/lib/types.ts
index 2de1f115..1e196f07 100644
--- a/editor/code/lib/types.ts
+++ b/editor/code/lib/types.ts
@@ -31,6 +31,7 @@ export interface Message {
export type Id = ["Id", string];
+// XXX: Only used in obligations, move them to Range
export interface Loc {
fname: any;
line_nb: number;
@@ -113,16 +114,26 @@ export interface FlecheSaveParams {
textDocument: VersionedTextDocumentIdentifier;
}
-export interface SentencePerfParams {
- loc: Loc;
+export interface PerfInfo {
+ // Original Execution Time (when not cached)
time: number;
- mem: number;
+ // Difference in words allocated in the heap using `Gc.quick_stat`
+ memory: number;
+ // Whether the execution was cached
+ cache_hit: boolean;
+ // Caching overhead
+ time_hash: number;
}
-export interface DocumentPerfParams {
+export interface SentencePerfParams {
+ range: R;
+ info: PerfInfo;
+}
+
+export interface DocumentPerfParams {
textDocument: VersionedTextDocumentIdentifier;
summary: string;
- timings: SentencePerfParams[];
+ timings: SentencePerfParams[];
}
// View messaging interfaces; should go on their own file
@@ -152,3 +163,48 @@ export type CoqMessagePayload = RenderGoals | WaitingForInfo | InfoError;
export interface CoqMessageEvent extends MessageEvent {
data: CoqMessagePayload;
}
+
+// For perf panel data
+export interface PerfUpdate {
+ method: "update";
+ params: DocumentPerfParams;
+}
+
+export interface PerfReset {
+ method: "reset";
+}
+
+export type PerfMessagePayload = PerfUpdate | PerfReset;
+
+export interface PerfMessageEvent extends MessageEvent {
+ data: PerfMessagePayload;
+}
+
+export interface ViewRangeParams {
+ textDocument: VersionedTextDocumentIdentifier;
+ range: Range;
+}
+
+// Server version and status notifications
+
+export interface CoqServerVersion {
+ coq: string;
+ ocaml: string;
+ coq_lsp: string;
+}
+
+export interface CoqBusyStatus {
+ status: "Busy";
+ modname: string;
+}
+
+export interface CoqIdleStatus {
+ status: "Idle";
+ mem: string;
+}
+
+export interface CoqStoppedStatus {
+ status: "Stopped";
+}
+
+export type CoqServerStatus = CoqBusyStatus | CoqIdleStatus | CoqStoppedStatus;
diff --git a/editor/code/package-lock.json b/editor/code/package-lock.json
index 6a382f61..3fb54af4 100644
--- a/editor/code/package-lock.json
+++ b/editor/code/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "coq-lsp",
- "version": "0.1.9-dev",
+ "version": "0.1.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "coq-lsp",
- "version": "0.1.9-dev",
+ "version": "0.1.9",
"dependencies": {
"@vscode/webview-ui-toolkit": "^1.2.2",
"jquery": "^3.7.1",
diff --git a/editor/code/package.json b/editor/code/package.json
index bca67a6b..c1bb58a6 100644
--- a/editor/code/package.json
+++ b/editor/code/package.json
@@ -2,7 +2,7 @@
"name": "coq-lsp",
"displayName": "Coq LSP",
"description": "Coq LSP provides native vsCode support for checking Coq proof documents",
- "version": "0.1.9-dev",
+ "version": "0.1.9",
"contributors": [
"Emilio Jesús Gallego Arias ",
"Ali Caglayan ",
@@ -29,7 +29,8 @@
"url": "https://github.com/ejgallego/coq-lsp"
},
"activationEvents": [
- "onLanguage:markdown"
+ "onLanguage:markdown",
+ "onLanguage:latex"
],
"contributes": {
"languages": [
@@ -54,6 +55,15 @@
"extensions": [
".mv"
]
+ },
+ {
+ "id": "latex",
+ "aliases": [
+ "LaTeX"
+ ],
+ "extensions": [
+ ".lv"
+ ]
}
],
"grammars": [
@@ -90,13 +100,17 @@
"command": "coq-lsp.toggle",
"title": "Coq LSP: Toggle the running status of Coq Language Server"
},
+ {
+ "command": "coq-lsp.toggle_mode",
+ "title": "Coq LSP: Toggle checking mode between continous / on-demand"
+ },
{
"command": "coq-lsp.goals",
- "title": "Coq LSP: Show Goals at Point"
+ "title": "Coq LSP: Show Goals at point"
},
{
"command": "coq-lsp.document",
- "title": "Coq LSP: Get document in serialized format, print in console"
+ "title": "Coq LSP: Serialize document to JSON"
},
{
"command": "coq-lsp.save",
@@ -104,15 +118,19 @@
},
{
"command": "coq-lsp.trim",
- "title": "Coq LSP: Request the server to trim caches and compact memory (useful to try reduce memory comsumption)"
+ "title": "Coq LSP: Free memory"
},
{
"command": "coq-lsp.sentenceNext",
- "title": "Coq LSP: try to jump to next Coq sentence"
+ "title": "Coq LSP: Try to jump to next Coq sentence"
+ },
+ {
+ "command": "coq-lsp.sentencePrevious",
+ "title": "Coq LSP: Try to jump to previous Coq sentence"
},
{
- "command": "coq-lsp.sentenceBack",
- "title": "Coq LSP: try to jump to previous Coq sentence"
+ "command": "coq-lsp.heatmap.toggle",
+ "title": "Coq LSP: Toggle heatmap"
}
],
"keybindings": [
@@ -121,6 +139,18 @@
"key": "alt+enter",
"mac": "meta+enter",
"when": "editorTextFocus && (editorLangId == coq || editorTextFocus && editorLangId == markdown)"
+ },
+ {
+ "command": "coq-lsp.sentenceNext",
+ "key": "Alt+N",
+ "mac": "cmd+N",
+ "when": "editorTextFocus && (editorLangId == coq || editorTextFocus && editorLangId == markdown)"
+ },
+ {
+ "command": "coq-lsp.sentencePrevious",
+ "key": "Alt+P",
+ "mac": "cmd+P",
+ "when": "editorTextFocus && (editorLangId == coq || editorTextFocus && editorLangId == markdown)"
}
],
"viewsContainers": {
@@ -278,8 +308,13 @@
},
"coq-lsp.check_only_on_request": {
"type": "boolean",
- "default": false,
- "description": "Check files lazily, that is to say, goal state for a point will only be computed when the data is actually demanded. Note that this option is experimental and not recommended for use yet; we expose it only for testing and further development."
+ "default": true,
+ "description": "(Experimental) Check files lazily, that is to say, goal state for a point will only be computed when the data is actually demanded."
+ },
+ "coq-lsp.check_on_scroll": {
+ "type": "boolean",
+ "default": true,
+ "description": "(Experimental) When in lazy mode, check files on scroll."
}
}
},
@@ -309,6 +344,41 @@
"description": "Automatically ignore Coq object files in the workspace (.vo, .vos, ...)"
}
}
+ },
+ {
+ "title": "Heatmap",
+ "type": "object",
+ "properties": {
+ "coq-lsp.heatmap.enabled": {
+ "type": "boolean",
+ "default": false,
+ "description": "Enable heatmap for Coq files."
+ },
+ "coq-lsp.heatmap.warmColor": {
+ "type": "string",
+ "default": "#ff0000",
+ "description": "Defines the warm color for the heatmap."
+ },
+ "coq-lsp.heatmap.coldColor": {
+ "type": "string",
+ "default": "#000000",
+ "description": "Defines the cold color for the heatmap."
+ },
+ "coq-lsp.heatmap.heatLevels": {
+ "type": "number",
+ "default": 100,
+ "description": "Defines the number of heat levels in the heatmap."
+ },
+ "coq-lsp.heatmap.mode": {
+ "type": "string",
+ "default": "time",
+ "enum": [
+ "time",
+ "memory"
+ ],
+ "description": "Defines the heatmap mode."
+ }
+ }
}
]
},
diff --git a/editor/code/src/client.ts b/editor/code/src/client.ts
index 302c3842..c600a556 100644
--- a/editor/code/src/client.ts
+++ b/editor/code/src/client.ts
@@ -13,10 +13,17 @@ import {
WorkspaceConfiguration,
Disposable,
languages,
+ Uri,
+ TextEditorVisibleRangesChangeEvent,
} from "vscode";
+import * as vscode from "vscode";
+import * as lsp from "vscode-languageserver-types";
+
import {
BaseLanguageClient,
+ DidChangeConfigurationNotification,
+ DidChangeConfigurationParams,
LanguageClientOptions,
NotificationType,
RequestType,
@@ -30,14 +37,43 @@ import {
GoalRequest,
GoalAnswer,
PpString,
+ DocumentPerfParams,
+ ViewRangeParams,
} from "../lib/types";
+
+import {
+ CoqLanguageStatus,
+ defaultVersion,
+ defaultStatus,
+ coqServerVersion,
+ coqServerStatus,
+} from "./status";
import { CoqLspClientConfig, CoqLspServerConfig, CoqSelector } from "./config";
import { InfoPanel, goalReq } from "./goals";
import { FileProgressManager } from "./progress";
import { coqPerfData, PerfDataView } from "./perf";
-import { sentenceNext, sentenceBack } from "./edit";
+import { sentenceNext, sentencePrevious } from "./edit";
+import { HeatMap, HeatMapConfig } from "./heatmap";
+import { debounce, throttle } from "throttle-debounce";
+
+// Convert perf data to VSCode format
+function toVsCodePerf(
+ p: DocumentPerfParams
+): DocumentPerfParams {
+ let textDocument = p.textDocument;
+ let summary = p.summary;
+ let timings = p.timings.map((t) => {
+ return {
+ range: client.protocol2CodeConverter.asRange(t.range),
+ info: t.info,
+ };
+ });
+ return { textDocument, summary, timings };
+}
let config: CoqLspClientConfig;
+let serverConfig: CoqLspServerConfig;
+
let client: BaseLanguageClient;
// Lifetime of the info panel == extension lifetime.
@@ -49,10 +85,17 @@ let fileProgress: FileProgressManager;
// Status Bar Button
let lspStatusItem: StatusBarItem;
+// Language Status Indicators
+let languageStatus: CoqLanguageStatus;
+let languageVersionHook: Disposable;
+let languageStatusHook: Disposable;
+
// Lifetime of the perf data setup == client lifetime for the hook, extension for the webview
let perfDataView: PerfDataView;
let perfDataHook: Disposable;
+let heatMap: HeatMap;
+
// Client Factory types
export type ClientFactoryType = (
context: ExtensionContext,
@@ -79,19 +122,32 @@ export function activateCoqLSP(
): CoqLspAPI {
window.showInformationMessage("Coq LSP Extension: Going to activate!");
- workspace.onDidChangeConfiguration((cfgChange) => {
- if (cfgChange.affectsConfiguration("coq-lsp")) {
- // Refactor to remove the duplicate call below
- const wsConfig = workspace.getConfiguration("coq-lsp");
- config = CoqLspClientConfig.create(wsConfig);
+ // Update config on client and server
+ function configDidChange(wsConfig: any): CoqLspServerConfig {
+ config = CoqLspClientConfig.create(wsConfig);
+ let client_version = context.extension.packageJSON.version;
+ let settings = CoqLspServerConfig.create(client_version, wsConfig);
+ let params: DidChangeConfigurationParams = { settings };
+
+ if (client && client.isRunning()) {
+ let type = DidChangeConfigurationNotification.type;
+ client.sendNotification(type, params);
}
- });
- function coqCommand(command: string, fn: () => void) {
+ // Store setting on the server for local use
+ serverConfig = settings;
+ return settings;
+ }
+
+ function coqCommand(
+ command: string,
+ fn: (...args: any[]) => void | Promise
+ ) {
let disposable = commands.registerCommand("coq-lsp." + command, fn);
context.subscriptions.push(disposable);
}
function coqEditorCommand(command: string, fn: (editor: TextEditor) => void) {
+ // EJGA: we should check for document selector here.
let disposable = commands.registerTextEditorCommand(
"coq-lsp." + command,
fn
@@ -125,30 +181,30 @@ export function activateCoqLSP(
...fexc,
});
}
+
const stop = () => {
if (client && client.isRunning()) {
- client
+ return client
.dispose(2000)
- .then(updateStatusBar)
- .then(() => {
+ .finally(updateStatusBar)
+ .finally(() => {
infoPanel.dispose();
fileProgress.dispose();
perfDataHook.dispose();
+ heatMap.dispose();
+ languageVersionHook.dispose();
+ languageStatusHook.dispose();
});
- }
+ } else return Promise.resolve();
};
const start = () => {
if (client && client.isRunning()) return;
const wsConfig = workspace.getConfiguration("coq-lsp");
- config = CoqLspClientConfig.create(wsConfig);
- let client_version = context.extension.packageJSON.version;
- const initializationOptions = CoqLspServerConfig.create(
- client_version,
- wsConfig
- );
+ // This also sets `config` variable
+ const initializationOptions: CoqLspServerConfig = configDidChange(wsConfig);
const clientOptions: LanguageClientOptions = {
documentSelector: CoqSelector.local,
@@ -163,39 +219,63 @@ export function activateCoqLSP(
fileProgress = new FileProgressManager(client);
perfDataHook = client.onNotification(coqPerfData, (data) => {
perfDataView.update(data);
+ heatMap.update(toVsCodePerf(data));
+ });
+
+ languageVersionHook = client.onNotification(coqServerVersion, (data) => {
+ languageStatus.updateVersion(data);
});
+
+ languageStatusHook = client.onNotification(coqServerStatus, (data) => {
+ languageStatus.updateStatus(data, serverConfig.check_only_on_request);
+ });
+
resolve(client);
});
- cP.then((client) =>
- client
- .start()
- .then(updateStatusBar)
- .then(() => {
- if (window.activeTextEditor) {
- goalsCall(
- window.activeTextEditor,
- TextEditorSelectionChangeKind.Command
- );
- }
- })
- ).catch((error) => {
- let emsg = error.toString();
- console.log(`Error in coq-lsp start: ${emsg}`);
- setFailedStatuBar(emsg);
- });
+ return cP
+ .then((client) =>
+ client
+ .start()
+ .then(updateStatusBar)
+ .then(() => {
+ if (window.activeTextEditor) {
+ goalsCall(
+ window.activeTextEditor,
+ TextEditorSelectionChangeKind.Command
+ );
+ }
+ })
+ )
+ .catch((error) => {
+ let emsg = error.toString();
+ console.log(`Error in coq-lsp start: ${emsg}`);
+ setFailedStatusBar(emsg);
+ });
};
- const restart = () => {
- stop();
- start();
+ const restart = async () => {
+ await stop().finally(start);
};
- const toggle = () => {
- if (client && client.isRunning()) {
- stop();
+ const toggle_lazy_checking = async () => {
+ let wsConfig = workspace.getConfiguration();
+ let newValue = !wsConfig.get("coq-lsp.check_only_on_request");
+ await wsConfig.update("coq-lsp.check_only_on_request", newValue);
+ languageStatus.updateStatus({ status: "Idle", mem: "" }, newValue);
+ };
+
+ // switches between the different status of the server
+ const toggle = async () => {
+ if (client && client.isRunning() && !serverConfig.check_only_on_request) {
+ // Server on, and in continous mode, set lazy
+ await toggle_lazy_checking().then(updateStatusBar);
+ } else if (client && client.isRunning()) {
+ // Server on, and in lazy mode, stop
+ await stop();
} else {
- start();
+ // Server is off, set continous mode and start
+ await toggle_lazy_checking().then(start);
}
};
@@ -204,15 +284,31 @@ export function activateCoqLSP(
// Check VSCoq is not installed
checkForVSCoq();
+ // Config change events setup
+ let onDidChange = workspace.onDidChangeConfiguration((cfgChange) => {
+ if (cfgChange.affectsConfiguration("coq-lsp")) {
+ // Refactor to remove the duplicate call below
+ const wsConfig = workspace.getConfiguration("coq-lsp");
+ configDidChange(wsConfig);
+ }
+ });
+
+ context.subscriptions.push(onDidChange);
+
// InfoPanel setup.
infoPanel = new InfoPanel(context.extensionUri);
context.subscriptions.push(infoPanel);
const goals = (editor: TextEditor) => {
if (!client.isRunning()) return;
- let uri = editor.document.uri;
+ let uri = editor.document.uri.toString();
let version = editor.document.version;
- let position = editor.selection.active;
+
+ // XXX: EJGA: For some reason TS doesn't catch the typing error here,
+ // beware, because this creates many problems once out of the standard
+ // Node-base Language client and object are serialized!
+ let cursor = editor.selection.active;
+ let position = client.code2ProtocolConverter.asPosition(cursor);
infoPanel.updateFromServer(
client,
uri,
@@ -264,6 +360,40 @@ export function activateCoqLSP(
context.subscriptions.push(goalsHook);
+ const viewRangeNotification = new NotificationType(
+ "coq/viewRange"
+ );
+
+ let viewRangeHook = window.onDidChangeTextEditorVisibleRanges(
+ throttle(400, (evt: TextEditorVisibleRangesChangeEvent) => {
+ if (
+ config.check_on_scroll &&
+ serverConfig.check_only_on_request &&
+ languages.match(CoqSelector.local, evt.textEditor.document) > 0 &&
+ evt.visibleRanges[0]
+ ) {
+ let uri = evt.textEditor.document.uri.toString();
+ let version = evt.textEditor.document.version;
+ let textDocument = { uri, version };
+ let range = client.code2ProtocolConverter.asRange(evt.visibleRanges[0]);
+ let params: ViewRangeParams = { textDocument, range };
+ client.sendNotification(viewRangeNotification, params);
+ }
+ })
+ );
+
+ context.subscriptions.push(viewRangeHook);
+
+ // Heatmap setup
+ heatMap = new HeatMap(
+ workspace.getConfiguration("coq-lsp").get("heatmap") as HeatMapConfig
+ );
+
+ const heatMapToggle = () => {
+ heatMap.toggle();
+ };
+
+ // Document request setup
const docReq = new RequestType(
"coq/getDocument"
);
@@ -276,15 +406,31 @@ export function activateCoqLSP(
version
);
let params: FlecheDocumentParams = { textDocument };
- client.sendRequest(docReq, params).then((fd) => console.log(fd));
+ client.sendRequest(docReq, params).then((fd) => {
+ // EJGA: uri_result could be used to set the suggested save path
+ // for the new editor, however we need to see how to do that
+ // and set `content` too for the new editor.
+ let path = `${uri.fsPath}-${version}.json`;
+ let uri_result = Uri.file(path).with({ scheme: "untitled" });
+
+ let open_options = {
+ language: "json",
+ content: JSON.stringify(fd, null, 2),
+ };
+ workspace.openTextDocument(open_options).then((document) => {
+ window.showTextDocument(document);
+ });
+ });
};
+ // Trim notification setup
const trimNot = new NotificationType<{}>("coq/trimCaches");
const cacheTrim = () => {
client.sendNotification(trimNot, {});
};
+ // Save request setup
const saveReq = new RequestType(
"coq/saveVo"
);
@@ -319,13 +465,22 @@ export function activateCoqLSP(
context.subscriptions.push(lspStatusItem);
};
+ // This stuff should likely go in the CoqLSP client class
+ languageStatus = new CoqLanguageStatus(defaultVersion, defaultStatus, false);
+
// Ali notes about the status item text: we should keep it short
// We violate this on the error case, but only because it is exceptional.
const updateStatusBar = () => {
if (client && client.isRunning()) {
- lspStatusItem.text = "$(check) coq-lsp (running)";
- lspStatusItem.backgroundColor = undefined;
- lspStatusItem.tooltip = "coq-lsp is running. Click to disable.";
+ if (serverConfig.check_only_on_request) {
+ lspStatusItem.text = "$(check) coq-lsp (on-demand checking)";
+ lspStatusItem.backgroundColor = undefined;
+ lspStatusItem.tooltip = "coq-lsp is running. Click to disable.";
+ } else {
+ lspStatusItem.text = "$(check) coq-lsp (continous checking)";
+ lspStatusItem.backgroundColor = undefined;
+ lspStatusItem.tooltip = "coq-lsp is running. Click to disable.";
+ }
} else {
lspStatusItem.text = "$(circle-slash) coq-lsp (stopped)";
lspStatusItem.backgroundColor = new ThemeColor(
@@ -335,7 +490,7 @@ export function activateCoqLSP(
}
};
- const setFailedStatuBar = (emsg: string) => {
+ const setFailedStatusBar = (emsg: string) => {
lspStatusItem.text = "$(circle-slash) coq-lsp (failed to start)";
lspStatusItem.backgroundColor = new ThemeColor(
"statusBarItem.errorBackground"
@@ -353,12 +508,16 @@ export function activateCoqLSP(
coqCommand("toggle", toggle);
coqCommand("trim", cacheTrim);
+ coqCommand("toggle_mode", toggle_lazy_checking);
+
coqEditorCommand("goals", goals);
coqEditorCommand("document", getDocument);
coqEditorCommand("save", saveDocument);
coqEditorCommand("sentenceNext", sentenceNext);
- coqEditorCommand("sentenceBack", sentenceBack);
+ coqEditorCommand("sentencePrevious", sentencePrevious);
+
+ coqEditorCommand("heatmap.toggle", heatMapToggle);
createEnableButton();
diff --git a/editor/code/src/config.ts b/editor/code/src/config.ts
index 68614e57..21f4821f 100644
--- a/editor/code/src/config.ts
+++ b/editor/code/src/config.ts
@@ -14,6 +14,7 @@ export interface CoqLspServerConfig {
show_stats_on_hover: boolean;
show_loc_info_on_hover: boolean;
check_only_on_request: boolean;
+ send_perf_data: boolean;
}
export namespace CoqLspServerConfig {
@@ -35,6 +36,7 @@ export namespace CoqLspServerConfig {
show_stats_on_hover: wsConfig.show_stats_on_hover,
show_loc_info_on_hover: wsConfig.show_loc_info_on_hover,
check_only_on_request: wsConfig.check_only_on_request,
+ send_perf_data: wsConfig.send_perf_data,
};
}
}
@@ -49,6 +51,7 @@ export enum ShowGoalsOnCursorChange {
export interface CoqLspClientConfig {
show_goals_on: ShowGoalsOnCursorChange;
pp_format: "Pp" | "Str";
+ check_on_scroll: boolean;
}
function pp_type_to_pp_format(pp_type: 0 | 1 | 2): "Pp" | "Str" {
@@ -67,6 +70,7 @@ export namespace CoqLspClientConfig {
let obj: CoqLspClientConfig = {
show_goals_on: wsConfig.show_goals_on,
pp_format: pp_type_to_pp_format(wsConfig.pp_type),
+ check_on_scroll: wsConfig.check_on_scroll,
};
return obj;
}
@@ -77,6 +81,8 @@ export namespace CoqSelector {
export const all: TextDocumentFilter[] = [
{ language: "coq" },
{ language: "markdown", pattern: "**/*.mv" },
+ { language: "latex", pattern: "**/*.lv" },
+ { language: "latex", pattern: "**/*.v.tex" },
];
// Local Coq files, suitable for interaction with a local server
diff --git a/editor/code/src/edit.ts b/editor/code/src/edit.ts
index e01c64a2..c537a6ec 100644
--- a/editor/code/src/edit.ts
+++ b/editor/code/src/edit.ts
@@ -1,7 +1,23 @@
// Edition facilities for Coq files
-import { TextEditor, Position, Range, Selection } from "vscode";
+import {
+ TextEditor,
+ Position,
+ Range,
+ Selection,
+ TextEditorRevealType,
+} from "vscode";
-export function sentenceBack(editor: TextEditor) {
+function setSelection(editor: TextEditor, newCursor: Position) {
+ editor.selection = new Selection(newCursor, newCursor);
+
+ // Is there not a better way?
+ editor.revealRange(
+ new Range(newCursor, newCursor),
+ TextEditorRevealType.InCenterIfOutsideViewport
+ );
+}
+
+export function sentencePrevious(editor: TextEditor) {
// Slice from the beginning of the document
let cursor = editor.selection.active;
let range = new Range(editor.document.positionAt(0), cursor);
@@ -19,7 +35,7 @@ export function sentenceBack(editor: TextEditor) {
index = text.lastIndexOf(match) + match.length;
}
let newCursor = editor.document.positionAt(index);
- editor.selection = new Selection(newCursor, newCursor);
+ setSelection(editor, newCursor);
}
}
@@ -40,6 +56,6 @@ export function sentenceNext(editor: TextEditor) {
let newCursor = editor.document.positionAt(
editor.document.offsetAt(cursor) + index
);
- editor.selection = new Selection(newCursor, newCursor);
+ setSelection(editor, newCursor);
}
}
diff --git a/editor/code/src/goals.ts b/editor/code/src/goals.ts
index c5c81184..7a085b1b 100644
--- a/editor/code/src/goals.ts
+++ b/editor/code/src/goals.ts
@@ -1,5 +1,4 @@
import {
- Position,
Uri,
WebviewPanel,
window,
@@ -11,6 +10,7 @@ import {
import {
BaseLanguageClient,
RequestType,
+ ResponseError,
VersionedTextDocumentIdentifier,
} from "vscode-languageclient";
import {
@@ -21,6 +21,12 @@ import {
ErrorData,
} from "../lib/types";
+import {
+ URI,
+ Position,
+ TextDocumentIdentifier,
+} from "vscode-languageserver-types";
+
export const goalReq = new RequestType, void>(
"proof/goals"
);
@@ -91,6 +97,10 @@ export class InfoPanel {
ensurePanel() {
if (!this.panel) {
this.panelFactory();
+ } else {
+ if (!this.panel.visible) {
+ this.panel.reveal(2, true);
+ }
}
}
postMessage({ method, params }: CoqMessagePayload) {
@@ -127,7 +137,12 @@ export class InfoPanel {
this.requestSent(params);
client.sendRequest(goalReq, params).then(
(goals) => this.requestDisplay(goals),
- (reason) => this.requestError(reason)
+ (error: ResponseError) => {
+ let textDocument = params.textDocument;
+ let position = params.position;
+ let message = error.message;
+ this.requestError({ textDocument, position, message });
+ }
);
}
@@ -139,22 +154,27 @@ export class InfoPanel {
let goals_fn = goals as GoalAnswer;
this.listeners.forEach((fn) => fn(goals_fn));
},
- // We should actually provide a better setup so we can pass the rejection of the promise to our clients, YMMV tho.
- (reason) => this.requestError(reason)
+ // We should actually provide a better setup so we can pass
+ // the rejection of the promise to our clients, YMMV tho.
+ (error: ResponseError) => {
+ let textDocument = params.textDocument;
+ let position = params.position;
+ let message = error.message;
+ this.requestError({ textDocument, position, message });
+ }
);
}
}
+
+ // Protocol-level data
updateFromServer(
client: BaseLanguageClient,
- uri: Uri,
+ uri: URI,
version: number,
position: Position,
pp_format: "Pp" | "Str"
) {
- let textDocument = VersionedTextDocumentIdentifier.create(
- uri.toString(),
- version
- );
+ let textDocument = VersionedTextDocumentIdentifier.create(uri, version);
// Example to test the `command` parameter
// let command = "idtac.";
diff --git a/editor/code/src/heatmap.ts b/editor/code/src/heatmap.ts
new file mode 100644
index 00000000..49746746
--- /dev/null
+++ b/editor/code/src/heatmap.ts
@@ -0,0 +1,178 @@
+import {
+ window,
+ TextEditorDecorationType,
+ Range,
+ Disposable,
+ TextEditor,
+ languages,
+ workspace,
+ ConfigurationChangeEvent,
+} from "vscode";
+
+import { CoqSelector } from "./config";
+import { DocumentPerfParams, SentencePerfParams } from "../lib/types";
+
+export interface HeatMapConfig {
+ enabled: boolean;
+ heatLevels: number;
+ warmColor: string;
+ coldColor: string;
+ perfDataType: "time" | "memory";
+}
+
+export class HeatMap {
+ private enabled: boolean = false;
+ private subscriptions: Disposable[] = [];
+ private heatStyles: TextEditorDecorationType[] = [];
+ private perfData: DocumentPerfParams | undefined = undefined;
+ private perfDataType: "time" | "memory" = "time";
+
+ constructor(config: HeatMapConfig) {
+ this.enabled = config.enabled;
+ this.apply_config(config);
+ this.subscriptions.push(this.listenToConfigChanges());
+ }
+
+ dispose() {
+ this.subscriptions.forEach((subscription) => subscription.dispose());
+ this.heatStyles.forEach((style) => style.dispose());
+ }
+
+ toggle() {
+ this.enabled = !this.enabled;
+ if (this.enabled) {
+ this.draw(window.activeTextEditor);
+ } else {
+ this.clearHeatMap();
+ }
+ }
+
+ draw(editor?: TextEditor) {
+ if (
+ !editor ||
+ !this.enabled ||
+ languages.match(CoqSelector.local, editor.document) === 0 ||
+ this.perfData === undefined
+ ) {
+ return;
+ }
+
+ const perfData = this.perfData;
+
+ const getData = (perf: SentencePerfParams) => {
+ if (this.perfDataType === "time") {
+ return perf.info.time;
+ } else if (this.perfDataType === "memory") {
+ return perf.info.memory;
+ } else {
+ throw new Error("Unknown perfDataType");
+ }
+ };
+
+ const dataPoints = perfData.timings.map(getData);
+ const minData = Math.min(...dataPoints);
+ const maxData = Math.max(...dataPoints);
+ const dataRange = maxData - minData;
+
+ if (dataRange === 0) {
+ return;
+ }
+
+ const dataPerLevel = dataRange / this.heatStyles.length;
+ const ranges: Range[][] = new Array(this.heatStyles.length)
+ .fill(null)
+ .map(() => []);
+
+ this.perfData.timings.forEach((perf) => {
+ const dataPoint = getData(perf);
+ const bucket = Math.min(
+ this.heatStyles.length - 1,
+ Math.floor((dataPoint - minData) / dataPerLevel)
+ );
+ ranges[bucket].push(perf.range);
+ });
+
+ this.heatStyles.forEach((style, i) =>
+ editor.setDecorations(style, ranges[i])
+ );
+ }
+
+ private clearHeatMap() {
+ const editor = window.activeTextEditor;
+ if (editor) {
+ this.heatStyles.forEach((style) => editor.setDecorations(style, []));
+ }
+ }
+
+ private listenToConfigChanges() {
+ return workspace.onDidChangeConfiguration(
+ (event: ConfigurationChangeEvent) => {
+ if (event.affectsConfiguration("coq-lsp.heatmap")) {
+ let config = workspace
+ .getConfiguration("coq-lsp")
+ .get("heatmap") as HeatMapConfig;
+ this.apply_config(config);
+ this.draw(window.activeTextEditor);
+ }
+ }
+ );
+ }
+
+ private apply_config(config: HeatMapConfig) {
+ // Read settings
+ const heatLevels = config.heatLevels || 100;
+ const warmColor = config.warmColor || "#ff0000";
+ const coldColor = config.coldColor || "#000000";
+ this.perfDataType = config.perfDataType || "time";
+
+ // Use the settings
+ this.updateHeatMapStyles(heatLevels, warmColor, coldColor);
+ }
+
+ private updateHeatMapStyles(
+ heatLevels: number,
+ warmColour: string,
+ coldColour: string
+ ) {
+ this.heatStyles.forEach((style) => style.dispose()); // Clear existing styles
+ this.heatStyles = [];
+
+ const [r1, g1, b1] = this.parseColor(coldColour);
+ const [r2, g2, b2] = this.parseColor(warmColour);
+
+ for (let i = 0; i < heatLevels; i++) {
+ const alphaValue = i / (heatLevels - 1);
+ const r = Math.round(r1 + (r2 - r1) * alphaValue);
+ const g = Math.round(g1 + (g2 - g1) * alphaValue);
+ const b = Math.round(b1 + (b2 - b1) * alphaValue);
+
+ this.heatStyles.push(
+ window.createTextEditorDecorationType({
+ backgroundColor: `rgba(${r}, ${g}, ${b}, ${alphaValue})`,
+ })
+ );
+ }
+ }
+
+ // Parse hexadecimal color
+ private parseColor(color: string): number[] {
+ const hexRegex = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i;
+ const matches = color.match(hexRegex);
+ if (!matches) {
+ console.warn(
+ `Invalid color format: ${color}. Using default color "#ff0000."`
+ );
+ return [0xff, 0, 0]; // Default to red
+ }
+ return [
+ parseInt(matches[1], 16),
+ parseInt(matches[2], 16),
+ parseInt(matches[3], 16),
+ ];
+ }
+
+ update(data: DocumentPerfParams) {
+ this.perfData = data;
+ this.draw(window.activeTextEditor);
+ }
+}
diff --git a/editor/code/src/perf.ts b/editor/code/src/perf.ts
index 34c73f40..c42aa915 100644
--- a/editor/code/src/perf.ts
+++ b/editor/code/src/perf.ts
@@ -6,16 +6,17 @@ import {
WebviewViewResolveContext,
window,
} from "vscode";
+import { Range } from "vscode-languageserver-types";
import { NotificationType } from "vscode-languageclient";
-import { DocumentPerfParams } from "../lib/types";
+import { DocumentPerfParams, PerfMessagePayload } from "../lib/types";
-export const coqPerfData = new NotificationType(
+export const coqPerfData = new NotificationType>(
"$/coq/filePerfData"
);
export class PerfDataView implements Disposable {
private panel: Disposable;
- private updateWebView: (data: DocumentPerfParams) => void = () => {};
+ private updateWebView: (data: DocumentPerfParams) => void = () => {};
constructor(extensionUri: Uri) {
let resolveWebviewView = (
@@ -50,12 +51,14 @@ export class PerfDataView implements Disposable {