Skip to content

Commit

Permalink
refactor(lsp): move elements out of main.rs
Browse files Browse the repository at this point in the history
  • Loading branch information
dnaka91 committed Dec 13, 2023
1 parent a9836cb commit 225a683
Show file tree
Hide file tree
Showing 4 changed files with 432 additions and 403 deletions.
93 changes: 93 additions & 0 deletions crates/stef-lsp/src/client.rs
Original file line number Diff line number Diff line change
@@ -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<T>(&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<T>(&mut self, params: T::Params) -> Result<T::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<Diagnostic>,
version: Option<i32>,
) -> Result<()> {
self.send_notification::<PublishDiagnostics>(PublishDiagnosticsParams {
uri,
diagnostics,
version,
})
}

pub fn configuration(
&mut self,
items: Vec<ConfigurationItem>,
) -> Result<Vec<serde_json::Value>> {
self.send_request::<WorkspaceConfiguration>(ConfigurationParams { items })
}

pub fn register_capability(&mut self, registrations: Vec<Registration>) -> Result<()> {
self.send_request::<RegisterCapability>(RegistrationParams { registrations })
}
}
257 changes: 257 additions & 0 deletions crates/stef-lsp/src/handlers.rs
Original file line number Diff line number Diff line change
@@ -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<InitializeResult> {
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(&params.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(&params.text_document.uri);
}

pub fn semantic_tokens_full(
_state: &mut GlobalState<'_>,
params: SemanticTokensParams,
) -> Result<Option<SemanticTokensResult>> {
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<lsp_types::Range>) -> Result<TextRange> {
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))
}
Loading

0 comments on commit 225a683

Please sign in to comment.