Skip to content
Closed
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
4 changes: 2 additions & 2 deletions anycode/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { TerminalPanel } from './features/terminal/TerminalPanel';
import { AgentPanel } from './features/agents/AgentPanel';
import { BrowserPanel } from './features/browser/BrowserPanel';
import { type DiffMode } from './types/diffMode';
import { normalizePath } from './utils';
import { normalizePath, uriToFilePath } from './utils';
import { useSettings } from './hooks/useSettings';

const toMultibufferFiles = (files: ChangedFile[]): MultibufferFile[] => files.map((file) => ({
Expand Down Expand Up @@ -402,7 +402,7 @@ const App: React.FC = () => {
const response = await editors.handleGoToDefinition(request);
const definition = Array.isArray(response) ? response[0] : response;
if (definition && definition.uri && definition.range) {
const filePath = definition.uri.replace('file://', '');
const filePath = uriToFilePath(definition.uri);
const line = definition.range.start.line;
const column = definition.range.start.character;
focusReviewFile(paneId, filePath, line, column);
Expand Down
13 changes: 2 additions & 11 deletions anycode/features/editor/ReferencesPeek.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { AnycodeEditor, AnycodeEditorReact } from 'anycode-react';
import type { ReferencesPeekItem, ReferencesPeekState } from '../../types';
import { getFileName, getLanguageFromFileName } from '../../utils';
import { getFileName, getLanguageFromFileName, uriToFilePath } from '../../utils';
import './ReferencesPeek.css';

type ReferenceGroup = {
Expand All @@ -17,16 +17,7 @@ type ReferencesPeekProps = {
};

const resolveItemPath = (item: ReferencesPeekItem): string => {
const source = item.uri || item.file || '';
if (!source.startsWith('file://')) {
return source;
}
const rawPath = source.slice('file://'.length);
try {
return decodeURIComponent(rawPath);
} catch {
return rawPath;
}
return uriToFilePath(item.uri || item.file || '');
};

let referencesPeekEditorCounter = 0;
Expand Down
43 changes: 13 additions & 30 deletions anycode/hooks/useEditors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
type WatcherEdits,
} from '../types';
import { BATCH_DELAY_MS } from '../constants';
import { getFileName, getLanguageFromFileName } from '../utils';
import { getFileName, getLanguageFromFileName, uriToFilePath } from '../utils';
import { loadItem, loadOpenFiles, saveItem, saveOpenFiles } from '../storage';
import type { DiffMode } from '../types/diffMode';
import { DEFAULT_DIFF_VIEW_MODE, getNextDiffMode } from '../types/diffMode';
Expand Down Expand Up @@ -117,21 +117,6 @@ const normalizeHoverResponse = (response: any): string | null => {
return null;
};

const uriToFilePath = (uriOrPath: string): string => {
if (!uriOrPath) return '';
if (!uriOrPath.startsWith('file://')) {
return uriOrPath;
}

const rawPath = uriOrPath.slice('file://'.length);
try {
return decodeURIComponent(rawPath);
} catch {
return rawPath;
}
};


const getPersistedActiveFileId = (files: FileState[], activeFileId: string | null): string | null => {
if (activeFileId && files.some((file) => file.id === activeFileId)) {
return activeFileId;
Expand Down Expand Up @@ -1056,19 +1041,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 Expand Up @@ -1285,7 +1268,7 @@ export const useEditors = ({ wsRef, isConnected, onFileClosed }: UseEditorsParam
}

if (!targetFileId) {
targetFileId = uri.replace('file://', '');
targetFileId = uriToFilePath(uri);
}

diagnosticsRef.current.set(targetFileId, diags);
Expand Down
18 changes: 18 additions & 0 deletions anycode/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@ export const normalizePath = (path: string): string => {
return path.replace(/\\/g, '/');
};

export const uriToFilePath = (uriOrPath: string): string => {
if (!uriOrPath || !uriOrPath.startsWith('file://')) return uriOrPath;

const rawPath = uriOrPath.slice('file://'.length);
try {
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 /^\/[A-Za-z]:\//.test(rawPath)
? rawPath.slice(1)
: rawPath;
}
};

export const getFileName = (path: string): string => {
const normalized = normalizePath(path);
const parts = normalized.split('/');
Expand Down