use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use increparse::prelude::*;
use increparse_lsp::{serve_on, Documents, NodeLabel, SimpleLanguage};
use lsp_server::{Connection, Message, Notification, Request, RequestId, Response};
use lsp_types::Diagnostic;
#[derive(Clone, Debug, PartialEq, Eq)]
enum Ctx {
File,
Region { name: &'static str },
}
struct Split;
impl Pass for Split {
type Ctx = Ctx;
fn parse(&self, source: &str, span: Span, ctx: &Ctx) -> Outcome<Ctx> {
if !matches!(ctx, Ctx::File) {
return Outcome::Failed;
}
let split = (span.start + 4).min(span.end);
let _head = source[split..].chars().next().unwrap_or('?');
Outcome::Expand(vec![
(
Span::new(span.start, split, span.rev),
Ctx::Region { name: "left" },
),
(
Span::new(split, span.end, span.rev),
Ctx::Region { name: "right" },
),
])
}
}
struct NoBang;
impl Pass for NoBang {
type Ctx = Ctx;
fn parse(&self, source: &str, span: Span, ctx: &Ctx) -> Outcome<Ctx> {
if matches!(ctx, Ctx::File) {
return Outcome::Failed;
}
if source[span.to_range()].contains('!') {
Outcome::Failed
} else {
Outcome::Done
}
}
}
fn language(calls: Arc<AtomicUsize>) -> SimpleLanguage<Ctx> {
SimpleLanguage::new(Engine::with((Split, NoBang)), Ctx::File)
.diagnostic_fn(move |_doc, _node| {
calls.fetch_add(1, Ordering::SeqCst);
Some(Diagnostic {
message: "bang".into(),
..Diagnostic::default()
})
})
.label_fn(|ctx| match ctx {
Ctx::File => None,
Ctx::Region { name } => Some(NodeLabel::new(*name)),
})
}
fn request(id: i64, method: &str, params: serde_json::Value) -> Message {
Message::Request(Request {
id: RequestId::from(id as i32),
method: method.into(),
params,
})
}
fn notification(method: &str, params: serde_json::Value) -> Message {
Message::Notification(Notification::new(method.into(), params))
}
fn next_response(connection: &Connection, id: i64) -> serde_json::Value {
for _ in 0..10 {
match connection.receiver.recv_timeout(Duration::from_secs(10)) {
Ok(Message::Response(Response {
id: got,
response_result,
})) => {
if got == RequestId::from(id as i32) {
return response_result.expect("response should be Ok");
}
}
Ok(_) => continue,
Err(err) => panic!("no server message: {err:?}"),
}
}
panic!("no response with id {id}");
}
fn next_diagnostics(connection: &Connection, version: i64) -> Vec<Diagnostic> {
for _ in 0..10 {
match connection.receiver.recv_timeout(Duration::from_secs(10)) {
Ok(Message::Notification(n)) => {
if n.method == "textDocument/publishDiagnostics"
&& n.params["version"].as_i64() == Some(version)
{
let diags: Vec<Diagnostic> =
serde_json::from_value(n.params["diagnostics"].clone()).unwrap();
return diags;
}
}
Ok(_) => continue,
Err(err) => panic!("no diagnostics: {err:?}"),
}
}
panic!("no diagnostics notification");
}
#[test]
fn simple_language_serves_end_to_end() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let (server, client) = Connection::memory();
let calls = Arc::new(AtomicUsize::new(0));
let calls2 = calls.clone();
let server_thread =
std::thread::spawn(move || serve_on::<Ctx, _>(server, language(calls2), Documents::new()));
client.sender.send(request(
1,
"initialize",
serde_json::json!({"capabilities": {}}),
))?;
let caps = next_response(&client, 1)["capabilities"].clone();
assert_eq!(caps["positionEncoding"], "utf-16");
assert_eq!(caps["documentSymbolProvider"], true);
client
.sender
.send(notification("initialized", serde_json::json!({})))?;
client.sender.send(notification(
"textDocument/didOpen",
serde_json::json!({
"textDocument": {"uri": "file:///s.mini", "languageId": "mini", "version": 0,
"text": "abcdef!ghij"},
}),
))?;
let diags = next_diagnostics(&client, 0);
assert_eq!(diags.len(), 1);
client.sender.send(request(
2,
"textDocument/documentSymbol",
serde_json::json!({"textDocument": {"uri": "file:///s.mini"}}),
))?;
let symbols = next_response(&client, 2);
let names: Vec<&str> = symbols
.as_array()
.unwrap()
.iter()
.map(|s| s["name"].as_str().unwrap())
.collect();
assert_eq!(names, ["left", "right"]);
client.sender.send(notification(
"textDocument/didChange",
serde_json::json!({
"textDocument": {"uri": "file:///s.mini", "version": 1},
"contentChanges": [{
"range": {"start": {"line": 0, "character": 6}, "end": {"line": 0, "character": 7}},
"text": "X",
}],
}),
))?;
let diags = next_diagnostics(&client, 1);
assert_eq!(diags.len(), 0);
client
.sender
.send(request(3, "shutdown", serde_json::Value::Null))?;
client
.sender
.send(notification("exit", serde_json::Value::Null))?;
server_thread.join().unwrap()?;
assert_eq!(calls.load(Ordering::SeqCst), 1);
Ok(())
}
#[test]
fn simple_language_without_hooks_advertises_no_symbols(
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let (server, client) = Connection::memory();
let server_thread = std::thread::spawn(move || {
serve_on::<Ctx, _>(
server,
SimpleLanguage::new(Engine::with((NoBang,)), Ctx::File),
Documents::new(),
)
});
client.sender.send(request(
1,
"initialize",
serde_json::json!({"capabilities": {}}),
))?;
let caps = next_response(&client, 1)["capabilities"].clone();
assert_eq!(caps["documentSymbolProvider"], false);
client
.sender
.send(notification("initialized", serde_json::json!({})))?;
client
.sender
.send(request(2, "shutdown", serde_json::Value::Null))?;
client
.sender
.send(notification("exit", serde_json::Value::Null))?;
server_thread.join().unwrap()?;
Ok(())
}