Skip to content

Commit

Permalink
feat(lsp): add LSP server
Browse files Browse the repository at this point in the history
  • Loading branch information
zkat committed Jan 17, 2025
1 parent 50926ee commit 485cb97
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 5 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
rust: [1.70.0, stable]
rust: [1.71.1, stable]
os: [ubuntu-latest, macOS-latest, windows-latest]

steps:
Expand Down
13 changes: 10 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,22 @@ span = []
v1-fallback = ["v1"]
v1 = ["kdlv1"]

[workspace]
members = ["tools/*"]

[dependencies]
miette = "7.2.0"
miette.workspace = true
thiserror.workspace = true
num = "0.4.2"
thiserror = "1.0.40"
winnow = { version = "0.6.20", features = ["alloc", "unstable-recover"] }
kdlv1 = { package = "kdl", version = "4.7.0", optional = true }

[workspace.dependencies]
miette = "7.2.0"
thiserror = "1.0.40"

[dev-dependencies]
miette = { version = "7.2.0", features = ["fancy"] }
miette = { workspace = true, features = ["fancy"] }
pretty_assertions = "1.3.0"

# docs.rs-specific configuration
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ means a few things:

### Minimum Supported Rust Version

You must be at least `1.70.0` tall to get on this ride.
You must be at least `1.71.1` tall to get on this ride.

### License

Expand Down
22 changes: 22 additions & 0 deletions tools/kdl-lsp/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "kdl-lsp"
version = "6.2.2"
edition = "2021"
description = "LSP Server for the KDL Document Language"
authors = ["Kat Marchán <[email protected]>", "KDL Community"]
license = "Apache-2.0"
readme = "README.md"
homepage = "https://kdl.dev"
repository = "https://github.com/kdl-org/kdl-rs"
keywords = ["kdl", "document", "serialization", "config", "lsp", "language server"]
rust-version = "1.70.0"

[dependencies]
miette.workspace = true
kdl = { version = "6.2.2", path = "../../", features = ["span", "v1-fallback"] }
tower-lsp = "0.20.0"
dashmap = "6.1.0"
ropey = "1.6.1"
tokio = { version = "1.43.0", features = ["full"] }
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
3 changes: 3 additions & 0 deletions tools/kdl-lsp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# `kdl-lsp`

This is an LSP server for KDL.
196 changes: 196 additions & 0 deletions tools/kdl-lsp/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
use dashmap::DashMap;
use kdl::{KdlDocument, KdlError};
use miette::Diagnostic as _;
use ropey::Rope;
use tower_lsp::jsonrpc::Result;
use tower_lsp::lsp_types::*;
use tower_lsp::{Client, LanguageServer, LspService, Server};
use tracing_subscriber::prelude::*;
use tracing_subscriber::EnvFilter;

#[derive(Debug)]
struct Backend {
client: Client,
document_map: DashMap<String, Rope>,
}

impl Backend {
async fn on_change(&self, uri: Url, text: &str) {
let rope = ropey::Rope::from_str(text);
self.document_map.insert(uri.to_string(), rope.clone());
}
}

#[tower_lsp::async_trait]
impl LanguageServer for Backend {
async fn initialize(&self, _: InitializeParams) -> Result<InitializeResult> {
Ok(InitializeResult {
capabilities: ServerCapabilities {
text_document_sync: Some(TextDocumentSyncCapability::Options(
TextDocumentSyncOptions {
open_close: Some(true),
change: Some(TextDocumentSyncKind::FULL),
save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions {
include_text: Some(true),
})),
..Default::default()
},
)),
workspace: Some(WorkspaceServerCapabilities {
workspace_folders: Some(WorkspaceFoldersServerCapabilities {
supported: Some(true),
change_notifications: Some(OneOf::Left(true)),
}),
file_operations: None,
}),
diagnostic_provider: Some(DiagnosticServerCapabilities::RegistrationOptions(
DiagnosticRegistrationOptions {
text_document_registration_options: TextDocumentRegistrationOptions {
document_selector: Some(vec![DocumentFilter {
language: Some("kdl".into()),
scheme: Some("file".into()),
pattern: None,
}]),
},
..Default::default()
},
)),
// hover_provider: Some(HoverProviderCapability::Simple(true)),
// completion_provider: Some(Default::default()),
..Default::default()
},
..Default::default()
})
}

