Skip to content

Commit 485cb97

Browse files
committed
feat(lsp): add LSP server
1 parent 50926ee commit 485cb97

File tree

6 files changed

+233
-5
lines changed

6 files changed

+233
-5
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
runs-on: ${{ matrix.os }}
2929
strategy:
3030
matrix:
31-
rust: [1.70.0, stable]
31+
rust: [1.71.1, stable]
3232
os: [ubuntu-latest, macOS-latest, windows-latest]
3333

3434
steps:

Cargo.toml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,22 @@ span = []
1717
v1-fallback = ["v1"]
1818
v1 = ["kdlv1"]
1919

20+
[workspace]
21+
members = ["tools/*"]
22+
2023
[dependencies]
21-
miette = "7.2.0"
24+
miette.workspace = true
25+
thiserror.workspace = true
2226
num = "0.4.2"
23-
thiserror = "1.0.40"
2427
winnow = { version = "0.6.20", features = ["alloc", "unstable-recover"] }
2528
kdlv1 = { package = "kdl", version = "4.7.0", optional = true }
2629

30+
[workspace.dependencies]
31+
miette = "7.2.0"
32+
thiserror = "1.0.40"
33+
2734
[dev-dependencies]
28-
miette = { version = "7.2.0", features = ["fancy"] }
35+
miette = { workspace = true, features = ["fancy"] }
2936
pretty_assertions = "1.3.0"
3037

3138
# docs.rs-specific configuration

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ means a few things:
142142

143143
### Minimum Supported Rust Version
144144

145-
You must be at least `1.70.0` tall to get on this ride.
145+
You must be at least `1.71.1` tall to get on this ride.
146146

147147
### License
148148

tools/kdl-lsp/Cargo.toml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[package]
2+
name = "kdl-lsp"
3+
version = "6.2.2"
4+
edition = "2021"
5+
description = "LSP Server for the KDL Document Language"
6+
authors = ["Kat Marchán <[email protected]>", "KDL Community"]
7+
license = "Apache-2.0"
8+
readme = "README.md"
9+
homepage = "https://kdl.dev"
10+
repository = "https://github.com/kdl-org/kdl-rs"
11+
keywords = ["kdl", "document", "serialization", "config", "lsp", "language server"]
12+
rust-version = "1.70.0"
13+
14+
[dependencies]
15+
miette.workspace = true
16+
kdl = { version = "6.2.2", path = "../../", features = ["span", "v1-fallback"] }
17+
tower-lsp = "0.20.0"
18+
dashmap = "6.1.0"
19+
ropey = "1.6.1"
20+
tokio = { version = "1.43.0", features = ["full"] }
21+
tracing = "0.1.41"
22+
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }

tools/kdl-lsp/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# `kdl-lsp`
2+
3+
This is an LSP server for KDL.

