use std::path::{Path, PathBuf};
use std::sync::mpsc::channel;
use strop_core::worker::CancelToken;
use strop_remote::worker_transport::RemoteWorker;
use strop_remote::{ReadFailureKind, ReadLimit, ReadSelection, RemoteClient, RemoteOffset};
use strop_workspace::{RemoteEndpoint, RemoteFile, RemoteLocation};
use super::attach::{AttachDecision, AttachRecord, DiscoverInput, LiveTransport};
use strop_lsp::languages::{Languages, LayerDiagnostic, RemoteLayer};
use strop_lsp::registry;
use strop_lsp::{Client, ServerId, Workspace};
const CONFIG_READ_LIMIT_BYTES: u64 = 256 * 1024;
enum Fetch {
Found(Vec<u8>),
Missing,
Cancelled,
Failed(String),
Transport(String),
}
fn fetch_small(
client: &RemoteClient,
seed: &RemoteFile,
path: &Path,
token: &CancelToken,
) -> Fetch {
let file = match seed.with_path(path.to_path_buf()) {
Ok(file) => file,
Err(error) => return Fetch::Failed(format!("not a canonical remote path: {error}")),
};
let selection = match ReadLimit::new(CONFIG_READ_LIMIT_BYTES) {
Ok(length) => ReadSelection::Range {
start: RemoteOffset::new(0),
length,
},
Err(error) => return Fetch::Failed(error.to_string()),
};
match client.read(&RemoteLocation::from(file), selection, token) {
Ok(snapshot) if snapshot.window.is_complete() => {
Fetch::Found(snapshot.buffer.text().to_string().into_bytes())
}
Ok(_) => {
Fetch::Failed("remote language configuration exceeds its bounded read limit".into())
}
Err(error) => match error.kind() {
ReadFailureKind::NotFound => Fetch::Missing,
ReadFailureKind::Cancelled => Fetch::Cancelled,
ReadFailureKind::Auth
| ReadFailureKind::Trust
| ReadFailureKind::Network
| ReadFailureKind::Connect
| ReadFailureKind::Deadline
| ReadFailureKind::Spawn
| ReadFailureKind::Subsystem => Fetch::Transport(error.to_string()),
_ => Fetch::Failed(error.to_string()),
},
}
}
enum LayerSearch {
Found(PathBuf, Vec<u8>),
Malformed(LayerDiagnostic),
None,
Cancelled,
Io(String),
}
fn find_project_layer(
client: &RemoteClient,
seed: &RemoteFile,
abs: &Path,
token: &CancelToken,
) -> LayerSearch {
let Some(mut dir) = abs.parent().map(Path::to_path_buf) else {
return LayerSearch::None;
};
let endpoint = seed.endpoint();
loop {
let candidate = dir.join(".strop").join("languages.toml");
match fetch_small(client, seed, &candidate, token) {
Fetch::Found(bytes) => return LayerSearch::Found(candidate, bytes),
Fetch::Missing => {}
Fetch::Cancelled => return LayerSearch::Cancelled,
Fetch::Transport(reason) => return LayerSearch::Io(reason),
Fetch::Failed(reason) => {
return LayerSearch::Malformed(LayerDiagnostic::remote_layer(
endpoint,
&candidate,
format!("{reason} — layer ignored"),
));
}
}
if !dir.pop() {
return LayerSearch::None;
}
}
}
fn workspace_root(
endpoint: &RemoteEndpoint,
parent: &Path,
worker: Option<&RemoteWorker>,
token: &CancelToken,
) -> Result<PathBuf, String> {
let Some(worker) = worker else {
return Ok(parent.to_path_buf());
};
strop_git::remote::discover(endpoint, parent, Some(worker), token)
.map(|found| found.unwrap_or_else(|| parent.to_path_buf()))
.map_err(|error| error.to_string())
}
pub(super) fn discover(
input: &DiscoverInput,
file: &RemoteFile,
client: &RemoteClient,
worker: &Option<RemoteWorker>,
token: &CancelToken,
) -> Option<AttachRecord> {
let DiscoverInput {
ticket,
ext,
language,
state_dir,
xdg,
transport,
..
} = input;
let endpoint = file.endpoint().clone();
let abs = file.path().to_path_buf();
let target = strop_workspace::Filesystem::Remote(endpoint.clone());
let parent = abs.parent().map(Path::to_path_buf).unwrap_or_default();
let record = |server: Option<ServerId>,
name: String,
root: PathBuf,
outcome: AttachDecision,
layers: Vec<LayerDiagnostic>| AttachRecord {
ticket: *ticket,
server,
language: language.to_string(),
name,
root,
target: target.clone(),
outcome,
layers,
};
let mut layers: Vec<LayerDiagnostic> = Vec::new();
let project = match find_project_layer(client, file, &abs, token) {
LayerSearch::Found(path, bytes) => Some((path, bytes)),
LayerSearch::Malformed(diagnostic) => {
layers.push(diagnostic);
None
}
LayerSearch::None => None,
LayerSearch::Cancelled => return None,
LayerSearch::Io(reason) => {
return Some(record(
None,
language.to_string(),
parent,
AttachDecision::RemoteIo { reason },
layers,
));
}
};
let languages = Languages::load_remote(
xdg.as_deref(),
project.as_ref().map(|(path, bytes)| RemoteLayer {
endpoint: &endpoint,
path,
bytes,
}),
);
layers.extend(languages.layer_diagnostics().iter().cloned());
let root = match project.as_ref().map(|(path, _)| path) {
Some(config) => config
.parent()
.and_then(Path::parent)
.map(Path::to_path_buf)
.unwrap_or_else(|| parent.clone()),
None => match workspace_root(&endpoint, &parent, worker.as_ref(), token) {
Ok(root) => root,
Err(_) if token.is_cancelled() => return None,
Err(reason) => {
return Some(record(
None,
language.to_string(),
parent,
AttachDecision::RemoteIo { reason },
layers,
))
}
},
};
let Some(spec) = registry::for_extension(ext, &languages) else {
return Some(record(
None,
language.to_string(),
root,
AttachDecision::NoServer,
layers,
));
};
let name = spec.name.to_string();
if spec.project_executable {
match crate::session::is_trusted_remote(state_dir.as_deref(), &endpoint, &root) {
Ok(true) => {}
Ok(false) => {
return Some(record(
None,
name,
root,
AttachDecision::TrustRequired {
command: spec.command.to_string(),
},
layers,
))
}
Err(error) => {
return Some(record(
None,
name,
root,
AttachDecision::TrustError {
error: error.to_string(),
},
layers,
))
}
}
}
if worker
.as_ref()
.is_some_and(|lease| lease.endpoint() != &endpoint)
{
return Some(record(
None,
name,
root,
AttachDecision::RemoteIo {
reason: "remote LSP lease belongs to a different endpoint".into(),
},
layers,
));
}
if token.is_cancelled() {
return None;
}
let (tx, rx) = channel();
match Client::spawn(
&spec,
Workspace::Remote {
endpoint: endpoint.clone(),
root: root.clone(),
},
tx,
worker.as_ref().map(|lease| lease.worker().clone()),
) {
Ok(client) => {
let server = client.id();
if let Ok(mut table) = transport.lock() {
table.insert(server, LiveTransport { client, rx });
}
Some(record(
Some(server),
name,
root,
AttachDecision::Attached,
layers,
))
}
Err(error) => Some(record(
None,
name,
root,
AttachDecision::SpawnFailed {
reason: error.to_string(),
},
layers,
)),
}
}