diff --git a/compiler-executable/iris-lsp-workspace/src/capabilities.rs b/compiler-executable/iris-lsp-workspace/src/capabilities.rs index 99ef23ea2..62310dd86 100644 --- a/compiler-executable/iris-lsp-workspace/src/capabilities.rs +++ b/compiler-executable/iris-lsp-workspace/src/capabilities.rs @@ -5,15 +5,11 @@ use iris_analysis::position::PositionEncoding; use lsp_types::*; pub(crate) fn negotiate_analyzer_capabilities( - parameters: &InitializeParams, + capabilities: &ClientCapabilities, ) -> AnalyzerCapabilities { - let workspace_edit = parameters - .capabilities - .workspace - .as_ref() - .and_then(|workspace| workspace.workspace_edit.as_ref()); - let honors_rename_annotations = parameters - .capabilities + let workspace_edit = + capabilities.workspace.as_ref().and_then(|workspace| workspace.workspace_edit.as_ref()); + let honors_rename_annotations = capabilities .text_document .as_ref() .and_then(|text_document| text_document.rename.as_ref()) @@ -28,8 +24,7 @@ pub(crate) fn negotiate_analyzer_capabilities( if change_annotations { negotiated = negotiated.with_change_annotations(); } - let markdown_hover = parameters - .capabilities + let markdown_hover = capabilities .text_document .as_ref() .and_then(|text_document| text_document.hover.as_ref()) @@ -46,12 +41,9 @@ pub(crate) fn negotiate_analyzer_capabilities( negotiated } -pub(crate) fn negotiate_position_encoding(parameters: &InitializeParams) -> PositionEncoding { - let Some(encodings) = parameters - .capabilities - .general - .as_ref() - .and_then(|general| general.position_encodings.as_ref()) +pub(crate) fn negotiate_position_encoding(capabilities: &ClientCapabilities) -> PositionEncoding { + let Some(encodings) = + capabilities.general.as_ref().and_then(|general| general.position_encodings.as_ref()) else { return PositionEncoding::Utf16; }; @@ -141,69 +133,66 @@ mod tests { use super::*; - fn initialize_parameters( + fn client_capabilities( position_encodings: Option>, - ) -> InitializeParams { - InitializeParams { - capabilities: ClientCapabilities { - general: Some(GeneralClientCapabilities { - position_encodings, - ..GeneralClientCapabilities::default() - }), - ..ClientCapabilities::default() - }, - ..InitializeParams::default() + ) -> ClientCapabilities { + ClientCapabilities { + general: Some(GeneralClientCapabilities { + position_encodings, + ..GeneralClientCapabilities::default() + }), + ..ClientCapabilities::default() } } #[test] fn defaults_to_utf16_without_client_preference() { - let parameters = InitializeParams::default(); + let capabilities = ClientCapabilities::default(); - let encoding = negotiate_position_encoding(¶meters); + let encoding = negotiate_position_encoding(&capabilities); assert_eq!(encoding, PositionEncoding::Utf16); } #[test] fn prefers_utf8_when_available() { - let parameters = initialize_parameters(Some(vec![ + let capabilities = client_capabilities(Some(vec![ PositionEncodingKind::UTF32, PositionEncodingKind::UTF16, PositionEncodingKind::UTF8, ])); - let encoding = negotiate_position_encoding(¶meters); + let encoding = negotiate_position_encoding(&capabilities); assert_eq!(encoding, PositionEncoding::Utf8); } #[test] fn falls_back_to_utf16_before_utf32() { - let parameters = initialize_parameters(Some(vec![ + let capabilities = client_capabilities(Some(vec![ PositionEncodingKind::UTF32, PositionEncodingKind::UTF16, ])); - let encoding = negotiate_position_encoding(¶meters); + let encoding = negotiate_position_encoding(&capabilities); assert_eq!(encoding, PositionEncoding::Utf16); } #[test] fn supports_utf32_when_it_is_the_only_known_option() { - let parameters = initialize_parameters(Some(vec![PositionEncodingKind::UTF32])); + let capabilities = client_capabilities(Some(vec![PositionEncodingKind::UTF32])); - let encoding = negotiate_position_encoding(¶meters); + let encoding = negotiate_position_encoding(&capabilities); assert_eq!(encoding, PositionEncoding::Utf32); } #[test] fn negotiates_hover_content_format() { - let mut parameters = InitializeParams::default(); + let mut capabilities = ClientCapabilities::default(); assert_eq!( - negotiate_analyzer_capabilities(¶meters).hover_format(), + negotiate_analyzer_capabilities(&capabilities).hover_format(), MarkupKind::PlainText ); - parameters.capabilities.text_document = Some(TextDocumentClientCapabilities { + capabilities.text_document = Some(TextDocumentClientCapabilities { hover: Some(HoverClientCapabilities { content_format: Some(vec![MarkupKind::PlainText]), ..HoverClientCapabilities::default() @@ -211,35 +200,21 @@ mod tests { ..TextDocumentClientCapabilities::default() }); assert_eq!( - negotiate_analyzer_capabilities(¶meters).hover_format(), + negotiate_analyzer_capabilities(&capabilities).hover_format(), MarkupKind::PlainText ); - parameters - .capabilities - .text_document - .as_mut() - .unwrap() - .hover - .as_mut() - .unwrap() - .content_format = Some(vec![MarkupKind::PlainText, MarkupKind::Markdown]); + capabilities.text_document.as_mut().unwrap().hover.as_mut().unwrap().content_format = + Some(vec![MarkupKind::PlainText, MarkupKind::Markdown]); assert_eq!( - negotiate_analyzer_capabilities(¶meters).hover_format(), + negotiate_analyzer_capabilities(&capabilities).hover_format(), MarkupKind::PlainText ); - parameters - .capabilities - .text_document - .as_mut() - .unwrap() - .hover - .as_mut() - .unwrap() - .content_format = Some(vec![MarkupKind::Markdown, MarkupKind::PlainText]); + capabilities.text_document.as_mut().unwrap().hover.as_mut().unwrap().content_format = + Some(vec![MarkupKind::Markdown, MarkupKind::PlainText]); assert_eq!( - negotiate_analyzer_capabilities(¶meters).hover_format(), + negotiate_analyzer_capabilities(&capabilities).hover_format(), MarkupKind::Markdown ); } diff --git a/compiler-executable/iris-lsp-workspace/src/service.rs b/compiler-executable/iris-lsp-workspace/src/service.rs index 8cc942db3..f12cceb5e 100644 --- a/compiler-executable/iris-lsp-workspace/src/service.rs +++ b/compiler-executable/iris-lsp-workspace/src/service.rs @@ -20,7 +20,8 @@ use iris_lsp_server::{ Answer, ControlMessage, OrderedMessage, Rejection, SettingsResponse, WorkspaceEvent, WorkspaceEventSender, WorkspaceFailure, WorkspaceReceivers, }; -use lsp_types::{InitializeParams, Uri, WorkspaceFolders}; +use lsp_types::{ClientCapabilities, Uri, WorkspaceFolders}; +use serde::Deserialize; use serde_json::{Value, json}; use tokio::sync::{mpsc, oneshot}; use tokio::task; @@ -71,6 +72,14 @@ struct Session { analyzer_capabilities: AnalyzerCapabilities, } +/// Decode only workspace-owned fields; `iris-lsp-server` interprets `processId`. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct InitializeParams { + capabilities: ClientCapabilities, + workspace_folders: Option, +} + const WORKSPACE_LOADING: &str = "Workspace is loading"; const WORKSPACE_FAILED: &str = "Workspace preparation failed"; const WORKSPACE_CANCELLED: &str = "Workspace preparation cancelled"; @@ -278,9 +287,10 @@ impl Actor { let parameters = serde_json::from_value::(params).map_err(|error| { Rejection::InvalidParams(format!("Failed to deserialize parameters: {error}")) })?; - self.session.position_encoding = negotiate_position_encoding(¶meters); - self.session.analyzer_capabilities = negotiate_analyzer_capabilities(¶meters); - let scope = match parameters.workspace_folders_initialize_params.workspace_folders { + self.session.position_encoding = negotiate_position_encoding(¶meters.capabilities); + self.session.analyzer_capabilities = + negotiate_analyzer_capabilities(¶meters.capabilities); + let scope = match parameters.workspace_folders { Some(WorkspaceFolders::WorkspaceFolderList(folders)) => { folders.first().map(|folder| Uri::clone(&folder.uri)) } diff --git a/compiler-executable/iris-lsp-workspace/src/tests.rs b/compiler-executable/iris-lsp-workspace/src/tests.rs index 0481a54c5..3f4dd418d 100644 --- a/compiler-executable/iris-lsp-workspace/src/tests.rs +++ b/compiler-executable/iris-lsp-workspace/src/tests.rs @@ -25,7 +25,7 @@ use iris_lsp_server::{ Answer, ControlMessage, OrderedMessage, Rejection, SettingsResponse, WorkspaceEvent, WorkspaceEventSender, WorkspaceFailure, WorkspaceSenders, }; -use lsp_types::{Position, Range, Uri}; +use lsp_types::{InitializeResult, Position, PositionEncodingKind, Range, Uri}; use parking_lot::Mutex; use serde_json::{Value, json}; use tempfile::TempDir; @@ -307,6 +307,27 @@ fn module(name: &str, value: &str) -> String { format!("module {name} where\n{value} = 1\n") } +#[tokio::test] +async fn an_out_of_range_process_id_does_not_prevent_initialization() { + let (events, _event_receiver) = WorkspaceEventSender::channel(); + let (senders, receivers) = WorkspaceSenders::channel(); + let actor = Actor::new(config(1), events); + let task = tokio::spawn(actor.run(receivers)); + let initialize = senders.initialize(json!({ + "processId": 2_147_483_648_i64, + "capabilities": {"general": {"positionEncodings": ["utf-8"]}}, + "workspaceFolders": null + })); + + let result = answer(initialize).await.unwrap(); + let result = serde_json::from_value::(result).unwrap(); + assert_eq!(result.capabilities.position_encoding, Some(PositionEncodingKind::UTF8)); + assert_eq!(result.server_info.unwrap().name, "iris"); + + drop(senders); + tokio::time::timeout(PATIENCE, task).await.unwrap().unwrap().unwrap(); +} + #[tokio::test] async fn a_change_wins_over_running_and_waiting_analysis() { // With one permit: a long request holds the permit, a second request waits for it, then a