use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use tempfile::TempDir;
use super::Translator;
use super::dto::{BoundedRange, Position, PositionRange};
use super::encoding_ctx::EncodingCtx;
use crate::bridge::encoding::PositionEncoding;
use crate::bridge::state::ResourceLimits;
use crate::bridge::{DiagnosticInfo, DocumentTracker, WorkspaceRoots};
use crate::config::{FileExtension, LanguageId, ServerId, ToolRouter};
use crate::lsp::LspServer;
pub(super) use crate::test_lsp::{
FakeServer, fake_lsp_client, read_framed_message, write_error_response, write_response,
};
pub(super) fn canonical_dir(dir: &TempDir) -> PathBuf {
dunce::canonicalize(dir.path()).unwrap()
}
pub(super) fn pos(line: u32, character: u32) -> Position {
Position::at(line, character)
}
pub(super) fn span(start: Position, end: Position) -> PositionRange {
PositionRange::new(start, end).expect("fixture range is ordered")
}
pub(super) fn bounded(start: Position, end: Position) -> BoundedRange {
BoundedRange::try_from(span(start, end)).expect("fixture range is within the size cap")
}
pub(super) fn test_ctx() -> EncodingCtx {
test_ctx_with(PositionEncoding::Utf16)
}
pub(super) fn test_ctx_with(encoding: PositionEncoding) -> EncodingCtx {
test_ctx_with_roots(encoding, WorkspaceRoots::default())
}
pub(super) fn test_ctx_with_roots(
encoding: PositionEncoding,
workspace_roots: WorkspaceRoots,
) -> EncodingCtx {
EncodingCtx::new(
encoding,
Arc::new(DocumentTracker::new(
ResourceLimits::default(),
HashMap::new(),
)),
workspace_roots,
)
}
pub(super) fn test_uri() -> lsp_types::Uri {
lsp_types::Uri::from("file:///test.rs")
}
pub(super) fn test_tracker() -> Arc<DocumentTracker> {
Arc::new(DocumentTracker::new(
ResourceLimits::default(),
HashMap::new(),
))
}
pub(super) fn lsp_diag(
line: u32,
end_character: u32,
severity: lsp_types::DiagnosticSeverity,
message: &str,
code: Option<&str>,
) -> lsp_types::Diagnostic {
lsp_types::Diagnostic {
range: lsp_types::Range {
start: lsp_types::Position { line, character: 0 },
end: lsp_types::Position {
line,
character: end_character,
},
},
severity: Some(severity),
message: message.to_string().into(),
code: code.map(|c| lsp_types::Code::String(c.to_string())),
source: None,
code_description: None,
related_information: None,
tags: None,
data: None,
}
}
pub(super) fn diag_info(diagnostics: Vec<lsp_types::Diagnostic>) -> DiagnosticInfo {
DiagnosticInfo {
uri: lsp_types::Uri::from("file:///test.rs"),
version: Some(crate::bridge::DocumentVersion::FIRST),
diagnostics,
}
}
pub(super) fn translator_with_capabilities(
dir: &TempDir,
server_id: &ServerId,
capabilities: lsp_types::ServerCapabilities,
) -> (Translator, FakeServer) {
let mut extensions = HashMap::new();
extensions.insert(
FileExtension::from_static("rs"),
LanguageId::from_static("rust"),
);
let mut translator =
Translator::new()
.with_extensions(extensions)
.with_router(ToolRouter::catch_all([(
server_id.clone(),
LanguageId::from_static("rust"),
)]));
translator
.set_workspace_roots(WorkspaceRoots::from_paths(&[dir.path().to_path_buf()]).unwrap());
let (client, server) = fake_lsp_client();
translator.register_client(server_id.clone(), client);
translator.register_server(server_id.clone(), LspServer::new_for_test(capabilities));
(translator, server)
}
pub(super) fn translator_with_capabilities_and_encoding(
dir: &TempDir,
server_id: &ServerId,
capabilities: lsp_types::ServerCapabilities,
position_encoding: lsp_types::PositionEncodingKind,
) -> (Translator, FakeServer) {
let mut extensions = HashMap::new();
extensions.insert(
FileExtension::from_static("rs"),
LanguageId::from_static("rust"),
);
let mut translator =
Translator::new()
.with_extensions(extensions)
.with_router(ToolRouter::catch_all([(
server_id.clone(),
LanguageId::from_static("rust"),
)]));
translator
.set_workspace_roots(WorkspaceRoots::from_paths(&[dir.path().to_path_buf()]).unwrap());
let (client, server) = fake_lsp_client();
translator.register_client(server_id.clone(), client);
translator.register_server(
server_id.clone(),
LspServer::new_for_test_with_encoding(capabilities, position_encoding),
);
(translator, server)
}
#[cfg(unix)]
mod sh_servers {
use std::fs;
use std::path::{Path, PathBuf};
use crate::ServerId;
use crate::bridge::WorkspaceRoots;
use crate::config::{
LanguageId, LspServerConfig, PositionEncodings, ServerCommand, TimeoutSecs,
};
use crate::lsp::ServerInitConfig;
use crate::test_lsp::with_read_preamble;
pub(in crate::bridge::translator) fn write_crash_after_init_script(dir: &Path) -> PathBuf {
let script_path = dir.join("crash_after_init.sh");
let body = with_read_preamble(
r#"body='{"jsonrpc":"2.0","id":1,"result":{"capabilities":{}}}'
printf 'Content-Length: %d\r\n\r\n%s' ${#body} "$body"
sleep 0.3
"#,
);
fs::write(&script_path, body).unwrap();
script_path
}
pub(in crate::bridge::translator) fn write_responder_script(
dir: &Path,
sleep_secs: u64,
) -> PathBuf {
let script_path = dir.join("responder.sh");
let template = with_read_preamble(
r#"body='{"jsonrpc":"2.0","id":1,"result":{"capabilities":{}}}'
printf 'Content-Length: %d\r\n\r\n%s' ${#body} "$body"
sleep __SLEEP__
"#,
);
fs::write(
&script_path,
template.replace("__SLEEP__", &sleep_secs.to_string()),
)
.unwrap();
script_path
}
pub(in crate::bridge::translator) fn stub_server_config(
id: &str,
script: &Path,
) -> ServerInitConfig {
ServerInitConfig::new(
LspServerConfig {
language_id: LanguageId::new(id).unwrap(),
command: ServerCommand::from_static("sh").into(),
args: crate::config::ServerArgs::try_from(vec![
script.to_string_lossy().to_string(),
])
.unwrap(),
env: crate::config::ServerEnv::default(),
file_patterns: vec![],
initialization_options: None,
settings: None,
timeout_seconds: TimeoutSecs::new(20).unwrap(),
request_timeout_seconds: TimeoutSecs::new(20).unwrap(),
heuristics: None,
name: Some(ServerId::new(id).unwrap()),
handles: None,
indexing: crate::bridge::IndexingPolicy::Auto,
},
WorkspaceRoots::default(),
PositionEncodings::DEFAULT,
std::sync::Arc::default(),
)
}
pub(in crate::bridge::translator) fn pid_is_running(pid: u32) -> bool {
let output = std::process::Command::new("ps")
.args(["-o", "stat=", "-p", &pid.to_string()])
.output()
.unwrap();
let stat = String::from_utf8_lossy(&output.stdout);
let stat = stat.trim();
!stat.is_empty() && !stat.starts_with('Z')
}
const PROTOCOL_SERVER_BODY: &str = r#"LOG='__LOG__'
echo "started $$" >> "$LOG"
reply() { printf 'Content-Length: %d\r\n\r\n%s' ${#1} "$1"; }
read_msg() {
content_length=0
while IFS= read -r header; do
header=$(printf '%s' "$header" | tr -d '\r')
[ -z "$header" ] && break
case "$header" in
Content-Length:*) content_length=$(printf '%s' "$header" | sed 's/^Content-Length: *//') ;;
esac
done
msg=$(dd bs=1 count="$content_length" 2>/dev/null)
}
reply '{"jsonrpc":"2.0","id":1,"result":{"capabilities":{}}}'
__PUBLISH__
while true; do
read_msg
[ -z "$msg" ] && exit 0
echo "$msg" >> "$LOG"
case "$msg" in
*'"method":"shutdown"'*)
id=$(printf '%s' "$msg" | sed 's/.*"id":\([0-9]*\).*/\1/')
reply "{\"jsonrpc\":\"2.0\",\"id\":$id,\"result\":null}"
;;
*'"method":"exit"'*) sleep __EXIT_DELAY__; echo exiting >> "$LOG"; exit 0 ;;
esac
done
"#;
pub(in crate::bridge::translator) fn write_protocol_server_script(
dir: &Path,
log: &Path,
publish_uri: Option<&str>,
) -> PathBuf {
write_protocol_script(dir, log, publish_uri, 0)
}
pub(in crate::bridge::translator) fn write_slow_exit_server_script(
dir: &Path,
log: &Path,
exit_delay_secs: u32,
) -> PathBuf {
write_protocol_script(dir, log, None, exit_delay_secs)
}
fn write_protocol_script(
dir: &Path,
log: &Path,
publish_uri: Option<&str>,
exit_delay_secs: u32,
) -> PathBuf {
let publish = publish_uri.map_or_else(String::new, |uri| {
format!(
r#"reply '{{"jsonrpc":"2.0","method":"textDocument/publishDiagnostics","params":{{"uri":"{uri}","diagnostics":[{{"range":{{"start":{{"line":0,"character":0}},"end":{{"line":0,"character":1}}}},"message":"boom"}}]}}}}'"#
)
});
let script_path = dir.join("protocol_server.sh");
let body = with_read_preamble(
&PROTOCOL_SERVER_BODY
.replace("__LOG__", &log.display().to_string())
.replace("__PUBLISH__", &publish)
.replace("__EXIT_DELAY__", &exit_delay_secs.to_string()),
);
fs::write(&script_path, body).unwrap();
script_path
}
}
#[cfg(unix)]
pub(super) use sh_servers::{
pid_is_running, stub_server_config, write_crash_after_init_script,
write_protocol_server_script, write_responder_script, write_slow_exit_server_script,
};
pub(super) fn checked_hierarchy_item(json: serde_json::Value) -> super::dto::CheckedHierarchyItem {
super::dto::CheckedHierarchyItem::from_client(hierarchy_item(json))
.expect("fixture hierarchy item is valid")
}
pub(super) fn hierarchy_item(json: serde_json::Value) -> super::dto::HierarchyItem {
serde_json::from_value(json).expect("fixture hierarchy item is valid")
}