diff --git a/crates/stef-lsp/src/client.rs b/crates/stef-lsp/src/client.rs new file mode 100644 index 0000000..778b4f9 --- /dev/null +++ b/crates/stef-lsp/src/client.rs @@ -0,0 +1,93 @@ +use std::time::Duration; + +use anyhow::{bail, ensure, Result}; +use lsp_server::{Connection, Message, Notification, Request, Response}; +use lsp_types::{ + notification::{Notification as LspNotification, PublishDiagnostics}, + request::{RegisterCapability, Request as LspRequest, WorkspaceConfiguration}, + ConfigurationItem, ConfigurationParams, Diagnostic, PublishDiagnosticsParams, Registration, + RegistrationParams, Url, +}; + +pub struct Client<'a> { + conn: &'a Connection, + next_id: i32, +} + +impl<'a> Client<'a> { + pub fn new(conn: &'a Connection) -> Self { + Self { conn, next_id: 0 } + } + + fn next_id(&mut self) -> i32 { + let id = self.next_id.wrapping_add(1); + self.next_id = id; + id + } + + fn send_notification(&self, params: T::Params) -> Result<()> + where + T: LspNotification, + { + self.conn + .sender + .send_timeout( + Notification::new(T::METHOD.to_owned(), params).into(), + Duration::from_secs(2), + ) + .map_err(Into::into) + } + + fn send_request(&mut self, params: T::Params) -> Result + where + T: LspRequest, + { + let req_id = self.next_id(); + + self.conn.sender.send_timeout( + Request::new(req_id.into(), T::METHOD.to_owned(), params).into(), + Duration::from_secs(2), + )?; + + match self.conn.receiver.recv_timeout(Duration::from_secs(2))? { + Message::Response(Response { + id, + result: Some(result), + error: None, + }) => { + ensure!(id == req_id.into(), "invalid ID"); + serde_json::from_value(result).map_err(Into::into) + } + Message::Response(Response { + id, + result: None, + error: Some(error), + }) => bail!("request {id} failed: {error:?}"), + _ => bail!("invalid message type"), + } + } + + pub fn publish_diagnostics( + &self, + uri: Url, + diagnostics: Vec, + version: Option, + ) -> Result<()> { + self.send_notification::(PublishDiagnosticsParams { + uri, + diagnostics, + version, + }) + } + + pub fn configuration( + &mut self, + items: Vec, + ) -> Result> { + self.send_request::(ConfigurationParams { items }) + } + + pub fn register_capability(&mut self, registrations: Vec) -> Result<()> { + self.send_request::(RegistrationParams { registrations }) + } +} diff --git a/crates/stef-lsp/src/handlers.rs b/crates/stef-lsp/src/handlers.rs new file mode 100644 index 0000000..6bea210 --- /dev/null +++ b/crates/stef-lsp/src/handlers.rs @@ -0,0 +1,257 @@ +#![allow(clippy::needless_pass_by_value, clippy::unnecessary_wraps)] + +use anyhow::{Context, Result}; +use line_index::{LineIndex, TextRange}; +use log::{as_debug, as_display, debug, error, warn}; +use lsp_types::{ + DidChangeConfigurationParams, DidChangeTextDocumentParams, DidCloseTextDocumentParams, + DidOpenTextDocumentParams, InitializeParams, InitializeResult, InitializedParams, + PositionEncodingKind, Registration, SemanticTokenModifier, SemanticTokenType, + SemanticTokensFullOptions, SemanticTokensLegend, SemanticTokensOptions, SemanticTokensParams, + SemanticTokensResult, SemanticTokensServerCapabilities, ServerCapabilities, ServerInfo, + TextDocumentSyncCapability, TextDocumentSyncKind, WorkDoneProgressOptions, +}; +use ropey::Rope; + +use crate::{compile, state::FileBuilder, GlobalState}; + +pub fn initialize( + _state: &mut GlobalState<'_>, + _params: InitializeParams, +) -> Result { + Ok(InitializeResult { + server_info: Some(ServerInfo { + name: env!("CARGO_CRATE_NAME").to_owned(), + version: Some(env!("CARGO_PKG_VERSION").to_owned()), + }), + capabilities: ServerCapabilities { + position_encoding: Some(PositionEncodingKind::UTF16), + text_document_sync: Some(TextDocumentSyncCapability::Kind( + TextDocumentSyncKind::INCREMENTAL, + )), + semantic_tokens_provider: Some( + SemanticTokensServerCapabilities::SemanticTokensOptions(SemanticTokensOptions { + work_done_progress_options: WorkDoneProgressOptions { + work_done_progress: Some(false), + }, + legend: SemanticTokensLegend { + token_types: vec![ + SemanticTokenType::NAMESPACE, + SemanticTokenType::TYPE, + SemanticTokenType::CLASS, + SemanticTokenType::ENUM, + SemanticTokenType::INTERFACE, + SemanticTokenType::STRUCT, + SemanticTokenType::TYPE_PARAMETER, + SemanticTokenType::PARAMETER, + SemanticTokenType::VARIABLE, + SemanticTokenType::PROPERTY, + SemanticTokenType::ENUM_MEMBER, + SemanticTokenType::EVENT, + SemanticTokenType::FUNCTION, + SemanticTokenType::METHOD, + SemanticTokenType::MACRO, + SemanticTokenType::KEYWORD, + SemanticTokenType::MODIFIER, + SemanticTokenType::COMMENT, + SemanticTokenType::STRING, + SemanticTokenType::NUMBER, + SemanticTokenType::REGEXP, + SemanticTokenType::OPERATOR, + SemanticTokenType::DECORATOR, + ], + token_modifiers: vec![ + SemanticTokenModifier::DECLARATION, + SemanticTokenModifier::DEFINITION, + SemanticTokenModifier::READONLY, + SemanticTokenModifier::STATIC, + SemanticTokenModifier::DEPRECATED, + SemanticTokenModifier::ABSTRACT, + SemanticTokenModifier::ASYNC, + SemanticTokenModifier::MODIFICATION, + SemanticTokenModifier::DOCUMENTATION, + SemanticTokenModifier::DEFAULT_LIBRARY, + ], + }, + range: Some(false), + full: Some(SemanticTokensFullOptions::Bool(true)), + }), + ), + ..ServerCapabilities::default() + }, + offset_encoding: None, + }) +} + +pub fn initialized(state: &mut GlobalState<'_>, _params: InitializedParams) { + if let Err(e) = state.reload_settings() { + error!(error = as_debug!(e); "failed loading initial settings"); + } + + if let Err(e) = state.client.register_capability(vec![Registration { + id: "1".to_owned(), + method: "workspace/didChangeConfiguration".to_owned(), + register_options: None, + }]) { + error!(error = as_debug!(e); "failed registering for configuration changes"); + } + + debug!("initialized"); +} + +pub fn did_open(state: &mut GlobalState<'_>, params: DidOpenTextDocumentParams) { + debug!(uri = as_display!(params.text_document.uri); "schema opened"); + + let text = params.text_document.text; + let file = FileBuilder { + rope: Rope::from_str(&text), + index: LineIndex::new(&text), + content: text, + schema_builder: |index, schema| { + compile::compile(params.text_document.uri.clone(), schema, index) + }, + } + .build(); + + if let Err(e) = state.client.publish_diagnostics( + params.text_document.uri.clone(), + file.borrow_schema() + .as_ref() + .err() + .map(|diag| vec![diag.clone()]) + .unwrap_or_default(), + None, + ) { + error!(error = as_debug!(e); "failed publishing diagnostics"); + } + + state.files.insert(params.text_document.uri, file); +} + +pub fn did_change(state: &mut GlobalState<'_>, mut params: DidChangeTextDocumentParams) { + debug!(uri = as_display!(params.text_document.uri); "schema changed"); + + let file = if params.content_changes.len() == 1 + && params + .content_changes + .first() + .is_some_and(|change| change.range.is_none()) + { + let text = params.content_changes.remove(0).text; + FileBuilder { + rope: Rope::from_str(&text), + index: LineIndex::new(&text), + content: text, + schema_builder: |index, schema| { + compile::compile(params.text_document.uri.clone(), schema, index) + }, + } + .build() + } else { + let Some(file) = state.files.remove(¶ms.text_document.uri) else { + warn!("missing state for changed file"); + return; + }; + + let mut heads = file.into_heads(); + + for change in params.content_changes { + let range = match convert_range(&heads.index, change.range) { + Ok(range) => range, + Err(e) => { + error!(error = as_debug!(e); "invalid change"); + continue; + } + }; + + let start = heads.rope.byte_to_char(range.start().into()); + let end = heads.rope.byte_to_char(range.end().into()); + heads.rope.remove(start..end); + heads.rope.insert(start, &change.text); + } + + let text = String::from(&heads.rope); + + FileBuilder { + rope: heads.rope, + index: LineIndex::new(&text), + content: text, + schema_builder: |index, schema| { + compile::compile(params.text_document.uri.clone(), schema, index) + }, + } + .build() + }; + + if let Err(e) = state.client.publish_diagnostics( + params.text_document.uri.clone(), + file.borrow_schema() + .as_ref() + .err() + .map(|diag| vec![diag.clone()]) + .unwrap_or_default(), + None, + ) { + error!(error = as_debug!(e); "failed publishing diagnostics"); + } + + state.files.insert(params.text_document.uri, file); +} + +pub fn did_close(state: &mut GlobalState<'_>, params: DidCloseTextDocumentParams) { + debug!(uri = as_display!(params.text_document.uri); "schema closed"); + state.files.remove(¶ms.text_document.uri); +} + +pub fn semantic_tokens_full( + _state: &mut GlobalState<'_>, + params: SemanticTokensParams, +) -> Result> { + debug!(uri = as_display!(params.text_document.uri); "requested semantic tokens"); + Ok(None) +} + +pub fn did_change_configuration( + state: &mut GlobalState<'_>, + _params: DidChangeConfigurationParams, +) { + debug!("configuration changed"); + + if let Err(e) = state.reload_settings() { + error!(error = as_debug!(e); "failed loading changed settings"); + } +} + +pub fn convert_range(index: &LineIndex, range: Option) -> Result { + let range = range.context("incremental change misses range")?; + + let start = index + .offset( + index + .to_utf8( + line_index::WideEncoding::Utf16, + line_index::WideLineCol { + line: range.start.line, + col: range.start.character, + }, + ) + .context("failed to convert start position to utf-8")?, + ) + .context("failed to convert start position to byte offset")?; + + let end = index + .offset( + index + .to_utf8( + line_index::WideEncoding::Utf16, + line_index::WideLineCol { + line: range.end.line, + col: range.end.character, + }, + ) + .context("failed to convert end position to utf-8")?, + ) + .context("failed to convert end position to byte offset")?; + + Ok(TextRange::new(start, end)) +} diff --git a/crates/stef-lsp/src/main.rs b/crates/stef-lsp/src/main.rs index e4cefe2..e04a9e3 100644 --- a/crates/stef-lsp/src/main.rs +++ b/crates/stef-lsp/src/main.rs @@ -1,371 +1,30 @@ #![warn(clippy::expect_used, clippy::unwrap_used)] #![allow(missing_docs)] -use std::{collections::HashMap, net::Ipv4Addr, time::Duration}; +use std::{collections::HashMap, net::Ipv4Addr}; -use anyhow::{bail, ensure, Context, Result}; -use line_index::{LineIndex, TextRange}; -use log::{as_debug, as_display, debug, error, info, warn}; -use lsp_server::{Connection, ErrorCode, Message, Notification, Request, RequestId, Response}; +use anyhow::{bail, Result}; +use log::{as_debug, debug, error, info, warn}; +use lsp_server::{Connection, ErrorCode, Notification, Request, RequestId, Response}; use lsp_types::{ notification::{ DidChangeConfiguration, DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument, - Initialized, Notification as LspNotification, PublishDiagnostics, + Initialized, Notification as LspNotification, }, - request::{ - RegisterCapability, Request as LspRequest, SemanticTokensFullRequest, Shutdown, - WorkspaceConfiguration, - }, - ConfigurationItem, ConfigurationParams, Diagnostic, DidChangeConfigurationParams, - DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, - InitializeParams, InitializeResult, InitializedParams, PositionEncodingKind, - PublishDiagnosticsParams, Registration, RegistrationParams, SemanticTokenModifier, - SemanticTokenType, SemanticTokens, SemanticTokensFullOptions, SemanticTokensLegend, - SemanticTokensOptions, SemanticTokensParams, SemanticTokensResult, - SemanticTokensServerCapabilities, ServerCapabilities, ServerInfo, TextDocumentSyncCapability, - TextDocumentSyncKind, Url, WorkDoneProgressOptions, + request::{Request as LspRequest, SemanticTokensFullRequest, Shutdown}, + InitializeParams, SemanticTokens, SemanticTokensResult, }; -use ouroboros::self_referencing; -use ropey::Rope; -use stef_parser::Schema; -use self::cli::Cli; +use self::{cli::Cli, client::Client}; +use crate::state::GlobalState; mod cli; +mod client; mod compile; mod config; +mod handlers; mod logging; - -struct Backend { - conn: Connection, - files: HashMap, - settings: config::Global, - next_id: i32, -} - -#[self_referencing] -#[derive(Debug)] -struct File { - rope: Rope, - index: LineIndex, - content: String, - #[borrows(index, content)] - #[covariant] - schema: Result, Diagnostic>, -} - -impl Backend { - fn send_notification(&self, params: T::Params) -> Result<()> - where - T: LspNotification, - { - self.conn - .sender - .send_timeout( - Notification::new(T::METHOD.to_owned(), params).into(), - Duration::from_secs(2), - ) - .map_err(Into::into) - } - - fn send_request(&mut self, params: T::Params) -> Result - where - T: LspRequest, - { - let next_id = self.next_id.wrapping_add(1); - self.next_id = next_id; - - self.conn.sender.send_timeout( - Request::new(next_id.into(), T::METHOD.to_owned(), params).into(), - Duration::from_secs(2), - )?; - - match self.conn.receiver.recv_timeout(Duration::from_secs(2))? { - Message::Response(Response { - id, - result: Some(result), - error: None, - }) => { - ensure!(id == next_id.into(), "invalid ID"); - serde_json::from_value(result).map_err(Into::into) - } - Message::Response(Response { - id, - result: None, - error: Some(error), - }) => bail!("request {id} failed: {error:?}"), - _ => bail!("invalid message type"), - } - } - - fn publish_diagnostics( - &self, - uri: Url, - diagnostics: Vec, - version: Option, - ) -> Result<()> { - self.send_notification::(PublishDiagnosticsParams { - uri, - diagnostics, - version, - }) - } - - fn configuration(&mut self, items: Vec) -> Result> { - self.send_request::(ConfigurationParams { items }) - } - - fn register_capability(&mut self, registrations: Vec) -> Result<()> { - self.send_request::(RegistrationParams { registrations }) - } - - fn reload_settings(&mut self) -> Result<()> { - let mut settings = self - .configuration(vec![ConfigurationItem { - scope_uri: None, - section: Some("stef".to_owned()), - }]) - .context("failed getting configuration from client")?; - - ensure!( - settings.len() == 1, - "configuration contains not exactly one object" - ); - - let settings = serde_json::from_value(settings.remove(0)) - .context("failed to parse raw configuration")?; - - debug!(settings = as_debug!(settings); "configuration loaded"); - - self.settings = settings; - - Ok(()) - } -} - -trait LanguageServer { - fn initialize(&mut self, params: InitializeParams) -> Result; - fn initialized(&mut self, params: InitializedParams); - fn shutdown(&mut self) -> Result<()>; - fn did_open(&mut self, params: DidOpenTextDocumentParams); - fn did_change(&mut self, params: DidChangeTextDocumentParams); - fn did_close(&mut self, params: DidCloseTextDocumentParams); - fn semantic_tokens_full( - &mut self, - params: SemanticTokensParams, - ) -> Result>; - fn did_change_configuration(&mut self, params: DidChangeConfigurationParams); -} - -impl LanguageServer for Backend { - fn initialize(&mut self, _params: InitializeParams) -> Result { - Ok(InitializeResult { - server_info: Some(ServerInfo { - name: env!("CARGO_CRATE_NAME").to_owned(), - version: Some(env!("CARGO_PKG_VERSION").to_owned()), - }), - capabilities: ServerCapabilities { - position_encoding: Some(PositionEncodingKind::UTF16), - text_document_sync: Some(TextDocumentSyncCapability::Kind( - TextDocumentSyncKind::INCREMENTAL, - )), - semantic_tokens_provider: Some( - SemanticTokensServerCapabilities::SemanticTokensOptions( - SemanticTokensOptions { - work_done_progress_options: WorkDoneProgressOptions { - work_done_progress: Some(false), - }, - legend: SemanticTokensLegend { - token_types: vec![ - SemanticTokenType::NAMESPACE, - SemanticTokenType::TYPE, - SemanticTokenType::CLASS, - SemanticTokenType::ENUM, - SemanticTokenType::INTERFACE, - SemanticTokenType::STRUCT, - SemanticTokenType::TYPE_PARAMETER, - SemanticTokenType::PARAMETER, - SemanticTokenType::VARIABLE, - SemanticTokenType::PROPERTY, - SemanticTokenType::ENUM_MEMBER, - SemanticTokenType::EVENT, - SemanticTokenType::FUNCTION, - SemanticTokenType::METHOD, - SemanticTokenType::MACRO, - SemanticTokenType::KEYWORD, - SemanticTokenType::MODIFIER, - SemanticTokenType::COMMENT, - SemanticTokenType::STRING, - SemanticTokenType::NUMBER, - SemanticTokenType::REGEXP, - SemanticTokenType::OPERATOR, - SemanticTokenType::DECORATOR, - ], - token_modifiers: vec![ - SemanticTokenModifier::DECLARATION, - SemanticTokenModifier::DEFINITION, - SemanticTokenModifier::READONLY, - SemanticTokenModifier::STATIC, - SemanticTokenModifier::DEPRECATED, - SemanticTokenModifier::ABSTRACT, - SemanticTokenModifier::ASYNC, - SemanticTokenModifier::MODIFICATION, - SemanticTokenModifier::DOCUMENTATION, - SemanticTokenModifier::DEFAULT_LIBRARY, - ], - }, - range: Some(false), - full: Some(SemanticTokensFullOptions::Bool(true)), - }, - ), - ), - ..ServerCapabilities::default() - }, - offset_encoding: None, - }) - } - - fn initialized(&mut self, _params: InitializedParams) { - if let Err(e) = self.reload_settings() { - error!(error = as_debug!(e); "failed loading initial settings"); - } - - if let Err(e) = self.register_capability(vec![Registration { - id: "1".to_owned(), - method: "workspace/didChangeConfiguration".to_owned(), - register_options: None, - }]) { - error!(error = as_debug!(e); "failed registering for configuration changes"); - } - - debug!("initialized"); - } - - fn shutdown(&mut self) -> Result<()> { - debug!("got shutdown request"); - Ok(()) - } - - fn did_open(&mut self, params: DidOpenTextDocumentParams) { - debug!(uri = as_display!(params.text_document.uri); "schema opened"); - - let text = params.text_document.text; - let file = FileBuilder { - rope: Rope::from_str(&text), - index: LineIndex::new(&text), - content: text, - schema_builder: |index, schema| { - compile::compile(params.text_document.uri.clone(), schema, index) - }, - } - .build(); - - if let Err(e) = self.publish_diagnostics( - params.text_document.uri.clone(), - file.borrow_schema() - .as_ref() - .err() - .map(|diag| vec![diag.clone()]) - .unwrap_or_default(), - None, - ) { - error!(error = as_debug!(e); "failed publishing diagnostics"); - } - - self.files.insert(params.text_document.uri, file); - } - - fn did_change(&mut self, mut params: DidChangeTextDocumentParams) { - debug!(uri = as_display!(params.text_document.uri); "schema changed"); - - let file = if params.content_changes.len() == 1 - && params - .content_changes - .first() - .is_some_and(|change| change.range.is_none()) - { - let text = params.content_changes.remove(0).text; - FileBuilder { - rope: Rope::from_str(&text), - index: LineIndex::new(&text), - content: text, - schema_builder: |index, schema| { - compile::compile(params.text_document.uri.clone(), schema, index) - }, - } - .build() - } else { - let Some(file) = self.files.remove(¶ms.text_document.uri) else { - warn!("missing state for changed file"); - return; - }; - - let mut heads = file.into_heads(); - - for change in params.content_changes { - let range = match convert_range(&heads.index, change.range) { - Ok(range) => range, - Err(e) => { - error!(error = as_debug!(e); "invalid change"); - continue; - } - }; - - let start = heads.rope.byte_to_char(range.start().into()); - let end = heads.rope.byte_to_char(range.end().into()); - heads.rope.remove(start..end); - heads.rope.insert(start, &change.text); - } - - let text = String::from(&heads.rope); - - FileBuilder { - rope: heads.rope, - index: LineIndex::new(&text), - content: text, - schema_builder: |index, schema| { - compile::compile(params.text_document.uri.clone(), schema, index) - }, - } - .build() - }; - - if let Err(e) = self.publish_diagnostics( - params.text_document.uri.clone(), - file.borrow_schema() - .as_ref() - .err() - .map(|diag| vec![diag.clone()]) - .unwrap_or_default(), - None, - ) { - error!(error = as_debug!(e); "failed publishing diagnostics"); - } - - self.files.insert(params.text_document.uri, file); - } - - fn did_close(&mut self, params: DidCloseTextDocumentParams) { - debug!(uri = as_display!(params.text_document.uri); "schema closed"); - self.files.remove(¶ms.text_document.uri); - } - - fn semantic_tokens_full( - &mut self, - params: SemanticTokensParams, - ) -> Result> { - debug!(uri = as_display!(params.text_document.uri); "requested semantic tokens"); - Ok(None) - } - - fn did_change_configuration(&mut self, _params: DidChangeConfigurationParams) { - debug!("configuration changed"); - - if let Err(e) = self.reload_settings() { - error!(error = as_debug!(e); "failed loading changed settings"); - } - } -} +mod state; fn main() -> Result<()> { let cli = Cli::parse(); @@ -381,24 +40,20 @@ fn main() -> Result<()> { bail!("no connection method provided") }; - let mut server = Backend { - conn: Connection { - sender: connection.sender.clone(), - receiver: connection.receiver.clone(), - }, + let mut state = GlobalState { + client: Client::new(&connection), files: HashMap::default(), settings: config::Global::default(), - next_id: 0, }; let (id, params) = connection.initialize_start()?; let init_params = serde_json::from_value::(params)?; - let init_result = server.initialize(init_params)?; + let init_result = handlers::initialize(&mut state, init_params)?; connection.initialize_finish(id, serde_json::to_value(init_result)?)?; info!("server initialized"); - if let Err(e) = main_loop(&connection, server) { + if let Err(e) = main_loop(&connection, state) { error!(error = as_debug!(e); "error in main loop"); return Err(e); } @@ -411,7 +66,7 @@ fn main() -> Result<()> { Ok(()) } -fn main_loop(conn: &Connection, mut server: impl LanguageServer) -> Result<()> { +fn main_loop(conn: &Connection, mut state: GlobalState<'_>) -> Result<()> { for msg in &conn.receiver { match msg { lsp_server::Message::Request(req) => { @@ -422,11 +77,11 @@ fn main_loop(conn: &Connection, mut server: impl LanguageServer) -> Result<()> { match req.method.as_str() { Shutdown::METHOD => { - server.shutdown()?; + warn!("should never reach this"); } SemanticTokensFullRequest::METHOD => { let (id, params) = cast_req::(req)?; - let result = server.semantic_tokens_full(params); + let result = handlers::semantic_tokens_full(&mut state, params); conn.sender.send( match result { @@ -464,19 +119,22 @@ fn main_loop(conn: &Connection, mut server: impl LanguageServer) -> Result<()> { } lsp_server::Message::Notification(notif) => match notif.method.as_str() { Initialized::METHOD => { - server.initialized(cast_notify::(notif)?); + handlers::initialized(&mut state, cast_notify::(notif)?); } DidOpenTextDocument::METHOD => { - server.did_open(cast_notify::(notif)?); + handlers::did_open(&mut state, cast_notify::(notif)?); } DidChangeTextDocument::METHOD => { - server.did_change(cast_notify::(notif)?); + handlers::did_change(&mut state, cast_notify::(notif)?); } DidCloseTextDocument::METHOD => { - server.did_close(cast_notify::(notif)?); + handlers::did_close(&mut state, cast_notify::(notif)?); } DidChangeConfiguration::METHOD => { - server.did_change_configuration(cast_notify::(notif)?); + handlers::did_change_configuration( + &mut state, + cast_notify::(notif)?, + ); } _ => debug!(notification = as_debug!(notif); "got unknown notification"), }, @@ -501,37 +159,3 @@ where { notif.extract(R::METHOD).map_err(Into::into) } - -fn convert_range(index: &LineIndex, range: Option) -> Result { - let range = range.context("incremental change misses range")?; - - let start = index - .offset( - index - .to_utf8( - line_index::WideEncoding::Utf16, - line_index::WideLineCol { - line: range.start.line, - col: range.start.character, - }, - ) - .context("failed to convert start position to utf-8")?, - ) - .context("failed to convert start position to byte offset")?; - - let end = index - .offset( - index - .to_utf8( - line_index::WideEncoding::Utf16, - line_index::WideLineCol { - line: range.end.line, - col: range.end.character, - }, - ) - .context("failed to convert end position to utf-8")?, - ) - .context("failed to convert end position to byte offset")?; - - Ok(TextRange::new(start, end)) -} diff --git a/crates/stef-lsp/src/state.rs b/crates/stef-lsp/src/state.rs new file mode 100644 index 0000000..a028150 --- /dev/null +++ b/crates/stef-lsp/src/state.rs @@ -0,0 +1,55 @@ +use std::collections::HashMap; + +use anyhow::{ensure, Context, Result}; +use line_index::LineIndex; +use log::{as_debug, debug}; +use lsp_types::{ConfigurationItem, Diagnostic, Url}; +use ouroboros::self_referencing; +use ropey::Rope; +use stef_parser::Schema; + +use crate::{client::Client, config}; + +#[allow(clippy::module_name_repetitions)] +pub struct GlobalState<'a> { + pub client: Client<'a>, + pub files: HashMap, + pub settings: config::Global, +} + +#[self_referencing(pub_extras)] +#[derive(Debug)] +pub struct File { + rope: Rope, + index: LineIndex, + content: String, + #[borrows(index, content)] + #[covariant] + pub schema: Result, Diagnostic>, +} + +impl GlobalState<'_> { + pub fn reload_settings(&mut self) -> Result<()> { + let mut settings = self + .client + .configuration(vec![ConfigurationItem { + scope_uri: None, + section: Some("stef".to_owned()), + }]) + .context("failed getting configuration from client")?; + + ensure!( + settings.len() == 1, + "configuration contains not exactly one object" + ); + + let settings = serde_json::from_value(settings.remove(0)) + .context("failed to parse raw configuration")?; + + debug!(settings = as_debug!(settings); "configuration loaded"); + + self.settings = settings; + + Ok(()) + } +}