tools/kdl-lsp/src/main.rs

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
use dashmap::DashMap;
2+
use kdl::{KdlDocument, KdlError};
3+
use miette::Diagnostic as _;
4+
use ropey::Rope;
5+
use tower_lsp::jsonrpc::Result;
6+
use tower_lsp::lsp_types::*;
7+
use tower_lsp::{Client, LanguageServer, LspService, Server};
8+
use tracing_subscriber::prelude::*;
9+
use tracing_subscriber::EnvFilter;
10+
11+
#[derive(Debug)]
12+
struct Backend {
13+
client: Client,
14+
document_map: DashMap<String, Rope>,
15+
}
16+
17+
impl Backend {
18+
async fn on_change(&self, uri: Url, text: &str) {
19+
let rope = ropey::Rope::from_str(text);
20+
self.document_map.insert(uri.to_string(), rope.clone());
21+
}
22+
}
23+
24+
#[tower_lsp::async_trait]
25+
impl LanguageServer for Backend {
26+
async fn initialize(&self, _: InitializeParams) -> Result<InitializeResult> {
27+
Ok(InitializeResult {
28+
capabilities: ServerCapabilities {
29+
text_document_sync: Some(TextDocumentSyncCapability::Options(
30+
TextDocumentSyncOptions {
31+
open_close: Some(true),
32+
change: Some(TextDocumentSyncKind::FULL),
33+
save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions {
34+
include_text: Some(true),
35+
})),
36+
..Default::default()
37+
},
38+
)),
39+
workspace: Some(WorkspaceServerCapabilities {
40+
workspace_folders: Some(WorkspaceFoldersServerCapabilities {
41+
supported: Some(true),
42+
change_notifications: Some(OneOf::Left(true)),
43+
}),
44+
file_operations: None,
45+
}),
46+
diagnostic_provider: Some(DiagnosticServerCapabilities::RegistrationOptions(
47+
DiagnosticRegistrationOptions {
48+
text_document_registration_options: TextDocumentRegistrationOptions {
49+
document_selector: Some(vec![DocumentFilter {
50+
language: Some("kdl".into()),
51+
scheme: Some("file".into()),
52+
pattern: None,
53+
}]),
54+
},
55+
..Default::default()
56+
},
57+
)),
58+
// hover_provider: Some(HoverProviderCapability::Simple(true)),
59+
// completion_provider: Some(Default::default()),
60+
..Default::default()
61+
},
62+
..Default::default()
63+
})
64+
}
65+
66+
async fn initialized(&self, _: InitializedParams) {
67+
self.client
68+
.log_message(MessageType::INFO, "server initialized!")
69+
.await;
70+
}
71+
72+
async fn shutdown(&self) -> Result<()> {
73+
self.client
74+
.log_message(MessageType::INFO, "server shutting down")
75+
.await;
76+
Ok(())
77+
}
78+
79+
async fn did_open(&self, params: DidOpenTextDocumentParams) {
80+
self.on_change(params.text_document.uri, &params.text_document.text)
81+
.await;
82+
}
83+
84+
async fn did_change(&self, params: DidChangeTextDocumentParams) {
85+
self.on_change(params.text_document.uri, &params.content_changes[0].text)
86+
.await;
87+
}
88+
89+
async fn did_save(&self, params: DidSaveTextDocumentParams) {
90+
if let Some(text) = params.text.as_ref() {
91+
self.on_change(params.text_document.uri, text).await;
92+
}
93+
}
94+
95+
async fn did_close(&self, params: DidCloseTextDocumentParams) {
96+
self.document_map
97+
.remove(&params.text_document.uri.to_string());
98+
}
99+
100+
async fn diagnostic(
101+
&self,
102+
params: DocumentDiagnosticParams,
103+
) -> Result<DocumentDiagnosticReportResult> {
104+
tracing::debug!("diagnostic req");
105+
if let Some(doc) = self.document_map.get(&params.text_document.uri.to_string()) {
106+
let res: std::result::Result<KdlDocument, KdlError> = doc.to_string().parse();
107+
if let Err(kdl_err) = res {
108+
let diags = kdl_err
109+
.diagnostics
110+
.into_iter()
111+
.map(|diag| {
112+
Diagnostic::new(
113+
Range::new(
114+
char_to_position(diag.span.offset(), &doc),
115+
char_to_position(diag.span.offset() + diag.span.len(), &doc),
116+
),
117+
diag.severity().map(to_lsp_sev),
118+
diag.code().map(|c| NumberOrString::String(c.to_string())),
119+
None,
120+
diag.to_string(),
121+
None,
122+
None,
123+
)
124+
})
125+
.collect();
126+
return Ok(DocumentDiagnosticReportResult::Report(
127+
DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport {
128+
related_documents: None,
129+
full_document_diagnostic_report: FullDocumentDiagnosticReport {
130+
result_id: None,
131+
items: diags,
132+
},
133+
}),
134+
));
135+
}
136+
}
137+
Ok(DocumentDiagnosticReportResult::Report(
138+
DocumentDiagnosticReport::Full(RelatedFullDocumentDiagnosticReport::default()),
139+
))
140+
}
141+
142+
// TODO(@zkat): autocomplete #-keywords
143+
// TODO(@zkat): autocomplete schema stuff
144+
// async fn completion(&self, _: CompletionParams) -> Result<Option<CompletionResponse>> {
145+
// tracing::debug!("Completion request");
146+
// Ok(Some(CompletionResponse::Array(vec![
147+
// CompletionItem::new_simple("Hello".to_string(), "Some detail".to_string()),
148+
// CompletionItem::new_simple("Bye".to_string(), "More detail".to_string()),
149+
// ])))
150+
// }
151+
152+
// TODO(@zkat): We'll use this when we actually do schema stuff.
153+
// async fn hover(&self, _: HoverParams) -> Result<Option<Hover>> {
154+
// tracing::debug!("Hover request");
155+
// Ok(Some(Hover {
156+
// contents: HoverContents::Scalar(MarkedString::String("You're hovering!".to_string())),
157+
// range: None,
158+
// }))
159+
// }
160+
}
161+
162+
fn char_to_position(char_idx: usize, rope: &Rope) -> Position {
163+
let line_idx = rope.char_to_line(char_idx);
164+
let line_char_idx = rope.line_to_char(line_idx);
165+
let column_idx = char_idx - line_char_idx;
166+
Position::new(line_idx as u32, column_idx as u32)
167+
}
168+
169+
fn to_lsp_sev(sev: miette::Severity) -> DiagnosticSeverity {
170+
match sev {
171+
miette::Severity::Advice => DiagnosticSeverity::HINT,
172+
miette::Severity::Warning => DiagnosticSeverity::WARNING,
173+
miette::Severity::Error => DiagnosticSeverity::ERROR,
174+
}
175+
}
176+
177+
#[tokio::main]
178+
async fn main() {
179+
tracing_subscriber::registry()
180+
.with(
181+
tracing_subscriber::fmt::layer()
182+
.map_writer(move |_| std::io::stderr)
183+
.with_ansi(false),
184+
)
185+
.with(EnvFilter::from_default_env())
186+
.init();
187+
188+
let stdin = tokio::io::stdin();
189+
let stdout = tokio::io::stdout();
190+
191+
let (service, socket) = LspService::new(|client| Backend {
192+
client,
193+
document_map: DashMap::new(),
194+
});
195+
Server::new(stdin, stdout, socket).serve(service).await;
196+
}

0 commit comments

Comments
 (0)