Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 52 additions & 2 deletions anycode-backend/src/lsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use tokio::io::{self};
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::process::Command;
use tokio::sync::Mutex;
use tokio::sync::{Mutex, Notify};
use tokio::sync::mpsc;
use tokio::time::Duration;
use tokio::time::{self};
Expand All @@ -20,6 +20,8 @@ use lsp_types::*;
use crate::config::Config;
use crate::utils::path_to_uri;

const PROJECT_INITIALIZATION_TIMEOUT: Duration = Duration::from_secs(30);

fn lsp_command(program: &str, args: &[&str]) -> Command {
let mut command = Command::new(program);
command
Expand Down Expand Up @@ -82,6 +84,8 @@ pub struct Lsp {
pending: Arc<Mutex<HashMap<usize, mpsc::Sender<String>>>>,
configuration: Arc<Mutex<Value>>,
ready: AtomicBool,
project_initialized: Arc<AtomicBool>,
project_initialization_notify: Arc<Notify>,
opened: HashSet<String>,
}

Expand All @@ -97,6 +101,8 @@ impl Lsp {
pending: Arc::new(Mutex::new(HashMap::new())),
configuration: Arc::new(Mutex::new(Value::Object(serde_json::Map::new()))),
ready: AtomicBool::new(false),
project_initialized: Arc::new(AtomicBool::new(false)),
project_initialization_notify: Arc::new(Notify::new()),
opened: HashSet::new(),
}
}
Expand Down Expand Up @@ -149,6 +155,8 @@ impl Lsp {

let pending = self.pending.clone();
let configuration = self.configuration.clone();
let project_initialized = self.project_initialized.clone();
let project_initialization_notify = self.project_initialization_notify.clone();

// reading from child stdout
tokio::spawn(async move {
Expand Down Expand Up @@ -221,6 +229,10 @@ impl Lsp {
}

match parsed_json.get("method").and_then(|v| v.as_str()) {
Some("workspace/projectInitializationComplete") => {
project_initialized.store(true, Ordering::SeqCst);
project_initialization_notify.notify_waiters();
}
Some("textDocument/publishDiagnostics") => {
// diagnostics
let v = parsed_json["params"].clone();
Expand Down Expand Up @@ -331,6 +343,9 @@ impl Lsp {
}

self.ready.store(true, Ordering::SeqCst);
if !self.requires_project_initialization() {
self.project_initialized.store(true, Ordering::SeqCst);
}
Ok(())
}

Expand Down Expand Up @@ -358,6 +373,8 @@ impl Lsp {
return Err(anyhow::anyhow!("LSP not ready"));
}

self.wait_for_project_initialization().await?;

let id = self.get_next_id();

let msg = serde_json::json!({
Expand Down Expand Up @@ -391,6 +408,32 @@ impl Lsp {
Ok(parsed)
}

fn requires_project_initialization(&self) -> bool {
self.lsp_name.as_deref() == Some("roslyn-language-server")
}

async fn wait_for_project_initialization(&self) -> anyhow::Result<()> {
if !self.requires_project_initialization() {
return Ok(());
}

let notification = self.project_initialization_notify.notified();
if self.project_initialized.load(Ordering::SeqCst) {
return Ok(());
}

if time::timeout(PROJECT_INITIALIZATION_TIMEOUT, notification)
.await
.is_err()
{
return Err(anyhow::anyhow!(
"Timed out waiting for LSP project initialization"
));
}

Ok(())
}

pub fn is_ready(&mut self) -> bool {
self.ready.load(Ordering::SeqCst)
}
Expand All @@ -406,7 +449,14 @@ impl Lsp {
}

pub fn did_open(&mut self, lang: &str, path: &str, text: &str) -> Result<()> {
self.opened.insert(path.to_string());
// A document may be opened by more than one editor pane or by a
// repeated file:open event. LSP requires didOpen to be sent only
// once per document and server connection. Some servers tolerate a
// duplicate notification, while Roslyn terminates the connection.
if !self.opened.insert(path.to_string()) {
return Ok(());
}

let uri = path_to_uri(path)?;
let params = DidOpenTextDocumentParams {
text_document: TextDocumentItem {
Expand Down
35 changes: 20 additions & 15 deletions anycode/hooks/useEditors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,16 @@ const uriToFilePath = (uriOrPath: string): string => {

const rawPath = uriOrPath.slice('file://'.length);
try {
return decodeURIComponent(rawPath);
const decodedPath = decodeURIComponent(rawPath);
// file:///C:/... is the canonical URI form for Windows, but the
// local filesystem path must be C:/..., without the URI root slash.
return /^\/[A-Za-z]:\//.test(decodedPath)
? decodedPath.slice(1)
: decodedPath;
} catch {
return rawPath;
return /^\/[A-Za-z]:\//.test(rawPath)
? rawPath.slice(1)
: rawPath;
}
};

Expand Down Expand Up @@ -1056,19 +1063,17 @@ export const useEditors = ({ wsRef, isConnected, onFileClosed }: UseEditorsParam
const range = definition.range;
const line = range.start.line;
const column = range.start.character;
const filePath = uri.replace('file://', '');
const fileName = getFileName(filePath);

const existingFile = filesRef.current.find((f) => f.id === filePath || f.name === fileName);
if (existingFile) {
setActiveFileId(existingFile.id);
const editor = editorRefs.current.get(existingFile.id);
if (editor) {
editor.requestFocus(line, column);
}
} else {
openFile(filePath, line, column);
}
const filePath = uriToFilePath(uri);
// Always route definition navigation through openFile.
// A persisted FileState can exist without a live editor
// instance (for example after a reload); handling that
// state here directly would silently skip file:open.
openFile(
filePath,
line,
column,
activeEditorPaneIdRef.current || DEFAULT_EDITOR_PANE_ID,
);

resolve(definition);
} else {
Expand Down