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;