async fn initialized(&self, _: InitializedParams) {
self.client
.log_message(MessageType::INFO, "server initialized!")
.await;
}

async fn shutdown(&self) -> Result<()> {
self.client
.log_message(MessageType::INFO, "server shutting down")
.await;
Ok(())
}

async fn did_open(&self, params: DidOpenTextDocumentParams) {
self.on_change(params.text_document.uri, &params.text_document.text)
.await;
}

async fn did_change(&self, params: DidChangeTextDocumentParams) {
self.on_change(params.text_document.uri, &params.content_changes[0].text)
.await;
}

async fn did_save(&self, params: DidSaveTextDocumentParams) {
if let Some(text) = params.text.as_ref() {
self.on_change(params.text_document.uri, text).await;
}
}

async fn did_close(&self, params: DidCloseTextDocumentParams) {
self.document_map
.remove(&params.text_document.uri.to_string());
}

async fn diagnostic(
&self,
params: DocumentDiagnosticParams,
) -> Result<DocumentDiagnosticReportResult> {
tracing::debug!("diagnostic req");
if let Some(doc) = self.document_map.get(&params.text_document.uri.to_string()) {
let res: std::result::Result<KdlDocument, KdlError> = doc.to_string().parse();
if let Err(kdl_err) = res {
let diags = kdl_err
.diagnostics
.into_iter()
.map(|diag| {
Diagnostic::new(
Range::new(
char_to_position(diag.span.offset(), &doc),
char_to_position(diag.span.offset() + diag.span.len(), &doc),
),
diag.severity().map(to_lsp_sev),
diag.code().map(|c| NumberOrString::String(c.to_string())),
None,
diag.to_string(),
None,
None,
)
})
.collect();
return Ok(DocumentDiagnosticReportResult::Report(
DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
related_documents: None,
full_document_diagnostic_report: FullDocumentDiagnosticReport {
result_id: None,
items: diags,
},
}),
));
}
}
Ok(DocumentDiagnosticReportResult::Report(
DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport::default()),
))
}

// TODO(@zkat): autocomplete #-keywords
// TODO(@zkat): autocomplete schema stuff
// async fn completion(&self, _: CompletionParams) -> Result<Option<CompletionResponse>> {
// tracing::debug!("Completion request");
// Ok(Some(CompletionResponse::Array(vec![
// CompletionItem::new_simple("Hello".to_string(), "Some detail".to_string()),
// CompletionItem::new_simple("Bye".to_string(), "More detail".to_string()),
// ])))
// }

// TODO(@zkat): We'll use this when we actually do schema stuff.
// async fn hover(&self, _: HoverParams) -> Result<Option<Hover>> {
// tracing::debug!("Hover request");
// Ok(Some(Hover {
// contents: HoverContents::Scalar(MarkedString::String("You're hovering!".to_string())),
// range: None,
// }))
// }
}

fn char_to_position(char_idx: usize, rope: &Rope) -> Position {
let line_idx = rope.char_to_line(char_idx);
let line_char_idx = rope.line_to_char(line_idx);
let column_idx = char_idx - line_char_idx;
Position::new(line_idx as u32, column_idx as u32)
}

fn to_lsp_sev(sev: miette::Severity) -> DiagnosticSeverity {
match sev {
miette::Severity::Advice => DiagnosticSeverity::HINT,
miette::Severity::Warning => DiagnosticSeverity::WARNING,
miette::Severity::Error => DiagnosticSeverity::ERROR,
}
}

#[tokio::main]
async fn main() {
tracing_subscriber::registry()
.with(
tracing_subscriber::fmt::layer()
.map_writer(move |_| std::io::stderr)
.with_ansi(false),
)
.with(EnvFilter::from_default_env())
.init();

let stdin = tokio::io::stdin();
let stdout = tokio::io::stdout();

let (service, socket) = LspService::new(|client| Backend {
client,
document_map: DashMap::new(),
});
Server::new(stdin, stdout, socket).serve(service).await;
}

0 comments on commit 485cb97

Please sign in to comment.