feat: Use builder pattern and log unhandled notifications (#84)
Some checks are pending
Build and Test / build (push) Waiting to run

Potentially fixes #83

Prior to this, any unhandled notification not starting with `$` would
have caused the main loop to exit with error resulting in server to
crash. Moreover, this unhandled_notification handler only works with
builder pattern which imo is cleaner and offers ability to separate out
implementation of various requests into its own files (i'll maybe do
that someday).
This commit is contained in:
Mohammad Ashar Khan 2025-06-06 03:26:50 +05:30 committed by GitHub
parent a15cfb7c00
commit dd71d54555
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 106 additions and 72 deletions

2
Cargo.lock generated
View file

@ -792,7 +792,7 @@ dependencies = [
[[package]]
name = "protols"
version = "0.12.5"
version = "0.12.6"
dependencies = [
"async-lsp",
"basic-toml",

View file

@ -1,7 +1,7 @@
[package]
name = "protols"
description = "Language server for proto3 files"
version = "0.12.5"
version = "0.12.6"
edition = "2024"
license = "MIT"
homepage = "https://github.com/coder3101/protols"
@ -12,8 +12,8 @@ keywords = ["lsp", "proto"]
exclude = ["assets/*", "sample/*"]
[dependencies]
async-lsp = { version = "0.2.0", features = ["tokio"] }
futures = "0.3.30"
async-lsp = { version = "0.2.2", features = ["tokio"] }
futures = "0.3.31"
tokio = { version = "1.38.0", features = ["time", "full"] }
tokio-util = { version = "0.7.11", features = ["compat"] }
tower = "0.5.2"

View file

@ -4,34 +4,30 @@ use tracing::{error, info};
use async_lsp::lsp_types::{
CompletionItem, CompletionItemKind, CompletionOptions, CompletionParams, CompletionResponse,
CreateFilesParams, DeleteFilesParams, DidChangeConfigurationParams,
DidChangeTextDocumentParams, DidChangeWorkspaceFoldersParams, DidCloseTextDocumentParams,
DidOpenTextDocumentParams, DidSaveTextDocumentParams, DocumentFormattingParams,
DocumentRangeFormattingParams, DocumentSymbolParams, DocumentSymbolResponse, Documentation,
FileOperationFilter, FileOperationPattern, FileOperationPatternKind,
FileOperationRegistrationOptions, GotoDefinitionParams, GotoDefinitionResponse, Hover,
HoverContents, HoverParams, HoverProviderCapability, InitializeParams, InitializeResult,
Location, MarkupContent, MarkupKind, OneOf, PrepareRenameResponse, ReferenceParams,
RenameFilesParams, RenameOptions, RenameParams, ServerCapabilities, ServerInfo,
TextDocumentPositionParams, TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit, Url,
WorkspaceEdit, WorkspaceFileOperationsServerCapabilities, WorkspaceFoldersServerCapabilities,
CreateFilesParams, DeleteFilesParams, DidChangeTextDocumentParams, DidOpenTextDocumentParams,
DidSaveTextDocumentParams, DocumentFormattingParams, DocumentRangeFormattingParams,
DocumentSymbolParams, DocumentSymbolResponse, Documentation, FileOperationFilter,
FileOperationPattern, FileOperationPatternKind, FileOperationRegistrationOptions,
GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, HoverParams,
HoverProviderCapability, InitializeParams, InitializeResult, Location, MarkupContent,
MarkupKind, OneOf, PrepareRenameResponse, ReferenceParams, RenameFilesParams, RenameOptions,
RenameParams, ServerCapabilities, ServerInfo, TextDocumentPositionParams,
TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit, Url, WorkspaceEdit,
WorkspaceFileOperationsServerCapabilities, WorkspaceFoldersServerCapabilities,
WorkspaceServerCapabilities,
};
use async_lsp::{LanguageClient, LanguageServer, ResponseError};
use async_lsp::{LanguageClient, ResponseError};
use futures::future::BoxFuture;
use crate::docs;
use crate::formatter::ProtoFormatter;
use crate::server::ProtoLanguageServer;
impl LanguageServer for ProtoLanguageServer {
type Error = ResponseError;
type NotifyResult = ControlFlow<async_lsp::Result<()>>;
fn initialize(
impl ProtoLanguageServer {
pub(super) fn initialize(
&mut self,
params: InitializeParams,
) -> BoxFuture<'static, Result<InitializeResult, Self::Error>> {
) -> BoxFuture<'static, Result<InitializeResult, ResponseError>> {
let (cname, version) = params
.client_info
.as_ref()
@ -122,10 +118,10 @@ impl LanguageServer for ProtoLanguageServer {
Box::pin(async move { Ok(response) })
}
fn hover(
pub(super) fn hover(
&mut self,
param: HoverParams,
) -> BoxFuture<'static, Result<Option<Hover>, Self::Error>> {
) -> BoxFuture<'static, Result<Option<Hover>, ResponseError>> {
let uri = param.text_document_position_params.text_document.uri;
let pos = param.text_document_position_params.position;
@ -154,10 +150,10 @@ impl LanguageServer for ProtoLanguageServer {
})
}
fn completion(
pub(super) fn completion(
&mut self,
params: CompletionParams,
) -> BoxFuture<'static, Result<Option<CompletionResponse>, Self::Error>> {
) -> BoxFuture<'static, Result<Option<CompletionResponse>, ResponseError>> {
let uri = params.text_document_position.text_document.uri;
// All keywords in the language
@ -203,10 +199,10 @@ impl LanguageServer for ProtoLanguageServer {
Box::pin(async move { Ok(Some(CompletionResponse::Array(completions))) })
}
fn prepare_rename(
pub(super) fn prepare_rename(
&mut self,
params: TextDocumentPositionParams,
) -> BoxFuture<'static, Result<Option<PrepareRenameResponse>, Self::Error>> {
) -> BoxFuture<'static, Result<Option<PrepareRenameResponse>, ResponseError>> {
let uri = params.text_document.uri;
let pos = params.position;
@ -220,10 +216,10 @@ impl LanguageServer for ProtoLanguageServer {
Box::pin(async move { Ok(response) })
}
fn rename(
pub(super) fn rename(
&mut self,
params: RenameParams,
) -> BoxFuture<'static, Result<Option<WorkspaceEdit>, Self::Error>> {
) -> BoxFuture<'static, Result<Option<WorkspaceEdit>, ResponseError>> {
let uri = params.text_document_position.text_document.uri;
let pos = params.text_document_position.position;
@ -271,7 +267,7 @@ impl LanguageServer for ProtoLanguageServer {
Box::pin(async move { Ok(response) })
}
fn references(
pub(super) fn references(
&mut self,
param: ReferenceParams,
) -> BoxFuture<'static, Result<Option<Vec<Location>>, ResponseError>> {
@ -318,7 +314,7 @@ impl LanguageServer for ProtoLanguageServer {
})
}
fn definition(
pub(super) fn definition(
&mut self,
param: GotoDefinitionParams,
) -> BoxFuture<'static, Result<Option<GotoDefinitionResponse>, ResponseError>> {
@ -353,10 +349,10 @@ impl LanguageServer for ProtoLanguageServer {
Box::pin(async move { Ok(response) })
}
fn document_symbol(
pub(super) fn document_symbol(
&mut self,
params: DocumentSymbolParams,
) -> BoxFuture<'static, Result<Option<DocumentSymbolResponse>, Self::Error>> {
) -> BoxFuture<'static, Result<Option<DocumentSymbolResponse>, ResponseError>> {
let uri = params.text_document.uri;
let Some(tree) = self.state.get_tree(&uri) else {
@ -371,10 +367,10 @@ impl LanguageServer for ProtoLanguageServer {
Box::pin(async move { Ok(Some(response)) })
}
fn formatting(
pub(super) fn formatting(
&mut self,
params: DocumentFormattingParams,
) -> BoxFuture<'static, Result<Option<Vec<TextEdit>>, Self::Error>> {
) -> BoxFuture<'static, Result<Option<Vec<TextEdit>>, ResponseError>> {
let uri = params.text_document.uri;
let content = self.state.get_content(&uri);
@ -386,10 +382,10 @@ impl LanguageServer for ProtoLanguageServer {
Box::pin(async move { Ok(response) })
}
fn range_formatting(
pub(super) fn range_formatting(
&mut self,
params: DocumentRangeFormattingParams,
) -> BoxFuture<'static, Result<Option<Vec<TextEdit>>, Self::Error>> {
) -> BoxFuture<'static, Result<Option<Vec<TextEdit>>, ResponseError>> {
let uri = params.text_document.uri;
let content = self.state.get_content(&uri);
@ -401,7 +397,10 @@ impl LanguageServer for ProtoLanguageServer {
Box::pin(async move { Ok(response) })
}
fn did_save(&mut self, params: DidSaveTextDocumentParams) -> Self::NotifyResult {
pub(super) fn did_save(
&mut self,
params: DidSaveTextDocumentParams,
) -> ControlFlow<async_lsp::Result<()>> {
let uri = params.text_document.uri;
let content = self.state.get_content(&uri);
@ -424,11 +423,10 @@ impl LanguageServer for ProtoLanguageServer {
ControlFlow::Continue(())
}
fn did_close(&mut self, _params: DidCloseTextDocumentParams) -> Self::NotifyResult {
ControlFlow::Continue(())
}
fn did_open(&mut self, params: DidOpenTextDocumentParams) -> Self::NotifyResult {
pub(super) fn did_open(
&mut self,
params: DidOpenTextDocumentParams,
) -> ControlFlow<async_lsp::Result<()>> {
let uri = params.text_document.uri;
let content = params.text_document.text;
@ -451,7 +449,10 @@ impl LanguageServer for ProtoLanguageServer {
ControlFlow::Continue(())
}
fn did_change(&mut self, params: DidChangeTextDocumentParams) -> Self::NotifyResult {
pub(super) fn did_change(
&mut self,
params: DidChangeTextDocumentParams,
) -> ControlFlow<async_lsp::Result<()>> {
let uri = params.text_document.uri;
let content = params.content_changes[0].text.clone();
@ -474,7 +475,10 @@ impl LanguageServer for ProtoLanguageServer {
ControlFlow::Continue(())
}
fn did_create_files(&mut self, params: CreateFilesParams) -> Self::NotifyResult {
pub(super) fn did_create_files(
&mut self,
params: CreateFilesParams,
) -> ControlFlow<async_lsp::Result<()>> {
for file in params.files {
if let Ok(uri) = Url::from_file_path(&file.uri) {
// Safety: The uri is always a file type
@ -488,7 +492,10 @@ impl LanguageServer for ProtoLanguageServer {
ControlFlow::Continue(())
}
fn did_rename_files(&mut self, params: RenameFilesParams) -> Self::NotifyResult {
pub(super) fn did_rename_files(
&mut self,
params: RenameFilesParams,
) -> ControlFlow<async_lsp::Result<()>> {
for file in params.files {
let Ok(new_uri) = Url::from_file_path(&file.new_uri) else {
error!(uri = file.new_uri, "failed to parse uri");
@ -505,7 +512,10 @@ impl LanguageServer for ProtoLanguageServer {
ControlFlow::Continue(())
}
fn did_delete_files(&mut self, params: DeleteFilesParams) -> Self::NotifyResult {
pub(super) fn did_delete_files(
&mut self,
params: DeleteFilesParams,
) -> ControlFlow<async_lsp::Result<()>> {
for file in params.files {
if let Ok(uri) = Url::from_file_path(&file.uri) {
self.state.delete_file(&uri);
@ -515,17 +525,4 @@ impl LanguageServer for ProtoLanguageServer {
}
ControlFlow::Continue(())
}
// Required because of: https://github.com/coder3101/protols/issues/32
fn did_change_configuration(&mut self, _: DidChangeConfigurationParams) -> Self::NotifyResult {
ControlFlow::Continue(())
}
// Required because when jumping to outside the workspace; this is triggered
fn did_change_workspace_folders(
&mut self,
_: DidChangeWorkspaceFoldersParams,
) -> Self::NotifyResult {
ControlFlow::Continue(())
}
}

View file

@ -37,7 +37,7 @@ async fn main() {
let cli = Cli::parse();
let dir = std::env::temp_dir();
eprintln!("Rolling file based logging at directory: {dir:?}");
eprintln!("file logging at directory: {dir:?}");
let file_appender = tracing_appender::rolling::daily(dir.clone(), "protols.log");
let file_appender = tracing_appender::non_blocking(file_appender);
@ -48,9 +48,10 @@ async fn main() {
.with_writer(file_appender.0)
.init();
tracing::info!("server version: {}", env!("CARGO_PKG_VERSION"));
let (server, _) = async_lsp::MainLoop::new_server(|client| {
tracing::info!("Using CLI options: {:?}", cli);
let server = ProtoLanguageServer::new_router(
let router = ProtoLanguageServer::new_router(
client.clone(),
cli.include_paths
.map(|ic| ic.into_iter().map(std::path::PathBuf::from).collect())
@ -76,7 +77,7 @@ async fn main() {
.layer(CatchUnwindLayer::default())
.layer(ConcurrencyLayer::default())
.layer(ClientProcessMonitorLayer::new(client.clone()))
.service(server)
.service(router)
});
// Prefer truly asynchronous piped stdin/stdout without blocking tasks.

View file

@ -1,6 +1,16 @@
use async_lsp::{
ClientSocket, LanguageClient,
lsp_types::{NumberOrString, ProgressParams, ProgressParamsValue},
lsp_types::{
NumberOrString, ProgressParams, ProgressParamsValue,
notification::{
DidChangeTextDocument, DidCreateFiles, DidDeleteFiles, DidOpenTextDocument,
DidRenameFiles, DidSaveTextDocument,
},
request::{
Completion, DocumentSymbolRequest, Formatting, GotoDefinition, HoverRequest,
Initialize, PrepareRenameRequest, RangeFormatting, References, Rename,
},
},
router::Router,
};
use std::{
@ -22,19 +32,45 @@ pub struct ProtoLanguageServer {
impl ProtoLanguageServer {
pub fn new_router(client: ClientSocket, cli_include_paths: Vec<PathBuf>) -> Router<Self> {
let mut router = Router::from_language_server(Self {
let mut router = Router::new(Self {
client,
counter: 0,
state: ProtoLanguageState::new(),
configs: WorkspaceProtoConfigs::new(cli_include_paths),
});
router.event(Self::on_tick);
router
}
fn on_tick(&mut self, _: TickEvent) -> ControlFlow<async_lsp::Result<()>> {
self.counter += 1;
ControlFlow::Continue(())
router.event::<TickEvent>(|st, _| {
st.counter += 1;
ControlFlow::Continue(())
});
// Ignore any unknown notification.
router.unhandled_notification(|_, notif| {
tracing::info!(notif.method, "ignored unknown notification");
ControlFlow::Continue(())
});
// Handling request
router.request::<Initialize, _>(|st, params| st.initialize(params));
router.request::<HoverRequest, _>(|st, params| st.hover(params));
router.request::<Completion, _>(|st, params| st.completion(params));
router.request::<PrepareRenameRequest, _>(|st, params| st.prepare_rename(params));
router.request::<Rename, _>(|st, params| st.rename(params));
router.request::<References, _>(|st, params| st.references(params));
router.request::<GotoDefinition, _>(|st, params| st.definition(params));
router.request::<DocumentSymbolRequest, _>(|st, params| st.document_symbol(params));
router.request::<Formatting, _>(|st, params| st.formatting(params));
router.request::<RangeFormatting, _>(|st, params| st.range_formatting(params));
// Handling notification
router.notification::<DidSaveTextDocument>(|st, params| st.did_save(params));
router.notification::<DidOpenTextDocument>(|st, params| st.did_open(params));
router.notification::<DidChangeTextDocument>(|st, params| st.did_change(params));
router.notification::<DidCreateFiles>(|st, params| st.did_create_files(params));
router.notification::<DidRenameFiles>(|st, params| st.did_rename_files(params));
router.notification::<DidDeleteFiles>(|st, params| st.did_delete_files(params));
router
}
pub fn with_report_progress(&self, token: NumberOrString) -> Sender<ProgressParamsValue> {