use anyhow::Result;
use colored::*;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::sync::RwLock;
use crate::crdt::{Operation, OperationType, Position};
use crate::storage::OperationLog;
use crate::sync::{SyncManager, GLOBAL_CLOCK};
use tracing::{debug};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LspChangeEvent {
pub uri: String,
pub changes: Vec<LspTextEdit>,
pub version: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LspTextEdit {
pub range: LspRange,
pub text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LspRange {
pub start: LspPosition,
pub end: LspPosition,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LspPosition {
pub line: u32,
pub character: u32,
}
pub struct LspDetector {
#[allow(dead_code)]
repo_root: PathBuf,
oplog: Arc<OperationLog>,
actor_id: String,
sync_mgr: Option<Arc<SyncManager>>,
document_versions: Arc<RwLock<std::collections::HashMap<String, i32>>>,
}
impl LspDetector {
pub fn new(
repo_root: PathBuf,
oplog: Arc<OperationLog>,
actor_id: String,
sync_mgr: Option<Arc<SyncManager>>,
) -> Self {
Self {
repo_root,
oplog,
actor_id,
sync_mgr,
document_versions: Arc::new(RwLock::new(std::collections::HashMap::new())),
}
}
pub async fn process_change(&self, event: LspChangeEvent) -> Result<()> {
{
let mut versions = self.document_versions.write().await;
if let Some(&last_version) = versions.get(&event.uri) {
if event.version <= last_version {
return Ok(());
}
}
versions.insert(event.uri.clone(), event.version);
}
let path = uri_to_path(&event.uri)?;
let operations = self.convert_changes_to_operations(&path, &event.changes)?;
for op in operations {
if self.oplog.append(op.clone())? {
if let Some(mgr) = &self.sync_mgr {
let _ = mgr.publish(Arc::new(op.clone()));
}
self.print_lsp_operation(&op);
}
}
Ok(())
}
fn convert_changes_to_operations(
&self,
path: &Path,
changes: &[LspTextEdit],
) -> Result<Vec<Operation>> {
let mut operations = Vec::new();
for change in changes {
let op = self.convert_edit_to_operation(path, change)?;
operations.push(op);
}
Ok(operations)
}
fn convert_edit_to_operation(&self, path: &Path, edit: &LspTextEdit) -> Result<Operation> {
let file_path = path.display().to_string();
let start_line = edit.range.start.line as usize + 1;
let start_col = edit.range.start.character as usize + 1;
let end_line = edit.range.end.line as usize + 1;
let end_col = edit.range.end.character as usize + 1;
let offset = 0;
let lamport = GLOBAL_CLOCK.tick();
let position = Position::new(
start_line,
start_col,
offset,
self.actor_id.clone(),
lamport,
);
let op_type = if edit.range.start.line == edit.range.end.line
&& edit.range.start.character == edit.range.end.character
{
OperationType::Insert {
position: position.clone(),
content: edit.text.clone(),
length: edit.text.chars().count(),
}
} else if edit.text.is_empty() {
let length = calculate_range_length(&edit.range);
OperationType::Delete {
position: position.clone(),
length,
}
} else {
OperationType::Replace {
position: position.clone(),
old_content: format!("({}:{} to {}:{})", start_line, start_col, end_line, end_col),
new_content: edit.text.clone(),
}
};
Ok(Operation::new(file_path, op_type, self.actor_id.clone()))
}
fn print_lsp_operation(&self, op: &Operation) {
let filename = Path::new(&op.file_path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or(&op.file_path);
let (action, details) = match &op.op_type {
OperationType::Insert {
position,
content,
length,
} => {
let preview = truncate_preview(content, 40);
(
"INSERT".green(),
format!(
"{}:{} +{} chars '{}'",
position.line,
position.column,
length,
preview.green()
),
)
}
OperationType::Delete { position, length } => (
"DELETE".red(),
format!("{}:{} -{} chars", position.line, position.column, length),
),
OperationType::Replace {
position,
new_content,
..
} => {
let preview = truncate_preview(new_content, 40);
(
"REPLACE".yellow(),
format!(
"{}:{} → '{}'",
position.line,
position.column,
preview.green()
),
)
}
_ => return,
};
debug!(
"{} {} {} {}",
"📡".bright_blue(),
"[LSP]".bright_blue().bold(),
action.bold(),
format!("{} {}", filename.bright_white(), details)
);
}
}
pub async fn detect_lsp_support() -> Result<bool> {
if std::env::var("DX_LSP_ENABLED").is_ok() {
return Ok(true);
}
let lsp_socket = std::env::temp_dir().join("dx-lsp.sock");
if lsp_socket.exists() {
return Ok(true);
}
if let Ok(home) = std::env::var("HOME").or_else(|_| std::env::var("USERPROFILE")) {
let vscode_extensions = PathBuf::from(home).join(".vscode").join("extensions");
if vscode_extensions.exists() {
if let Ok(entries) = std::fs::read_dir(vscode_extensions) {
for entry in entries.flatten() {
if let Some(name) = entry.file_name().to_str() {
if name.starts_with("dx-") || name.contains("forge") {
return Ok(true);
}
}
}
}
}
}
Ok(false)
}
pub async fn start_lsp_monitoring(
repo_root: PathBuf,
oplog: Arc<OperationLog>,
actor_id: String,
sync_mgr: Option<Arc<SyncManager>>,
) -> Result<()> {
use super::detector;
detector::start_watching(
repo_root,
oplog,
actor_id.clone(),
String::new(), sync_mgr,
)
.await
}
fn uri_to_path(uri: &str) -> Result<PathBuf> {
let path_str = uri
.strip_prefix("file://")
.or_else(|| uri.strip_prefix("file:///"))
.unwrap_or(uri);
#[cfg(windows)]
let path_str = path_str.trim_start_matches('/');
Ok(PathBuf::from(path_str))
}
fn calculate_range_length(range: &LspRange) -> usize {
if range.start.line == range.end.line {
(range.end.character - range.start.character) as usize
} else {
100 }
}
fn truncate_preview(s: &str, max_len: usize) -> String {
if s.len() <= max_len {
s.replace('\n', "\\n").replace('\t', "\\t")
} else {
let truncated = &s[..max_len.min(s.len())];
format!("{}…", truncated.replace('\n', "\\n").replace('\t', "\\t"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_uri_to_path() {
let uri = "file:///home/user/project/src/main.rs";
let path = uri_to_path(uri).unwrap();
assert!(path.to_string_lossy().contains("main.rs"));
}
#[test]
fn test_calculate_range_length() {
let range = LspRange {
start: LspPosition {
line: 0,
character: 5,
},
end: LspPosition {
line: 0,
character: 10,
},
};
assert_eq!(calculate_range_length(&range), 5);
}
}