diff --git a/CHANGELOG.md b/CHANGELOG.md index ff3ebf727..fe6670165 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Add OCaml onboarding welcome screen. (#1737) - Add `ocaml.server.codelens.forNestedBindings` setting to control whether CodeLens should be displayed for nested bindings. Defaults to `false`, showing CodeLens only for top-level bindings. diff --git a/assets/ocaml_survey.png b/assets/ocaml_survey.png new file mode 100644 index 000000000..53e5e007c Binary files /dev/null and b/assets/ocaml_survey.png differ diff --git a/assets/opam_init.jpg b/assets/opam_init.jpg new file mode 100644 index 000000000..7b9a54edd Binary files /dev/null and b/assets/opam_init.jpg differ diff --git a/assets/opam_install.jpg b/assets/opam_install.jpg new file mode 100644 index 000000000..139b6cd5e Binary files /dev/null and b/assets/opam_install.jpg differ diff --git a/assets/opam_switch.jpg b/assets/opam_switch.jpg new file mode 100644 index 000000000..6c9c4a91b Binary files /dev/null and b/assets/opam_switch.jpg differ diff --git a/assets/utop.png b/assets/utop.png new file mode 100644 index 000000000..f877839b3 Binary files /dev/null and b/assets/utop.png differ diff --git a/assets/vscode-ocaml-commands.gif b/assets/vscode-ocaml-commands.gif new file mode 100644 index 000000000..884113c09 Binary files /dev/null and b/assets/vscode-ocaml-commands.gif differ diff --git a/package.json b/package.json index ec4c315f3..9bd4dad92 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,114 @@ "test": "vscode-test" }, "contributes": { + "walkthroughs": [ + { + "id": "ocaml-onboarding", + "title": "OCaml: Setup opam dev environment (manual)", + "description": "A guided, terminal based, installation of OCaml and its essential tools with the opam package manager.", + "steps": [ + { + "id": "install-opam-windows", + "when": "isWindows", + "title": "Install opam", + "description": "opam is the package manager for OCaml. Installing opam also installs the OCaml compiler. On Windows 10 or 11, install it using Winget:\n\nRun `winget install Git.Git OCaml.opam` in PowerShell.\n\n[Open a terminal and run winget](command:ocaml.install-opam)", + "media": { + "image": "assets/opam_install.jpg", + "altText": "While installing opam, you'll be asked a series of questions: (1) Where should opam be installed? You can press enter and use the default location, unless you wish to install it somehwere else. (2) Sometimes it may request administrator write access, you should enter your password to authorize the installer." + }, + "completionEvents": [ + "onCommand:ocaml.install-opam" + ] + }, + { + "id": "install-opam-mac", + "when": "isMac", + "title": "Install opam", + "description": "opam is the package manager for OCaml. Installing opam also installs the OCaml compiler. Install it via opam's official script installer.\n\n[Open a terminal and run the opam install script](command:ocaml.install-opam)", + "media": { + "image": "assets/opam_install.jpg", + "altText": "While installing opam, you'll be asked a series of questions: (1) Where should opam be installed? You can press enter and use the default location, unless you wish to install it somehwere else. (2) Sometimes it may request administrator write access, you should enter your password to authorize the installer." + }, + "completionEvents": [ + "onCommand:ocaml.install-opam" + ] + }, + { + "id": "install-opam-linux", + "when": "isLinux", + "title": "Install opam", + "description": "opam is the package manager for OCaml. Installing opam also installs the OCaml compiler. Install it via opam's official script installer.\n\n[Open a terminal and run the opam install script](command:ocaml.install-opam)", + "media": { + "image": "assets/opam_install.jpg", + "altText": "While installing opam, you'll be asked a series of questions: (1) Where should opam be installed? You can press enter and use the default location, unless you wish to install it somehwere else. (2) Sometimes it may request administrator write access, you should enter your password to authorize the installer." + }, + "completionEvents": [ + "onCommand:ocaml.install-opam" + ] + }, + { + "id": "init-opam", + "title": "Initialize opam", + "description": "After installing opam, we initialise it so as to prepare your system to use it for managing OCaml packages and compilers.\n\n[Open a terminal and run opam init](command:ocaml.init-opam)", + "media": { + "image": "assets/opam_init.jpg", + "altText": "For the different options: (1) → (recommended) Updates ~/.profile to automatically configure opam for future shell sessions. (2) → Does not update shell configs; requires manual eval $(opam env) after switching. (3) → Allows selecting a different shell (e.g., zsh, fish) for configuration. (4) → Lets you specify a custom config file instead of ~/.profile.5 → No automatic setup; you must manually run eval $(opam env) when needed." + }, + "completionEvents": [ + "onCommand:ocaml.init-opam" + ] + }, + { + "id": "activate-opam-switch", + "title": "Activate the opam switch", + "description": "An opam switch is an isolated OCaml environment (like a Python virtual environment) where you can install different OCaml versions and packages. \n\n[Select a switch and activate it](command:ocaml.select-sandbox)", + "media": { + "image": "assets/opam_switch.jpg", + "altText": "An image of tiny ocaml logos arranged in the shape of a camel" + }, + "completionEvents": [ + "onCommand:ocaml.select-sandbox" + ] + }, + { + "id": "install-platform-tools", + "title": "Install OCaml Platform Tools", + "description": "Now install essential development tools:\n\n- OCaml LSP server enables editor support for OCaml, odoc generates documentation, camlformat automatically formats OCaml code, utop is an interactive REPL for OCaml and dune is the official OCaml build system.\n\n[Install Platform Tools](command:ocaml.install-ocaml-dev)", + "media": { + "image": "assets/vscode-ocaml-commands.gif", + "altText": "A gif of how to access all commands provided by the OCaml VSCode plugin, which can be accessed by pressing Ctrl+Alt+P and typing ocaml" + }, + "completionEvents": [ + "onCommand:ocaml.install-ocaml-dev" + ] + }, + { + "id": "check-installation", + "title": "Check Installation", + "description": "Verify your OCaml installation by running utop. If everything is set up correctly, you should see a prompt like the image on the right. \n\n[Check Installation](command:ocaml.open-utop)", + "media": { + "image": "assets/utop.png", + "altText": "A screenshot showing the output of running utop in a command line" + }, + "completionEvents": [ + "onCommand:ocaml.open-utop" + ] + }, + { + "id": "finish-onboarding", + "title": "Congratulations", + "description": "You now have OCaml installed and setup on your computer. Please take a few minutes to answer this short survey.\n\n[OCaml Survey](https://docs.google.com/forms/d/e/1FAIpQLSfGGFZBiw4PF7L0yt2DBX8443G5_7aFL5v6wvo6p5MwL-DW8Q/viewform?usp=pp_url&entry.454013858=Link+in+the+VSCode+plugin)", + "media": { + "image": "assets/ocaml_survey.png", + "altText": "A screenshot of some of the various companies that use OCaml including JaneStreet, Bloomberg, ahrefs, Tezos, Facebook, Microsoft, Docker" + }, + "completionEvents": [ + "onLink:https://docs.google.com/forms/d/e/1FAIpQLSfGGFZBiw4PF7L0yt2DBX8443G5_7aFL5v6wvo6p5MwL-DW8Q/viewform?usp=pp_url&entry.454013858=Link+in+the+VSCode+plugin" + ] + } + ] + } + ], "breakpoints": [ { "language": "ocaml" @@ -1378,4 +1486,4 @@ "color": "#f29100", "theme": "light" } -} +} \ No newline at end of file diff --git a/src/command_api.ml b/src/command_api.ml index b8f880ef5..964c8a0a9 100644 --- a/src/command_api.ml +++ b/src/command_api.ml @@ -150,6 +150,10 @@ module Internal = struct let augment_selection_type_verbosity = unit_handle "augment-selection-type-verbosity" let install_dune_lsp = unit_handle "install-dune-lsp" let run_dune_pkg_lock = unit_handle "run_dune_pkg_lock" + let install_opam = unit_handle "install-opam" + let init_opam = unit_handle "init-opam" + let install_ocaml_dev = unit_handle "install-ocaml-dev" + let open_utop = unit_handle "open-utop" end module Vscode = struct diff --git a/src/command_api.mli b/src/command_api.mli index ed6ded0f8..039d312de 100644 --- a/src/command_api.mli +++ b/src/command_api.mli @@ -51,6 +51,10 @@ module Internal : sig val augment_selection_type_verbosity : (unit, unit) handle val install_dune_lsp : (unit, unit) handle val run_dune_pkg_lock : (unit, unit) handle + val install_opam : (unit, unit) handle + val init_opam : (unit, unit) handle + val install_ocaml_dev : (unit, unit) handle + val open_utop : (unit, unit) handle end module Vscode : sig diff --git a/src/extension_commands.ml b/src/extension_commands.ml index c1d7357fe..02d31ae52 100644 --- a/src/extension_commands.ml +++ b/src/extension_commands.ml @@ -247,6 +247,233 @@ let _switch_impl_intf = command Command_api.Internal.switch_impl_intf callback ;; +let walkthrough_terminal_instance = ref None + +let walkthrough_terminal instance = + match !walkthrough_terminal_instance with + | Some t -> t + | None -> + let t = + Terminal_sandbox.create + ~name:"OCaml Platform Walkthrough" + (Extension_instance.sandbox instance) + in + walkthrough_terminal_instance := Some t; + t +;; + +let _install_opam = + let outdated_opam = ref false in + let callback (instance : Extension_instance.t) () = + let process_installation () = + let open Promise.Syntax in + let* opam = Opam.make () in + match opam, !outdated_opam with + | None, true | Some _, true | None, false -> + let options = + ProgressOptions.create + ~location:(`ProgressLocation Notification) + ~title:"Installing opam package manager" + ~cancellable:false + () + in + let task ~progress:_ ~token:_ = + let+ result = + match Platform.t with + | Win32 -> + let _ = + let terminal = + Extension_instance.sandbox instance |> Terminal_sandbox.create + in + let _ = Terminal_sandbox.show ~preserveFocus:true terminal in + Terminal_sandbox.send terminal "winget install Git.Git OCaml.opam" + in + Ok () |> Promise.return + | Darwin | Linux | Other -> + let open Promise.Result.Syntax in + let+ _ = + let _ = + let terminal = walkthrough_terminal instance in + let _ = Terminal_sandbox.show ~preserveFocus:true terminal in + Terminal_sandbox.send + terminal + "bash -c \"sh <(curl -fsSL https://opam.ocaml.org/install.sh)\"" + in + Ok () |> Promise.return + in + () + in + match result with + | Ok () -> Ojs.null + | Error err -> + show_message `Error "An error occured while installing opam %s" err; + Ojs.null + in + let+ _ = Vscode.Window.withProgress (module Ojs) ~options ~task in + () + | Some opam, _ -> + let* latest_version = + Cmd.output + ?cwd:(Sandbox.workspace_root ()) + (Cmd.Shell + "curl -s -H \"Accept: application/vnd.github+json\" \ + https://api.github.com/repos/ocaml/opam/releases/latest | jq -r \ + '.tag_name'") + in + let cwd = Sandbox.workspace_root () in + let+ installed_version = Cmd.output ?cwd (Opam.version opam) in + let comb = + Result.combine + latest_version + installed_version + ~ok:(fun lv iv -> + String.compare lv iv + |> Int.is_positive + |> fun is_outdated -> is_outdated, lv, iv) + ~err:(fun e1 e2 -> e1 ^ ", " ^ e2) + in + (match comb with + | Ok (false, _latest_version, installed_version) -> + show_message + `Info + "opam is already installed and up to date (version %s)." + installed_version + | Ok (true, latest_version, installed_version) -> + outdated_opam := true; + let _ = + let+ selection = + Window.showInformationMessage + ~message: + (Printf.sprintf + "opam is already installed (version %s). A newer version (%s) is \ + available." + installed_version + latest_version) + ~choices:[ "Update opam", () ] + () + in + Option.iter selection ~f:(fun () -> + let (_ : unit Promise.t) = + Command_api.(execute Internal.install_opam) () + in + ()) + in + () + | Error err -> show_message `Warn "Could not determine opam version: %s" err) + in + let (_ : unit Promise.t) = process_installation () in + () + in + command Command_api.Internal.install_opam callback +;; + +let _init_opam = + let callback (instance : Extension_instance.t) () = + let options = + ProgressOptions.create + ~location:(`ProgressLocation Notification) + ~title:"Initialising opam" + ~cancellable:false + () + in + let task ~progress:_ ~token:_ = + let open Promise.Syntax in + let+ result = + let open Promise.Result.Syntax in + let+ _ = + let _ = + let terminal = walkthrough_terminal instance in + let _ = Terminal_sandbox.show ~preserveFocus:true terminal in + Terminal_sandbox.send terminal "opam init" + in + Ok () |> Promise.return + in + () + in + match result with + | Ok () -> Ojs.null + | Error err -> + show_message `Error "An error occured while initializing opam %s" err; + Ojs.null + in + let _ = Vscode.Window.withProgress (module Ojs) ~options ~task in + () + in + command Command_api.Internal.init_opam callback +;; + +let _install_ocaml_dev = + let callback (instance : Extension_instance.t) () = + let options = + ProgressOptions.create + ~location:(`ProgressLocation Notification) + ~title:"Installing development packages" + ~cancellable:false + () + in + let task ~progress:_ ~token:_ = + let open Promise.Syntax in + let+ result = + let open Promise.Result.Syntax in + let+ _ = + let _ = + let terminal = walkthrough_terminal instance in + let _ = Terminal_sandbox.show ~preserveFocus:true terminal in + Terminal_sandbox.send + terminal + "opam install ocaml-lsp-server odoc ocamlformat utop" + in + Ok () |> Promise.return + in + () + in + match result with + | Ok () -> Ojs.null + | Error err -> + show_message `Error "An error occured while installing packages %s" err; + Ojs.null + in + let _ = Vscode.Window.withProgress (module Ojs) ~options ~task in + () + in + command Command_api.Internal.install_ocaml_dev callback +;; + +let _open_utop = + let callback (instance : Extension_instance.t) () = + let options = + ProgressOptions.create + ~location:(`ProgressLocation Notification) + ~title:"Launching utop" + ~cancellable:false + () + in + let task ~progress:_ ~token:_ = + let open Promise.Syntax in + let+ result = + let open Promise.Result.Syntax in + let+ _ = + let _ = + let terminal = walkthrough_terminal instance in + let _ = Terminal_sandbox.show ~preserveFocus:true terminal in + Terminal_sandbox.send terminal "opam exec -- utop" + in + Ok () |> Promise.return + in + () + in + match result with + | Ok () -> Ojs.null + | Error err -> + show_message `Error "An error occured while opening utop %s" err; + Ojs.null + in + let _ = Vscode.Window.withProgress (module Ojs) ~options ~task in + () + in + command Command_api.Internal.open_utop callback +;; + let _open_current_dune_file = let callback (_ : Extension_instance.t) () = match Vscode.Window.activeTextEditor () with diff --git a/src/opam.ml b/src/opam.ml index 7af00e080..ce3e6c394 100644 --- a/src/opam.ml +++ b/src/opam.ml @@ -316,6 +316,7 @@ let upgrade ?(packages = []) t switch = ;; let remove t switch packages = spawn t ("remove" :: switch_arg switch :: "-y" :: packages) +let version t = spawn t [ "--version" ] let switch_compiler t switch = let open Promise.Syntax in diff --git a/src/opam.mli b/src/opam.mli index d4ef87a1a..808e84f62 100644 --- a/src/opam.mli +++ b/src/opam.mli @@ -52,6 +52,9 @@ val upgrade : ?packages:string list -> t -> Switch.t -> Cmd.t (* Remove a list of packages from a switch *) val remove : t -> Switch.t -> string list -> Cmd.t +(* Get the version of opam installed *) +val version : t -> Cmd.t + (* Initialize a new Opam environment. *) val init : t -> Cmd.t