pub use crate::error::DocumentError;
use crate::{
capabilities::{self, diagnostic::Diagnostics},
config::{Config, Warnings},
core::{session::Session, sync},
error::{DirectoryError, LanguageServerError},
utils::debug,
};
use dashmap::DashMap;
use forc_pkg::manifest::PackageManifestFile;
use forc_tracing::{init_tracing_subscriber, TracingSubscriberOptions, TracingWriterMode};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::{
fs::File,
io::Write,
ops::Deref,
path::{Path, PathBuf},
sync::Arc,
};
use sway_types::{Ident, Spanned};
use tower_lsp::lsp_types::*;
use tower_lsp::{jsonrpc, Client, LanguageServer};
use tracing::metadata::LevelFilter;
#[derive(Debug)]
pub struct Backend {
pub client: Client,
pub config: RwLock<Config>,
sessions: DashMap<PathBuf, Arc<Session>>,
}
impl Backend {
pub fn new(client: Client) -> Self {
let sessions = DashMap::new();
let config = RwLock::new(Default::default());
Backend {
client,
config,
sessions,
}
}
fn init(&self, uri: &Url) -> Result<(), LanguageServerError> {
let session = Arc::new(Session::new());
let project_name = session.init(uri)?;
self.sessions.insert(project_name, session);
Ok(())
}
fn get_uri_and_session(
&self,
workspace_uri: &Url,
) -> Result<(Url, Arc<Session>), LanguageServerError> {
let session = self.url_to_session(workspace_uri)?;
let uri = session.sync.workspace_to_temp_url(workspace_uri)?;
Ok((uri, session))
}
async fn parse_project(&self, uri: Url, workspace_uri: Url, session: Arc<Session>) {
let diagnostics = match session.parse_project(&uri) {
Ok(diagnostics) => diagnostics,
Err(err) => {
tracing::error!("{}", err.to_string().as_str());
if let LanguageServerError::FailedToParse { diagnostics } = err {
diagnostics
} else {
Diagnostics {
warnings: vec![],
errors: vec![],
}
}
}
};
self.publish_diagnostics(&uri, &workspace_uri, session, diagnostics)
.await;
}
}
fn capabilities() -> ServerCapabilities {
ServerCapabilities {
text_document_sync: Some(TextDocumentSyncCapability::Kind(
TextDocumentSyncKind::INCREMENTAL,
)),
semantic_tokens_provider: Some(
SemanticTokensOptions {
legend: SemanticTokensLegend {
token_types: capabilities::semantic_tokens::SUPPORTED_TYPES.to_vec(),
token_modifiers: capabilities::semantic_tokens::SUPPORTED_MODIFIERS.to_vec(),
},
full: Some(SemanticTokensFullOptions::Bool(true)),
range: None,
..Default::default()
}
.into(),
),
document_symbol_provider: Some(OneOf::Left(true)),
completion_provider: Some(CompletionOptions {
resolve_provider: Some(false),
trigger_characters: None,
..Default::default()
}),
document_formatting_provider: Some(OneOf::Left(true)),
definition_provider: Some(OneOf::Left(true)),
inlay_hint_provider: Some(OneOf::Left(true)),
code_action_provider: Some(CodeActionProviderCapability::Simple(true)),
hover_provider: Some(HoverProviderCapability::Simple(true)),
..ServerCapabilities::default()
}
}
impl Backend {
fn url_to_session(&self, uri: &Url) -> Result<Arc<Session>, LanguageServerError> {
let path = PathBuf::from(uri.path());
let manifest = PackageManifestFile::from_dir(&path).map_err(|_| {
DocumentError::ManifestFileNotFound {
dir: path.to_string_lossy().to_string(),
}
})?;
let manifest_dir = manifest
.path()
.parent()
.ok_or(DirectoryError::ManifestDirNotFound)?
.to_path_buf();
let session = match self.sessions.try_get(&manifest_dir).try_unwrap() {
Some(item) => item.value().clone(),
None => {
self.init(uri)?;
self.sessions
.try_get(&manifest_dir)
.try_unwrap()
.map(|item| item.value().clone())
.expect("no session found even though it was just inserted into the map")
}
};
Ok(session)
}
async fn publish_diagnostics(
&self,
uri: &Url,
workspace_uri: &Url,
session: Arc<Session>,
diagnostics: Diagnostics,
) {
let diagnostics_res = {
let mut diagnostics_to_publish = vec![];
let config = &self.config.read();
let tokens = session.token_map().tokens_for_file(uri);
match config.debug.show_collected_tokens_as_warnings {
Warnings::Parsed => diagnostics_to_publish
.extend(debug::generate_warnings_for_parsed_tokens(tokens)),
Warnings::Typed => {
diagnostics_to_publish.extend(debug::generate_warnings_for_typed_tokens(tokens))
}
Warnings::Default => {}
}
if config.diagnostic.show_warnings {
diagnostics_to_publish.extend(diagnostics.warnings);
}
if config.diagnostic.show_errors {
diagnostics_to_publish.extend(diagnostics.errors);
}
diagnostics_to_publish
};
self.client
.publish_diagnostics(workspace_uri.clone(), diagnostics_res, None)
.await;
}
}
#[tower_lsp::async_trait]
impl LanguageServer for Backend {
async fn initialize(&self, params: InitializeParams) -> jsonrpc::Result<InitializeResult> {
if let Some(initialization_options) = ¶ms.initialization_options {
let mut config = self.config.write();
*config = serde_json::from_value(initialization_options.clone())
.ok()
.unwrap_or_default();
}
let config = self.config.read();
if config.logging.level != LevelFilter::OFF {
let tracing_options = TracingSubscriberOptions {
log_level: Some(config.logging.level),
writer_mode: Some(TracingWriterMode::Stderr),
..Default::default()
};
init_tracing_subscriber(tracing_options);
}
tracing::info!("Initializing the Sway Language Server");
Ok(InitializeResult {
server_info: None,
capabilities: capabilities(),
..InitializeResult::default()
})
}
async fn initialized(&self, _: InitializedParams) {
tracing::info!("Sway Language Server Initialized");
}
async fn shutdown(&self) -> jsonrpc::Result<()> {
tracing::info!("Shutting Down the Sway Language Server");
let _ = self.sessions.iter().map(|item| {
let session = item.value();
session.shutdown();
});
Ok(())
}
async fn did_open(&self, params: DidOpenTextDocumentParams) {
match self.get_uri_and_session(¶ms.text_document.uri) {
Ok((uri, session)) => {
session.handle_open_file(&uri);
self.parse_project(uri, params.text_document.uri, session.clone())
.await;
}
Err(err) => tracing::error!("{}", err.to_string()),
}
}
async fn did_change(&self, params: DidChangeTextDocumentParams) {
match self.get_uri_and_session(¶ms.text_document.uri) {
Ok((uri, session)) => {
match session.write_changes_to_file(&uri, params.content_changes) {
Ok(_) => {
self.parse_project(uri, params.text_document.uri, session.clone())
.await;
}
Err(err) => tracing::error!("{}", err.to_string()),
}
}
Err(err) => tracing::error!("{}", err.to_string()),
}
}
async fn did_save(&self, params: DidSaveTextDocumentParams) {
match self.get_uri_and_session(¶ms.text_document.uri) {
Ok((uri, session)) => {
if let Err(err) = session.sync.clone_manifest_dir_to_temp() {
tracing::error!("{}", err.to_string().as_str());
}
let _ = session
.sync
.manifest_path()
.and_then(|manifest_path| PackageManifestFile::from_dir(&manifest_path).ok())
.map(|manifest| {
if let Some(temp_manifest_path) = &session.sync.temp_manifest_path() {
sync::edit_manifest_dependency_paths(&manifest, temp_manifest_path)
}
});
self.parse_project(uri, params.text_document.uri, session.clone())
.await;
}
Err(err) => tracing::error!("{}", err.to_string()),
}
}
async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {
for event in params.changes {
if event.typ == FileChangeType::DELETED {
match self.get_uri_and_session(&event.uri) {
Ok((uri, session)) => {
let _ = session.remove_document(&uri);
}
Err(err) => tracing::error!("{}", err.to_string()),
}
}
}
}
async fn hover(&self, params: HoverParams) -> jsonrpc::Result<Option<Hover>> {
match self.get_uri_and_session(¶ms.text_document_position_params.text_document.uri) {
Ok((uri, session)) => {
let position = params.text_document_position_params.position;
Ok(capabilities::hover::hover_data(session, uri, position))
}
Err(err) => {
tracing::error!("{}", err.to_string());
Ok(None)
}
}
}
async fn code_action(
&self,
params: CodeActionParams,
) -> jsonrpc::Result<Option<CodeActionResponse>> {
match self.get_uri_and_session(¶ms.text_document.uri) {
Ok((temp_uri, session)) => Ok(capabilities::code_actions(
session,
¶ms.range,
params.text_document,
&temp_uri,
)),
Err(err) => {
tracing::error!("{}", err.to_string());
Ok(None)
}
}
}
async fn completion(
&self,
params: CompletionParams,
) -> jsonrpc::Result<Option<CompletionResponse>> {
match self.get_uri_and_session(¶ms.text_document_position.text_document.uri) {
Ok((_, session)) => {
Ok(session.completion_items().map(CompletionResponse::Array))
}
Err(err) => {
tracing::error!("{}", err.to_string());
Ok(None)
}
}
}
async fn document_symbol(
&self,
params: DocumentSymbolParams,
) -> jsonrpc::Result<Option<DocumentSymbolResponse>> {
match self.get_uri_and_session(¶ms.text_document.uri) {
Ok((uri, session)) => Ok(session
.symbol_information(&uri)
.map(DocumentSymbolResponse::Flat)),
Err(err) => {
tracing::error!("{}", err.to_string());
Ok(None)
}
}
}
async fn semantic_tokens_full(
&self,
params: SemanticTokensParams,
) -> jsonrpc::Result<Option<SemanticTokensResult>> {
match self.get_uri_and_session(¶ms.text_document.uri) {
Ok((uri, session)) => Ok(capabilities::semantic_tokens::semantic_tokens_full(
session, &uri,
)),
Err(err) => {
tracing::error!("{}", err.to_string());
Ok(None)
}
}
}
async fn document_highlight(
&self,
params: DocumentHighlightParams,
) -> jsonrpc::Result<Option<Vec<DocumentHighlight>>> {
match self.get_uri_and_session(¶ms.text_document_position_params.text_document.uri) {
Ok((uri, session)) => {
let position = params.text_document_position_params.position;
Ok(capabilities::highlight::get_highlights(
session, uri, position,
))
}
Err(err) => {
tracing::error!("{}", err.to_string());
Ok(None)
}
}
}
async fn goto_definition(
&self,
params: GotoDefinitionParams,
) -> jsonrpc::Result<Option<GotoDefinitionResponse>> {
match self.get_uri_and_session(¶ms.text_document_position_params.text_document.uri) {
Ok((uri, session)) => {
let position = params.text_document_position_params.position;
Ok(session.token_definition_response(uri, position))
}
Err(err) => {
tracing::error!("{}", err.to_string());
Ok(None)
}
}
}
async fn formatting(
&self,
params: DocumentFormattingParams,
) -> jsonrpc::Result<Option<Vec<TextEdit>>> {
self.get_uri_and_session(¶ms.text_document.uri)
.and_then(|(uri, session)| session.format_text(&uri).map(Some))
.or_else(|err| {
tracing::error!("{}", err.to_string());
Ok(None)
})
}
async fn rename(&self, params: RenameParams) -> jsonrpc::Result<Option<WorkspaceEdit>> {
match self.get_uri_and_session(¶ms.text_document_position.text_document.uri) {
Ok((uri, session)) => {
let new_name = params.new_name;
let position = params.text_document_position.position;
Ok(capabilities::rename::rename(
session, new_name, uri, position,
))
}
Err(err) => {
tracing::error!("{}", err.to_string());
Ok(None)
}
}
}
async fn prepare_rename(
&self,
params: TextDocumentPositionParams,
) -> jsonrpc::Result<Option<PrepareRenameResponse>> {
match self.get_uri_and_session(¶ms.text_document.uri) {
Ok((uri, session)) => {
let position = params.position;
Ok(capabilities::rename::prepare_rename(session, uri, position))
}
Err(err) => {
tracing::error!("{}", err.to_string());
Ok(None)
}
}
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RunnableParams {
pub text_document: TextDocumentIdentifier,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ShowAstParams {
pub text_document: TextDocumentIdentifier,
pub ast_kind: String,
}
impl Backend {
pub async fn inlay_hints(
&self,
params: InlayHintParams,
) -> jsonrpc::Result<Option<Vec<InlayHint>>> {
match self.get_uri_and_session(¶ms.text_document.uri) {
Ok((uri, session)) => {
let config = &self.config.read().inlay_hints;
Ok(capabilities::inlay_hints::inlay_hints(
session,
&uri,
¶ms.range,
config,
))
}
Err(err) => {
tracing::error!("{}", err.to_string());
Ok(None)
}
}
}
pub async fn runnables(
&self,
params: RunnableParams,
) -> jsonrpc::Result<Option<Vec<(Range, String)>>> {
match self.get_uri_and_session(¶ms.text_document.uri) {
Ok((_, session)) => {
let ranges = session
.runnables
.try_get(&capabilities::runnable::RunnableType::MainFn)
.try_unwrap()
.map(|item| {
let runnable = item.value();
vec![(runnable.range, format!("{}", runnable.tree_type))]
});
Ok(ranges)
}
Err(err) => {
tracing::error!("{}", err.to_string());
Ok(None)
}
}
}
pub async fn show_ast(
&self,
params: ShowAstParams,
) -> jsonrpc::Result<Option<TextDocumentIdentifier>> {
match self.get_uri_and_session(¶ms.text_document.uri) {
Ok((_, session)) => {
let current_open_file = params.text_document.uri;
let path = current_open_file.to_file_path().ok();
let write_ast_to_file =
|path: &Path, ast_string: &String| -> Option<TextDocumentIdentifier> {
if let Ok(mut file) = File::create(path) {
let _ = writeln!(&mut file, "{}", ast_string);
if let Ok(uri) = Url::from_file_path(path) {
return Some(TextDocumentIdentifier::new(uri));
}
}
None
};
let path_is_submodule = |ident: &Ident, path: &Option<PathBuf>| -> bool {
ident.span().path().map(|a| a.deref()) == path.as_ref()
};
{
let program = session.compiled_program.read();
match params.ast_kind.as_str() {
"lexed" => {
Ok(program.lexed.as_ref().and_then(|lexed_program| {
let mut formatted_ast = format!("{:#?}", program.lexed);
for (ident, submodule) in &lexed_program.root.submodules {
if path_is_submodule(ident, &path) {
formatted_ast = format!("{:#?}", submodule.module.tree);
}
}
let tmp_ast_path = Path::new("/tmp/lexed_ast.rs");
write_ast_to_file(tmp_ast_path, &formatted_ast)
}))
}
"parsed" => {
Ok(program.parsed.as_ref().and_then(|parsed_program| {
let mut formatted_ast =
format!("{:#?}", parsed_program.root.tree.root_nodes);
for (ident, submodule) in &parsed_program.root.submodules {
if path_is_submodule(ident, &path) {
formatted_ast =
format!("{:#?}", submodule.module.tree.root_nodes);
}
}
let tmp_ast_path = Path::new("/tmp/parsed_ast.rs");
write_ast_to_file(tmp_ast_path, &formatted_ast)
}))
}
"typed" => {
Ok(program.typed.as_ref().and_then(|typed_program| {
let mut formatted_ast = debug::print_decl_engine_types(
&typed_program.root.all_nodes,
&session.decl_engine.read(),
);
for (ident, submodule) in &typed_program.root.submodules {
if path_is_submodule(ident, &path) {
formatted_ast = debug::print_decl_engine_types(
&submodule.module.all_nodes,
&session.decl_engine.read(),
);
}
}
let tmp_ast_path = Path::new("/tmp/typed_ast.rs");
write_ast_to_file(tmp_ast_path, &formatted_ast)
}))
}
_ => Ok(None),
}
}
}
Err(err) => {
tracing::error!("{}", err.to_string());
Ok(None)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::utils::test::{
assert_server_requests, doc_comments_dir, e2e_test_dir, get_fixture, test_fixtures_dir,
};
use serde_json::json;
use std::{borrow::Cow, fs, io::Read, path::PathBuf};
use tower::{Service, ServiceExt};
use tower_lsp::{
jsonrpc::{self, Id, Request, Response},
ExitedError, LspService,
};
fn load_sway_example(manifest_dir: PathBuf) -> (Url, String) {
let src_path = manifest_dir.join("src/main.sw");
let mut file = fs::File::open(&src_path).unwrap();
let mut sway_program = String::new();
file.read_to_string(&mut sway_program).unwrap();
let uri = Url::from_file_path(src_path).unwrap();
(uri, sway_program)
}
fn build_request_with_id(
method: impl Into<Cow<'static, str>>,
params: serde_json::Value,
id: impl Into<Id>,
) -> Request {
Request::build(method).params(params).id(id).finish()
}
async fn call_request(
service: &mut LspService<Backend>,
req: Request,
) -> Result<Option<Response>, ExitedError> {
service.ready().await?.call(req).await
}
async fn initialize_request(service: &mut LspService<Backend>) -> Request {
let params = json!({ "capabilities": capabilities() });
let initialize = build_request_with_id("initialize", params, 1);
let response = call_request(service, initialize.clone()).await;
let ok = Response::from_ok(1.into(), json!({ "capabilities": capabilities() }));
assert_eq!(response, Ok(Some(ok)));
initialize
}
async fn initialized_notification(service: &mut LspService<Backend>) {
let initialized = Request::build("initialized").finish();
let response = call_request(service, initialized).await;
assert_eq!(response, Ok(None));
}
async fn shutdown_request(service: &mut LspService<Backend>) -> Request {
let shutdown = Request::build("shutdown").id(1).finish();
let response = call_request(service, shutdown.clone()).await;
let ok = Response::from_ok(1.into(), json!(null));
assert_eq!(response, Ok(Some(ok)));
shutdown
}
async fn exit_notification(service: &mut LspService<Backend>) {
let exit = Request::build("exit").finish();
let response = call_request(service, exit.clone()).await;
assert_eq!(response, Ok(None));
}
async fn did_open_notification(service: &mut LspService<Backend>, uri: &Url, text: &str) {
let params = json!({
"textDocument": {
"uri": uri,
"languageId": "sway",
"version": 1,
"text": text,
},
});
let did_open = Request::build("textDocument/didOpen")
.params(params)
.finish();
let response = call_request(service, did_open).await;
assert_eq!(response, Ok(None));
}
async fn did_change_request(service: &mut LspService<Backend>, uri: &Url) -> Request {
let params = json!({
"textDocument": {
"uri": uri,
"version": 2
},
"contentChanges": [
{
"range": {
"start": {
"line": 1,
"character": 0
},
"end": {
"line": 1,
"character": 0
}
},
"rangeLength": 0,
"text": "\n",
}
]
});
let did_change = Request::build("textDocument/didChange")
.params(params)
.finish();
let response = call_request(service, did_change.clone()).await;
assert_eq!(response, Ok(None));
did_change
}
async fn did_close_notification(service: &mut LspService<Backend>) {
let exit = Request::build("textDocument/didClose").finish();
let response = call_request(service, exit.clone()).await;
assert_eq!(response, Ok(None));
}
async fn show_ast_request(service: &mut LspService<Backend>, uri: &Url) -> Request {
let params = json!({
"textDocument": {
"uri": uri
},
"astKind": "typed",
});
let show_ast = build_request_with_id("sway/show_ast", params, 1);
let response = call_request(service, show_ast.clone()).await;
let ok = Response::from_ok(1.into(), json!({"uri": "file:///tmp/typed_ast.rs"}));
assert_eq!(response, Ok(Some(ok)));
show_ast
}
async fn semantic_tokens_request(service: &mut LspService<Backend>, uri: &Url) -> Request {
let params = json!({
"textDocument": {
"uri": uri,
},
});
let semantic_tokens = build_request_with_id("textDocument/semanticTokens/full", params, 1);
let _response = call_request(service, semantic_tokens.clone()).await;
semantic_tokens
}
async fn document_symbol_request(service: &mut LspService<Backend>, uri: &Url) -> Request {
let params = json!({
"textDocument": {
"uri": uri,
},
});
let document_symbol = build_request_with_id("textDocument/documentSymbol", params, 1);
let _response = call_request(service, document_symbol.clone()).await;
document_symbol
}
async fn go_to_definition_request(
service: &mut LspService<Backend>,
uri: &Url,
token_req_line: i32,
token_def_line: i32,
id: i64,
) -> Request {
let params = json!({
"textDocument": {
"uri": uri,
},
"position": {
"line": token_req_line,
"character": 24,
}
});
let definition = build_request_with_id("textDocument/definition", params, id);
let response = call_request(service, definition.clone()).await;
let ok = Response::from_ok(
id.into(),
json!({
"range": {
"end": {
"character": 11,
"line": token_def_line,
},
"start": {
"character": 7,
"line": token_def_line,
}
},
"uri": uri,
}),
);
assert_eq!(response, Ok(Some(ok)));
definition
}
async fn hover_request(service: &mut LspService<Backend>, uri: &Url) -> Request {
let params = json!({
"textDocument": {
"uri": uri,
},
"position": {
"line": 44,
"character": 24
}
});
let hover = build_request_with_id("textDocument/hover", params, 1);
let response = call_request(service, hover.clone()).await;
let ok = Response::from_ok(
1.into(),
json!({
"contents": {
"kind": "markdown",
"value": "```sway\nstruct Data\n```\n---\n Struct holding:\n\n 1. A `value` of type `NumberOrString`\n 2. An `address` of type `u64`"
},
"range": {
"end": {
"character": 27,
"line": 44
},
"start": {
"character": 23,
"line": 44
}
}
}),
);
assert_eq!(response, Ok(Some(ok)));
hover
}
async fn format_request(service: &mut LspService<Backend>, uri: &Url) -> Request {
let params = json!({
"textDocument": {
"uri": uri,
},
"options": {
"tabSize": 4,
"insertSpaces": true
},
});
let formatting = build_request_with_id("textDocument/formatting", params, 1);
let _response = call_request(service, formatting.clone()).await;
formatting
}
async fn highlight_request(service: &mut LspService<Backend>, uri: &Url) -> Request {
let params = json!({
"textDocument": {
"uri": uri,
},
"position": {
"line": 45,
"character": 37
}
});
let highlight = build_request_with_id("textDocument/documentHighlight", params, 1);
let response = call_request(service, highlight.clone()).await;
let ok = Response::from_ok(
1.into(),
json!([{
"range": {
"end": {
"character": 41,
"line": 45
},
"start": {
"character": 35,
"line": 45
}
}
}
]),
);
assert_eq!(response, Ok(Some(ok)));
highlight
}
async fn init_and_open(service: &mut LspService<Backend>, manifest_dir: PathBuf) -> Url {
let _ = initialize_request(service).await;
initialized_notification(service).await;
let (uri, sway_program) = load_sway_example(manifest_dir);
did_open_notification(service, &uri, &sway_program).await;
uri
}
async fn shutdown_and_exit(service: &mut LspService<Backend>) {
let _ = shutdown_request(service).await;
exit_notification(service).await;
}
#[tokio::test]
async fn initialize() {
let (mut service, _) = LspService::new(Backend::new);
let _ = initialize_request(&mut service).await;
}
#[tokio::test]
async fn initialized() {
let (mut service, _) = LspService::new(Backend::new);
let _ = initialize_request(&mut service).await;
initialized_notification(&mut service).await;
}
#[tokio::test]
async fn initializes_only_once() {
let (mut service, _) = LspService::new(Backend::new);
let initialize = initialize_request(&mut service).await;
initialized_notification(&mut service).await;
let response = call_request(&mut service, initialize).await;
let err = Response::from_error(1.into(), jsonrpc::Error::invalid_request());
assert_eq!(response, Ok(Some(err)));
}
#[tokio::test]
async fn shutdown() {
let (mut service, _) = LspService::new(Backend::new);
let _ = initialize_request(&mut service).await;
initialized_notification(&mut service).await;
let shutdown = shutdown_request(&mut service).await;
let response = call_request(&mut service, shutdown).await;
let err = Response::from_error(1.into(), jsonrpc::Error::invalid_request());
assert_eq!(response, Ok(Some(err)));
exit_notification(&mut service).await;
}
#[tokio::test]
async fn refuses_requests_after_shutdown() {
let (mut service, _) = LspService::new(Backend::new);
let _ = initialize_request(&mut service).await;
let shutdown = shutdown_request(&mut service).await;
let response = call_request(&mut service, shutdown).await;
let err = Response::from_error(1.into(), jsonrpc::Error::invalid_request());
assert_eq!(response, Ok(Some(err)));
}
#[tokio::test]
async fn did_open() {
let (mut service, _) = LspService::new(Backend::new);
let _ = init_and_open(&mut service, e2e_test_dir()).await;
shutdown_and_exit(&mut service).await;
}
#[tokio::test]
async fn did_close() {
let (mut service, _) = LspService::new(Backend::new);
let _ = init_and_open(&mut service, e2e_test_dir()).await;
did_close_notification(&mut service).await;
shutdown_and_exit(&mut service).await;
}
#[tokio::test]
async fn did_change() {
let (mut service, _) = LspService::new(Backend::new);
let uri = init_and_open(&mut service, doc_comments_dir()).await;
let _ = did_change_request(&mut service, &uri).await;
shutdown_and_exit(&mut service).await;
}
#[tokio::test]
async fn lsp_syncs_with_workspace_edits() {
let (mut service, _) = LspService::new(Backend::new);
let uri = init_and_open(&mut service, doc_comments_dir()).await;
let _ = go_to_definition_request(&mut service, &uri, 44, 19, 1).await;
let _ = did_change_request(&mut service, &uri).await;
let _ = go_to_definition_request(&mut service, &uri, 45, 20, 2).await;
shutdown_and_exit(&mut service).await;
}
#[tokio::test]
async fn show_ast() {
let (mut service, _) = LspService::build(Backend::new)
.custom_method("sway/show_ast", Backend::show_ast)
.finish();
let uri = init_and_open(&mut service, e2e_test_dir()).await;
let _ = show_ast_request(&mut service, &uri).await;
shutdown_and_exit(&mut service).await;
}
#[tokio::test]
async fn go_to_definition() {
let (mut service, _) = LspService::new(Backend::new);
let uri = init_and_open(&mut service, doc_comments_dir()).await;
let _ = go_to_definition_request(&mut service, &uri, 44, 19, 1).await;
shutdown_and_exit(&mut service).await;
}
#[tokio::test]
async fn publish_diagnostics_dead_code_warning() {
let (mut service, socket) = LspService::new(Backend::new);
let fixture = get_fixture(test_fixtures_dir().join("diagnostics/dead_code/expected.json"));
let expected_requests = vec![fixture];
let socket_handle = assert_server_requests(socket, expected_requests, None).await;
let _ = init_and_open(
&mut service,
test_fixtures_dir().join("diagnostics/dead_code"),
)
.await;
socket_handle
.await
.unwrap_or_else(|e| panic!("Test failed: {:?}", e));
shutdown_and_exit(&mut service).await;
}
macro_rules! test_lsp_capability {
($example_dir:expr, $capability:expr) => {{
let (mut service, _) = LspService::new(Backend::new);
let uri = init_and_open(&mut service, $example_dir).await;
let _ = $capability(&mut service, &uri).await;
shutdown_and_exit(&mut service).await;
}};
}
macro_rules! lsp_capability_test {
($test:ident, $capability:expr) => {
#[tokio::test]
async fn $test() {
test_lsp_capability!(doc_comments_dir(), $capability);
}
};
}
lsp_capability_test!(semantic_tokens, semantic_tokens_request);
lsp_capability_test!(document_symbol, document_symbol_request);
lsp_capability_test!(format, format_request);
lsp_capability_test!(hover, hover_request);
lsp_capability_test!(highlight, highlight_request);
}