#![deny(clippy::print_stderr, clippy::print_stdout)]
#![cfg_attr(test, allow(clippy::print_stderr, clippy::print_stdout))]
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
fn validate_file_exists(path: &Path, description: &str) -> Result<()> {
if !path.exists() {
anyhow::bail!("{} does not exist: {}", description, path.display());
}
if !path.is_file() {
anyhow::bail!("{} is not a file: {}", description, path.display());
}
Ok(())
}
fn validate_directory_exists(path: &Path, description: &str) -> Result<()> {
if !path.exists() {
anyhow::bail!("{} does not exist: {}", description, path.display());
}
if !path.is_dir() {
anyhow::bail!("{} is not a directory: {}", description, path.display());
}
Ok(())
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LaunchConfiguration {
pub program: PathBuf,
#[serde(default)]
pub args: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cwd: Option<PathBuf>,
#[serde(default)]
pub env: HashMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub perl_path: Option<PathBuf>,
#[serde(default)]
pub include_paths: Vec<PathBuf>,
}
impl LaunchConfiguration {
pub fn resolve_paths(&mut self, workspace_root: &Path) -> Result<()> {
if !self.program.is_absolute() {
self.program = workspace_root.join(&self.program);
}
if let Some(ref mut cwd) = self.cwd
&& !cwd.is_absolute()
{
*cwd = workspace_root.join(&cwd);
}
for include_path in &mut self.include_paths {
if !include_path.is_absolute() {
*include_path = workspace_root.join(&include_path);
}
}
Ok(())
}
pub fn validate(&self) -> Result<()> {
validate_file_exists(&self.program, "Program file")?;
if let Some(ref cwd) = self.cwd {
validate_directory_exists(cwd, "Working directory")?;
}
if let Some(ref perl_path) = self.perl_path {
validate_file_exists(perl_path, "Perl binary")?;
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AttachConfiguration {
pub host: String,
pub port: u16,
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout_ms: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_on_entry: Option<bool>,
}
impl Default for AttachConfiguration {
fn default() -> Self {
Self {
host: "localhost".to_string(),
port: 13603,
timeout_ms: Some(5000),
stop_on_entry: None,
}
}
}
impl AttachConfiguration {
pub fn validate(&self) -> Result<()> {
if self.host.trim().is_empty() {
anyhow::bail!("Host cannot be empty");
}
if self.port == 0 {
anyhow::bail!("Port must be in range 1-65535");
}
if let Some(timeout) = self.timeout_ms {
if timeout == 0 {
anyhow::bail!("Timeout must be greater than 0 milliseconds");
}
if timeout > 300_000 {
anyhow::bail!("Timeout cannot exceed 300000 milliseconds (5 minutes)");
}
}
Ok(())
}
}
pub fn create_launch_json_snippet() -> String {
let json = serde_json::json!({
"type": "perl",
"request": "launch",
"name": "Launch Perl Script",
"program": "${workspaceFolder}/script.pl",
"args": [],
"perlPath": "perl",
"includePaths": ["${workspaceFolder}/lib"],
"cwd": "${workspaceFolder}",
"env": {}
});
serde_json::to_string_pretty(&json).unwrap_or_else(|e| {
tracing::error!(error = %e, "Failed to serialize launch.json snippet");
"{}".to_string()
})
}
pub fn create_attach_json_snippet() -> String {
let json = serde_json::json!({
"type": "perl",
"request": "attach",
"name": "Attach to Perl::LanguageServer",
"host": "localhost",
"port": 13603,
"timeout": 5000,
"stopOnEntry": false
});
serde_json::to_string_pretty(&json).unwrap_or_else(|e| {
tracing::error!(error = %e, "Failed to serialize attach.json snippet");
"{}".to_string()
})
}