From 61293246a6db1de10094b63fdb9a57ba8a62b0cb Mon Sep 17 00:00:00 2001 From: Adam Welc Date: Tue, 28 May 2024 14:28:54 -0700 Subject: [PATCH] [move-ide] Move analyzer versioning (#17929) ## Description We would like to use the same versioning scheme we use for Sui binaries (particularly for `sui` binary) for the `move-analyzer` as well. Both `sui` and `move-analyzer` binaries "bundle" the move compiler and being able to tell which version you are using is very useful (and really necessary to avoid discrepancies in the compilation process between the IDE and CLI). It's apparently not possible to have to `move-analyzer` binaries in the repo (one in `crates` and one in `external-crates`) so I renamed the one in `external-crates` as it's the one in `crates` that is intended to be externally used from now on. ## Test plan All existing tests must pass --- .github/workflows/release.yml | 2 - Cargo.lock | 131 +++++-- Cargo.toml | 3 + binary-build-list.json | 3 +- crates/sui-move-lsp/Cargo.toml | 15 + crates/sui-move-lsp/src/bin/move-analyzer.rs | 22 ++ .../move-analyzer/editors/code/README.md | 2 +- .../move/crates/move-analyzer/src/analyzer.rs | 348 ++++++++++++++++++ .../move-analyzer/src/bin/move-analyzer.rs | 346 +---------------- .../move/crates/move-analyzer/src/lib.rs | 1 + 10 files changed, 493 insertions(+), 380 deletions(-) create mode 100644 crates/sui-move-lsp/Cargo.toml create mode 100644 crates/sui-move-lsp/src/bin/move-analyzer.rs create mode 100644 external-crates/move/crates/move-analyzer/src/analyzer.rs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c0730a695f600..dee1ae1b55155 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -141,7 +141,6 @@ jobs: shell: bash run: | [ -f ~/.cargo/env ] && source ~/.cargo/env ; cargo build --release && cargo build --profile=dev --bin sui - cd external-crates/move && cargo build -p move-analyzer --release - name: Rename binaries for ${{ matrix.os }} if: ${{ env.s3_archive_exist == '' }} @@ -156,7 +155,6 @@ jobs: done mv ./target/debug/sui${{ env.extention }} ${{ env.TMP_BUILD_DIR }}/sui-debug${{ env.extention }} - mv ./external-crates/move/target/release/move-analyzer${{ env.extention }} ${{ env.TMP_BUILD_DIR }}/move-analyzer${{ env.extention }} tar -cvzf ./tmp/sui-${{ env.sui_tag }}-${{ env.os_type }}.tgz -C ${{ env.TMP_BUILD_DIR }} . [[ ${{ env.sui_tag }} == *"testnet"* ]] && aws s3 cp ./tmp/sui-${{ env.sui_tag }}-${{ env.os_type }}.tgz s3://sui-releases/releases/sui-${{ env.sui_tag }}-${{ env.os_type }}.tgz || true diff --git a/Cargo.lock b/Cargo.lock index 90f18802f0a8d..93d219c5c66dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2888,49 +2888,62 @@ dependencies = [ "once_cell", ] +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + [[package]] name = "crossbeam-channel" -version = "0.5.8" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ - "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.2" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" dependencies = [ - "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" -version = "0.9.13" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "autocfg", - "cfg-if", "crossbeam-utils", - "memoffset 0.7.1", - "scopeguard", ] [[package]] -name = "crossbeam-utils" -version = "0.8.14" +name = "crossbeam-queue" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" dependencies = [ - "cfg-if", + "crossbeam-utils", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + [[package]] name = "crossterm" version = "0.25.0" @@ -6266,6 +6279,31 @@ dependencies = [ "hashbrown 0.13.2", ] +[[package]] +name = "lsp-server" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248f65b78f6db5d8e1b1604b4098a28b43d21a8eb1deeca22b1c421b276c7095" +dependencies = [ + "crossbeam-channel", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "lsp-types" +version = "0.95.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e34d33a8e9b006cd3fc4fe69a921affa097bae4bb65f76271f4644f9a334365" +dependencies = [ + "bitflags 1.3.2", + "serde", + "serde_json", + "serde_repr", + "url", +] + [[package]] name = "lz4-sys" version = "1.9.4" @@ -6383,15 +6421,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "memoffset" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" -dependencies = [ - "autocfg", -] - [[package]] name = "miette" version = "7.0.0" @@ -6558,6 +6587,32 @@ dependencies = [ name = "move-abstract-stack" version = "0.0.1" +[[package]] +name = "move-analyzer" +version = "1.0.0" +dependencies = [ + "anyhow", + "clap", + "codespan-reporting", + "crossbeam", + "derivative", + "dunce", + "im", + "itertools 0.10.5", + "lsp-server", + "lsp-types", + "move-command-line-common", + "move-compiler", + "move-ir-types", + "move-package", + "move-symbol-pool", + "serde_json", + "sha2 0.9.9", + "tempfile", + "url", + "vfs", +] + [[package]] name = "move-binary-format" version = "0.0.3" @@ -7941,7 +7996,7 @@ dependencies = [ "cc", "cfg-if", "libc", - "memoffset 0.6.5", + "memoffset", ] [[package]] @@ -10864,9 +10919,9 @@ checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4" [[package]] name = "serde" -version = "1.0.190" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" +checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" dependencies = [ "serde_derive", ] @@ -10903,9 +10958,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.190" +version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" +checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" dependencies = [ "proc-macro2 1.0.78", "quote 1.0.35", @@ -10925,11 +10980,11 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.95" +version = "1.0.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744" +checksum = "4d1bd37ce2324cf3bf85e5a25f96eb4baf0d5aa6eba43e7ae8958870c4ec48ed" dependencies = [ - "indexmap 1.9.3", + "indexmap 2.1.0", "itoa", "ryu", "serde", @@ -13074,6 +13129,16 @@ dependencies = [ "tempfile", ] +[[package]] +name = "sui-move-lsp" +version = "1.27.0" +dependencies = [ + "bin-version", + "clap", + "move-analyzer", + "tokio", +] + [[package]] name = "sui-move-natives-latest" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 31a1615150ec2..2385c7cca3472 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -124,6 +124,7 @@ members = [ "crates/sui-metric-checker", "crates/sui-move", "crates/sui-move-build", + "crates/sui-move-lsp", "crates/sui-network", "crates/sui-node", "crates/sui-open-rpc", @@ -563,6 +564,7 @@ move-stackless-bytecode = { path = "external-crates/move/crates/move-stackless-b move-symbol-pool = { path = "external-crates/move/crates/move-symbol-pool" } move-abstract-interpreter = { path = "external-crates/move/crates/move-abstract-interpreter" } move-abstract-stack = { path = "external-crates/move/crates/move-abstract-stack" } +move-analyzer = { path = "external-crates/move/crates/move-analyzer" } fastcrypto = { git = "https://github.com/MystenLabs/fastcrypto", rev = "c101a5176799db3eb9c801b844e7add92153d291" } fastcrypto-tbls = { git = "https://github.com/MystenLabs/fastcrypto", rev = "c101a5176799db3eb9c801b844e7add92153d291" } @@ -622,6 +624,7 @@ sui-macros = { path = "crates/sui-macros" } sui-metric-checker = { path = "crates/sui-metric-checker" } sui-move = { path = "crates/sui-move" } sui-move-build = { path = "crates/sui-move-build" } +sui-move-lsp = { path = "crates/sui-move-lsp" } sui-network = { path = "crates/sui-network" } sui-node = { path = "crates/sui-node" } sui-open-rpc = { path = "crates/sui-open-rpc" } diff --git a/binary-build-list.json b/binary-build-list.json index 48d5966706d0f..4bb16bb391a6f 100644 --- a/binary-build-list.json +++ b/binary-build-list.json @@ -8,7 +8,8 @@ "sui-data-ingestion", "sui-bridge", "sui-bridge-cli", - "sui-graphql-rpc" + "sui-graphql-rpc", + "move-analyzer" ], "internal_binaries": [ "stress", diff --git a/crates/sui-move-lsp/Cargo.toml b/crates/sui-move-lsp/Cargo.toml new file mode 100644 index 0000000000000..6018fe6c9b528 --- /dev/null +++ b/crates/sui-move-lsp/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "sui-move-lsp" +version.workspace = true +authors = ["Mysten Labs "] +license = "Apache-2.0" +publish = false +edition = "2021" + +[dependencies] +clap = { version = "4.1.4", features = ["derive"] } +tokio = { workspace = true, features = ["full"] } + +bin-version.workspace = true +move-analyzer.workspace = true + diff --git a/crates/sui-move-lsp/src/bin/move-analyzer.rs b/crates/sui-move-lsp/src/bin/move-analyzer.rs new file mode 100644 index 0000000000000..10187b21ca468 --- /dev/null +++ b/crates/sui-move-lsp/src/bin/move-analyzer.rs @@ -0,0 +1,22 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use clap::*; +use move_analyzer::analyzer; + +// Define the `GIT_REVISION` and `VERSION` consts +bin_version::bin_version!(); + +#[derive(Parser)] +#[clap( + name = env!("CARGO_BIN_NAME"), + rename_all = "kebab-case", + author, + version = VERSION, +)] +struct App {} + +fn main() { + App::parse(); + analyzer::run(); +} diff --git a/external-crates/move/crates/move-analyzer/editors/code/README.md b/external-crates/move/crates/move-analyzer/editors/code/README.md index 0992d87e32bb9..985a44420010c 100644 --- a/external-crates/move/crates/move-analyzer/editors/code/README.md +++ b/external-crates/move/crates/move-analyzer/editors/code/README.md @@ -42,7 +42,7 @@ This can be done in two steps: as prerequisites for Sui installation - for Linux, macOS and Windows these prerequisites and their installation instructions can be found [here](https://docs.sui.io/guides/developer/getting-started/sui-install#additional-prerequisites-by-operating-system) -2. Invoke `cargo install --git https://github.com/MystenLabs/sui move-analyzer` to install the +2. Invoke `cargo install --git https://github.com/MystenLabs/sui sui-move-lsp` to install the `move-analyzer` language server in your Cargo binary directory, which is typically located in the `~/.cargo/bin` (macOS/Linux) or `C:\Users\USER\.cargo\bin` (Windows) directory. 3. Copy the move-analyzer binary to `~/.sui/bin` (macOS/Linux) or `C:\Users\USER\.sui\bin` diff --git a/external-crates/move/crates/move-analyzer/src/analyzer.rs b/external-crates/move/crates/move-analyzer/src/analyzer.rs new file mode 100644 index 0000000000000..f34fd8c380a34 --- /dev/null +++ b/external-crates/move/crates/move-analyzer/src/analyzer.rs @@ -0,0 +1,348 @@ +// Copyright (c) The Diem Core Contributors +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +use anyhow::Result; +use crossbeam::channel::{bounded, select}; +use lsp_server::{Connection, Message, Notification, Request, Response}; +use lsp_types::{ + notification::Notification as _, request::Request as _, CompletionOptions, Diagnostic, + HoverProviderCapability, InlayHintOptions, InlayHintServerCapabilities, OneOf, SaveOptions, + TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions, + TypeDefinitionProviderCapability, WorkDoneProgressOptions, +}; +use move_compiler::linters::LintLevel; +use std::{ + collections::BTreeMap, + path::PathBuf, + sync::{Arc, Mutex}, +}; + +use crate::{ + completion::on_completion_request, context::Context, inlay_hints, symbols, + vfs::on_text_document_sync_notification, +}; +use url::Url; +use vfs::{impls::memory::MemoryFS, VfsPath}; + +const LINT_NONE: &str = "none"; +const LINT_DEFAULT: &str = "default"; +const LINT_ALL: &str = "all"; + +#[allow(deprecated)] +pub fn run() { + // stdio is used to communicate Language Server Protocol requests and responses. + // stderr is used for logging (and, when Visual Studio Code is used to communicate with this + // server, it captures this output in a dedicated "output channel"). + let exe = std::env::current_exe() + .unwrap() + .to_string_lossy() + .to_string(); + eprintln!( + "Starting language server '{}' communicating via stdio...", + exe + ); + + let (connection, io_threads) = Connection::stdio(); + let symbols = Arc::new(Mutex::new(symbols::empty_symbols())); + let pkg_deps = Arc::new(Mutex::new( + BTreeMap::::new(), + )); + let ide_files_root: VfsPath = MemoryFS::new().into(); + + let (id, client_response) = connection + .initialize_start() + .expect("could not start connection initialization"); + + let capabilities = serde_json::to_value(lsp_types::ServerCapabilities { + // The server receives notifications from the client as users open, close, + // and modify documents. + text_document_sync: Some(TextDocumentSyncCapability::Options( + TextDocumentSyncOptions { + open_close: Some(true), + // TODO: We request that the language server client send us the entire text of any + // files that are modified. We ought to use the "incremental" sync kind, which would + // have clients only send us what has changed and where, thereby requiring far less + // data be sent "over the wire." However, to do so, our language server would need + // to be capable of applying deltas to its view of the client's open files. See the + // 'move_analyzer::vfs' module for details. + change: Some(TextDocumentSyncKind::FULL), + will_save: None, + will_save_wait_until: None, + save: Some( + SaveOptions { + include_text: Some(true), + } + .into(), + ), + }, + )), + selection_range_provider: None, + hover_provider: Some(HoverProviderCapability::Simple(true)), + // The server provides completions as a user is typing. + completion_provider: Some(CompletionOptions { + resolve_provider: None, + // In Move, `foo::` and `foo.` should trigger completion suggestions for after + // the `:` or `.` + // (Trigger characters are just that: characters, such as `:`, and not sequences of + // characters, such as `::`. So when the language server encounters a completion + // request, it checks whether completions are being requested for `foo:`, and returns no + // completions in that case.) + trigger_characters: Some(vec![":".to_string(), ".".to_string(), "{".to_string()]), + all_commit_characters: None, + work_done_progress_options: WorkDoneProgressOptions { + work_done_progress: None, + }, + completion_item: None, + }), + definition_provider: Some(OneOf::Left(true)), + type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)), + references_provider: Some(OneOf::Left(true)), + document_symbol_provider: Some(OneOf::Left(true)), + inlay_hint_provider: Some(OneOf::Right(InlayHintServerCapabilities::Options( + InlayHintOptions { + work_done_progress_options: WorkDoneProgressOptions { + work_done_progress: None, + }, + resolve_provider: None, + }, + ))), + ..Default::default() + }) + .expect("could not serialize server capabilities"); + + let (diag_sender, diag_receiver) = bounded::>>>(0); + let initialize_params: lsp_types::InitializeParams = + serde_json::from_value(client_response).expect("could not deserialize client capabilities"); + + // determine if linting is on or off based on what the editor requested + let lint = { + let lint_level = initialize_params + .initialization_options + .as_ref() + .and_then(|init_options| init_options.get("lintLevel")) + .and_then(serde_json::Value::as_str) + .unwrap_or(LINT_DEFAULT); + if lint_level == LINT_ALL { + LintLevel::All + } else if lint_level == LINT_NONE { + LintLevel::None + } else { + LintLevel::Default + } + }; + eprintln!("linting level {:?}", lint); + + let symbolicator_runner = symbols::SymbolicatorRunner::new( + ide_files_root.clone(), + symbols.clone(), + pkg_deps.clone(), + diag_sender, + lint, + ); + + // If initialization information from the client contains a path to the directory being + // opened, try to initialize symbols before sending response to the client. Do not bother + // with diagnostics as they will be recomputed whenever the first source file is opened. The + // main reason for this is to enable unit tests that rely on the symbolication information + // to be available right after the client is initialized. + if let Some(uri) = initialize_params.root_uri { + if let Some(p) = symbols::SymbolicatorRunner::root_dir(&uri.to_file_path().unwrap()) { + if let Ok((Some(new_symbols), _)) = symbols::get_symbols( + Arc::new(Mutex::new(BTreeMap::new())), + ide_files_root.clone(), + p.as_path(), + lint, + ) { + let mut old_symbols = symbols.lock().unwrap(); + (*old_symbols).merge(new_symbols); + } + } + } + + let context = Context { + connection, + symbols: symbols.clone(), + inlay_type_hints: initialize_params + .initialization_options + .as_ref() + .and_then(|init_options| init_options.get("inlayHintsType")) + .and_then(serde_json::Value::as_bool) + .unwrap_or_default(), + }; + + eprintln!("inlay type hints enabled: {}", context.inlay_type_hints); + + context + .connection + .initialize_finish( + id, + serde_json::json!({ + "capabilities": capabilities, + }), + ) + .expect("could not finish connection initialization"); + + let mut shutdown_req_received = false; + loop { + select! { + recv(diag_receiver) -> message => { + match message { + Ok(result) => { + match result { + Ok(diags) => { + for (k, v) in diags { + let url = Url::from_file_path(k).unwrap(); + let params = lsp_types::PublishDiagnosticsParams::new(url, v, None); + let notification = Notification::new(lsp_types::notification::PublishDiagnostics::METHOD.to_string(), params); + if let Err(err) = context + .connection + .sender + .send(lsp_server::Message::Notification(notification)) { + eprintln!("could not send diagnostics response: {:?}", err); + }; + } + }, + Err(err) => { + let typ = lsp_types::MessageType::ERROR; + let message = format!("{err}"); + // report missing manifest only once to avoid re-generating + // user-visible error in cases when the developer decides to + // keep editing a file that does not belong to a packages + let params = lsp_types::ShowMessageParams { typ, message }; + let notification = Notification::new(lsp_types::notification::ShowMessage::METHOD.to_string(), params); + if let Err(err) = context + .connection + .sender + .send(lsp_server::Message::Notification(notification)) { + eprintln!("could not send compiler error response: {:?}", err); + }; + }, + } + }, + Err(error) => { + eprintln!("symbolicator message error: {:?}", error); + // if the analyzer crashes in a separate thread, this error will keep + // getting generated for a while unless we explicitly end the process + // obscuring the real logged reason for the crash + std::process::exit(-1); + } + } + }, + recv(context.connection.receiver) -> message => { + match message { + Ok(Message::Request(request)) => { + // the server should not quit after receiving the shutdown request to give itself + // a chance of completing pending requests (but should not accept new requests + // either which is handled inside on_requst) - instead it quits after receiving + // the exit notification from the client, which is handled below + shutdown_req_received = on_request(&context, &request, ide_files_root.clone(), pkg_deps.clone(), shutdown_req_received); + } + Ok(Message::Response(response)) => on_response(&context, &response), + Ok(Message::Notification(notification)) => { + match notification.method.as_str() { + lsp_types::notification::Exit::METHOD => break, + lsp_types::notification::Cancel::METHOD => { + // TODO: Currently the server does not implement request cancellation. + // It ought to, especially once it begins processing requests that may + // take a long time to respond to. + } + _ => on_notification(ide_files_root.clone(), &symbolicator_runner, ¬ification), + } + } + Err(error) => eprintln!("IDE message error: {:?}", error), + } + } + }; + } + + io_threads.join().expect("I/O threads could not finish"); + symbolicator_runner.quit(); + eprintln!("Shut down language server '{}'.", exe); +} + +/// This function returns `true` if shutdown request has been received, and `false` otherwise. +/// The reason why this information is also passed as an argument is that according to the LSP +/// spec, if any additional requests are received after shutdownd then the LSP implementation +/// should respond with a particular type of error. +fn on_request( + context: &Context, + request: &Request, + ide_files_root: VfsPath, + pkg_dependencies: Arc>>, + shutdown_request_received: bool, +) -> bool { + if shutdown_request_received { + let response = lsp_server::Response::new_err( + request.id.clone(), + lsp_server::ErrorCode::InvalidRequest as i32, + "a shutdown request already received by the server".to_string(), + ); + if let Err(err) = context + .connection + .sender + .send(lsp_server::Message::Response(response)) + { + eprintln!("could not send shutdown response: {:?}", err); + } + return true; + } + match request.method.as_str() { + lsp_types::request::Completion::METHOD => { + on_completion_request(context, request, ide_files_root.clone(), pkg_dependencies) + } + lsp_types::request::GotoDefinition::METHOD => { + symbols::on_go_to_def_request(context, request, &context.symbols.lock().unwrap()); + } + lsp_types::request::GotoTypeDefinition::METHOD => { + symbols::on_go_to_type_def_request(context, request, &context.symbols.lock().unwrap()); + } + lsp_types::request::References::METHOD => { + symbols::on_references_request(context, request, &context.symbols.lock().unwrap()); + } + lsp_types::request::HoverRequest::METHOD => { + symbols::on_hover_request(context, request, &context.symbols.lock().unwrap()); + } + lsp_types::request::DocumentSymbolRequest::METHOD => { + symbols::on_document_symbol_request(context, request, &context.symbols.lock().unwrap()); + } + lsp_types::request::InlayHintRequest::METHOD => { + inlay_hints::on_inlay_hint_request(context, request, &context.symbols.lock().unwrap()); + } + lsp_types::request::Shutdown::METHOD => { + eprintln!("Shutdown request received"); + let response = + lsp_server::Response::new_ok(request.id.clone(), serde_json::Value::Null); + if let Err(err) = context + .connection + .sender + .send(lsp_server::Message::Response(response)) + { + eprintln!("could not send shutdown response: {:?}", err); + } + return true; + } + _ => eprintln!("handle request '{}' from client", request.method), + } + false +} + +fn on_response(_context: &Context, _response: &Response) { + eprintln!("handle response from client"); +} + +fn on_notification( + ide_files_root: VfsPath, + symbolicator_runner: &symbols::SymbolicatorRunner, + notification: &Notification, +) { + match notification.method.as_str() { + lsp_types::notification::DidOpenTextDocument::METHOD + | lsp_types::notification::DidChangeTextDocument::METHOD + | lsp_types::notification::DidSaveTextDocument::METHOD + | lsp_types::notification::DidCloseTextDocument::METHOD => { + on_text_document_sync_notification(ide_files_root, symbolicator_runner, notification) + } + _ => eprintln!("handle notification '{}' from client", notification.method), + } +} diff --git a/external-crates/move/crates/move-analyzer/src/bin/move-analyzer.rs b/external-crates/move/crates/move-analyzer/src/bin/move-analyzer.rs index a43a488fd76e2..a65f50131bc0e 100644 --- a/external-crates/move/crates/move-analyzer/src/bin/move-analyzer.rs +++ b/external-crates/move/crates/move-analyzer/src/bin/move-analyzer.rs @@ -1,34 +1,8 @@ -// Copyright (c) The Diem Core Contributors -// Copyright (c) The Move Contributors +// Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -use anyhow::Result; use clap::Parser; -use crossbeam::channel::{bounded, select}; -use lsp_server::{Connection, Message, Notification, Request, Response}; -use lsp_types::{ - notification::Notification as _, request::Request as _, CompletionOptions, Diagnostic, - HoverProviderCapability, InlayHintOptions, InlayHintServerCapabilities, OneOf, SaveOptions, - TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions, - TypeDefinitionProviderCapability, WorkDoneProgressOptions, -}; -use move_compiler::linters::LintLevel; -use std::{ - collections::BTreeMap, - path::PathBuf, - sync::{Arc, Mutex}, -}; - -use move_analyzer::{ - completion::on_completion_request, context::Context, inlay_hints, symbols, - vfs::on_text_document_sync_notification, -}; -use url::Url; -use vfs::{impls::memory::MemoryFS, VfsPath}; - -const LINT_NONE: &str = "none"; -const LINT_DEFAULT: &str = "default"; -const LINT_ALL: &str = "all"; +use move_analyzer::analyzer; #[derive(Parser)] #[clap(author, version, about)] @@ -39,319 +13,5 @@ fn main() { // For now, move-analyzer only responds to options built-in to clap, // such as `--help` or `--version`. Options::parse(); - - // stdio is used to communicate Language Server Protocol requests and responses. - // stderr is used for logging (and, when Visual Studio Code is used to communicate with this - // server, it captures this output in a dedicated "output channel"). - let exe = std::env::current_exe() - .unwrap() - .to_string_lossy() - .to_string(); - eprintln!( - "Starting language server '{}' communicating via stdio...", - exe - ); - - let (connection, io_threads) = Connection::stdio(); - let symbols = Arc::new(Mutex::new(symbols::empty_symbols())); - let pkg_deps = Arc::new(Mutex::new( - BTreeMap::::new(), - )); - let ide_files_root: VfsPath = MemoryFS::new().into(); - - let (id, client_response) = connection - .initialize_start() - .expect("could not start connection initialization"); - - let capabilities = serde_json::to_value(lsp_types::ServerCapabilities { - // The server receives notifications from the client as users open, close, - // and modify documents. - text_document_sync: Some(TextDocumentSyncCapability::Options( - TextDocumentSyncOptions { - open_close: Some(true), - // TODO: We request that the language server client send us the entire text of any - // files that are modified. We ought to use the "incremental" sync kind, which would - // have clients only send us what has changed and where, thereby requiring far less - // data be sent "over the wire." However, to do so, our language server would need - // to be capable of applying deltas to its view of the client's open files. See the - // 'move_analyzer::vfs' module for details. - change: Some(TextDocumentSyncKind::FULL), - will_save: None, - will_save_wait_until: None, - save: Some( - SaveOptions { - include_text: Some(true), - } - .into(), - ), - }, - )), - selection_range_provider: None, - hover_provider: Some(HoverProviderCapability::Simple(true)), - // The server provides completions as a user is typing. - completion_provider: Some(CompletionOptions { - resolve_provider: None, - // In Move, `foo::` and `foo.` should trigger completion suggestions for after - // the `:` or `.` - // (Trigger characters are just that: characters, such as `:`, and not sequences of - // characters, such as `::`. So when the language server encounters a completion - // request, it checks whether completions are being requested for `foo:`, and returns no - // completions in that case.) - trigger_characters: Some(vec![":".to_string(), ".".to_string(), "{".to_string()]), - all_commit_characters: None, - work_done_progress_options: WorkDoneProgressOptions { - work_done_progress: None, - }, - completion_item: None, - }), - definition_provider: Some(OneOf::Left(true)), - type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)), - references_provider: Some(OneOf::Left(true)), - document_symbol_provider: Some(OneOf::Left(true)), - inlay_hint_provider: Some(OneOf::Right(InlayHintServerCapabilities::Options( - InlayHintOptions { - work_done_progress_options: WorkDoneProgressOptions { - work_done_progress: None, - }, - resolve_provider: None, - }, - ))), - ..Default::default() - }) - .expect("could not serialize server capabilities"); - - let (diag_sender, diag_receiver) = bounded::>>>(0); - let initialize_params: lsp_types::InitializeParams = - serde_json::from_value(client_response).expect("could not deserialize client capabilities"); - - // determine if linting is on or off based on what the editor requested - let lint = { - let lint_level = initialize_params - .initialization_options - .as_ref() - .and_then(|init_options| init_options.get("lintLevel")) - .and_then(serde_json::Value::as_str) - .unwrap_or(LINT_DEFAULT); - if lint_level == LINT_ALL { - LintLevel::All - } else if lint_level == LINT_NONE { - LintLevel::None - } else { - LintLevel::Default - } - }; - eprintln!("linting level {:?}", lint); - - let symbolicator_runner = symbols::SymbolicatorRunner::new( - ide_files_root.clone(), - symbols.clone(), - pkg_deps.clone(), - diag_sender, - lint, - ); - - // If initialization information from the client contains a path to the directory being - // opened, try to initialize symbols before sending response to the client. Do not bother - // with diagnostics as they will be recomputed whenever the first source file is opened. The - // main reason for this is to enable unit tests that rely on the symbolication information - // to be available right after the client is initialized. - if let Some(uri) = initialize_params.root_uri { - if let Some(p) = symbols::SymbolicatorRunner::root_dir(&uri.to_file_path().unwrap()) { - if let Ok((Some(new_symbols), _)) = symbols::get_symbols( - Arc::new(Mutex::new(BTreeMap::new())), - ide_files_root.clone(), - p.as_path(), - lint, - ) { - let mut old_symbols = symbols.lock().unwrap(); - (*old_symbols).merge(new_symbols); - } - } - } - - let context = Context { - connection, - symbols: symbols.clone(), - inlay_type_hints: initialize_params - .initialization_options - .as_ref() - .and_then(|init_options| init_options.get("inlayHintsType")) - .and_then(serde_json::Value::as_bool) - .unwrap_or_default(), - }; - - eprintln!("inlay type hints enabled: {}", context.inlay_type_hints); - - context - .connection - .initialize_finish( - id, - serde_json::json!({ - "capabilities": capabilities, - }), - ) - .expect("could not finish connection initialization"); - - let mut shutdown_req_received = false; - loop { - select! { - recv(diag_receiver) -> message => { - match message { - Ok(result) => { - match result { - Ok(diags) => { - for (k, v) in diags { - let url = Url::from_file_path(k).unwrap(); - let params = lsp_types::PublishDiagnosticsParams::new(url, v, None); - let notification = Notification::new(lsp_types::notification::PublishDiagnostics::METHOD.to_string(), params); - if let Err(err) = context - .connection - .sender - .send(lsp_server::Message::Notification(notification)) { - eprintln!("could not send diagnostics response: {:?}", err); - }; - } - }, - Err(err) => { - let typ = lsp_types::MessageType::ERROR; - let message = format!("{err}"); - // report missing manifest only once to avoid re-generating - // user-visible error in cases when the developer decides to - // keep editing a file that does not belong to a packages - let params = lsp_types::ShowMessageParams { typ, message }; - let notification = Notification::new(lsp_types::notification::ShowMessage::METHOD.to_string(), params); - if let Err(err) = context - .connection - .sender - .send(lsp_server::Message::Notification(notification)) { - eprintln!("could not send compiler error response: {:?}", err); - }; - }, - } - }, - Err(error) => { - eprintln!("symbolicator message error: {:?}", error); - // if the analyzer crashes in a separate thread, this error will keep - // getting generated for a while unless we explicitly end the process - // obscuring the real logged reason for the crash - std::process::exit(-1); - } - } - }, - recv(context.connection.receiver) -> message => { - match message { - Ok(Message::Request(request)) => { - // the server should not quit after receiving the shutdown request to give itself - // a chance of completing pending requests (but should not accept new requests - // either which is handled inside on_requst) - instead it quits after receiving - // the exit notification from the client, which is handled below - shutdown_req_received = on_request(&context, &request, ide_files_root.clone(), pkg_deps.clone(), shutdown_req_received); - } - Ok(Message::Response(response)) => on_response(&context, &response), - Ok(Message::Notification(notification)) => { - match notification.method.as_str() { - lsp_types::notification::Exit::METHOD => break, - lsp_types::notification::Cancel::METHOD => { - // TODO: Currently the server does not implement request cancellation. - // It ought to, especially once it begins processing requests that may - // take a long time to respond to. - } - _ => on_notification(ide_files_root.clone(), &symbolicator_runner, ¬ification), - } - } - Err(error) => eprintln!("IDE message error: {:?}", error), - } - } - }; - } - - io_threads.join().expect("I/O threads could not finish"); - symbolicator_runner.quit(); - eprintln!("Shut down language server '{}'.", exe); -} - -/// This function returns `true` if shutdown request has been received, and `false` otherwise. -/// The reason why this information is also passed as an argument is that according to the LSP -/// spec, if any additional requests are received after shutdownd then the LSP implementation -/// should respond with a particular type of error. -fn on_request( - context: &Context, - request: &Request, - ide_files_root: VfsPath, - pkg_dependencies: Arc>>, - shutdown_request_received: bool, -) -> bool { - if shutdown_request_received { - let response = lsp_server::Response::new_err( - request.id.clone(), - lsp_server::ErrorCode::InvalidRequest as i32, - "a shutdown request already received by the server".to_string(), - ); - if let Err(err) = context - .connection - .sender - .send(lsp_server::Message::Response(response)) - { - eprintln!("could not send shutdown response: {:?}", err); - } - return true; - } - match request.method.as_str() { - lsp_types::request::Completion::METHOD => { - on_completion_request(context, request, ide_files_root.clone(), pkg_dependencies) - } - lsp_types::request::GotoDefinition::METHOD => { - symbols::on_go_to_def_request(context, request, &context.symbols.lock().unwrap()); - } - lsp_types::request::GotoTypeDefinition::METHOD => { - symbols::on_go_to_type_def_request(context, request, &context.symbols.lock().unwrap()); - } - lsp_types::request::References::METHOD => { - symbols::on_references_request(context, request, &context.symbols.lock().unwrap()); - } - lsp_types::request::HoverRequest::METHOD => { - symbols::on_hover_request(context, request, &context.symbols.lock().unwrap()); - } - lsp_types::request::DocumentSymbolRequest::METHOD => { - symbols::on_document_symbol_request(context, request, &context.symbols.lock().unwrap()); - } - lsp_types::request::InlayHintRequest::METHOD => { - inlay_hints::on_inlay_hint_request(context, request, &context.symbols.lock().unwrap()); - } - lsp_types::request::Shutdown::METHOD => { - eprintln!("Shutdown request received"); - let response = - lsp_server::Response::new_ok(request.id.clone(), serde_json::Value::Null); - if let Err(err) = context - .connection - .sender - .send(lsp_server::Message::Response(response)) - { - eprintln!("could not send shutdown response: {:?}", err); - } - return true; - } - _ => eprintln!("handle request '{}' from client", request.method), - } - false -} - -fn on_response(_context: &Context, _response: &Response) { - eprintln!("handle response from client"); -} - -fn on_notification( - ide_files_root: VfsPath, - symbolicator_runner: &symbols::SymbolicatorRunner, - notification: &Notification, -) { - match notification.method.as_str() { - lsp_types::notification::DidOpenTextDocument::METHOD - | lsp_types::notification::DidChangeTextDocument::METHOD - | lsp_types::notification::DidSaveTextDocument::METHOD - | lsp_types::notification::DidCloseTextDocument::METHOD => { - on_text_document_sync_notification(ide_files_root, symbolicator_runner, notification) - } - _ => eprintln!("handle notification '{}' from client", notification.method), - } + analyzer::run(); } diff --git a/external-crates/move/crates/move-analyzer/src/lib.rs b/external-crates/move/crates/move-analyzer/src/lib.rs index 55a3d5db016d0..15082886df6a0 100644 --- a/external-crates/move/crates/move-analyzer/src/lib.rs +++ b/external-crates/move/crates/move-analyzer/src/lib.rs @@ -5,6 +5,7 @@ #[macro_use(sp)] extern crate move_ir_types; +pub mod analyzer; pub mod completion; pub mod context; pub mod diagnostics;