feat(nav): Complete file paths and flag unresolved web file references

Path completion in href/src/url()/@import, Markdown links and path-like strings, folders first. HTML and CSS paths that name no file get a 'Cannot resolve file' warning. File references resolve in the backend like IDEA's: relative to the file, /-rooted against the project and resource roots.
Цей коміт міститься в:
l0ng-ai committed 2026-10-24 23:28:20 +08:00
1 джерело 87b463990c
коміт 68dcea409c
16 files changed
+652 -74

No files matched your search

+3
Переглянути файл
@@ -104,6 +104,9 @@ pub fn builder() -> tauri_specta::Builder<tauri::Wry> {
nav::mark_recent,
nav::recent_files,
nav::web_goto,
nav::fs_resolve_reference,
nav::path_completions,
nav::web_unresolved_references,
search::search_start,
search::replace_in_files,
search::replace_preview,
+75 -3
Переглянути файл
@@ -1,8 +1,9 @@
use super::doc::blocking;
use crate::state::AppState;
use ide_core::{DocId, IdeResult, Location, Pos};
use crate::state::{AppState, Project};
use ide_core::{DocId, IdeResult, IpcPath, Location, Pos};
use ide_index::{FileMatch, IndexStatus, SymbolFilter, SymbolMatch};
use ide_language::WebReference;
use ide_language::{FileReference, WebReference};
use ide_workspace::file_refs::{self, PathCompletion};
use serde::{Deserialize, Serialize};
use specta::Type;
use std::path::PathBuf;
@@ -96,3 +97,74 @@ pub async fn web_goto(window: Window, state: State<'_, AppState>, doc: DocId, po
})
.await
}
/// The project root and its resource roots (served at `/`), for file references.
fn reference_roots(project: &Project) -> (PathBuf, Vec<PathBuf>) {
let resources = project.roots().resource_dirs().iter().map(|d| project.root.join(d)).collect();
(project.root.clone(), resources)
}
/// The existing file or directory a path written in `from` names (`href`, `url()`, a string literal).
#[tauri::command]
#[specta::specta]
pub async fn fs_resolve_reference(
window: Window,
state: State<'_, AppState>,
from: IpcPath,
value: String,
) -> IdeResult<Option<IpcPath>> {
let project = state.project(window.label()).ok();
blocking(move || {
let (root, resources) = project.as_deref().map(reference_roots).unzip();
let resources = resources.unwrap_or_default();
Ok(file_refs::resolve(&value, &from, root.as_deref(), &resources).map(Into::into))
})
.await
}
/// Files and folders that continue `prefix`, a path being typed in `from`.
#[tauri::command]
#[specta::specta]
pub async fn path_completions(
window: Window,
state: State<'_, AppState>,
from: IpcPath,
prefix: String,
) -> IdeResult<Vec<PathCompletion>> {
let project = state.project(window.label()).ok();
blocking(move || {
let (root, resources) = project.as_deref().map(reference_roots).unzip();
let resources = resources.unwrap_or_default();
Ok(file_refs::complete(&prefix, &from, root.as_deref(), &resources))
})
.await
}
/// Paths in an HTML document or stylesheet that name no existing file.
#[tauri::command]
#[specta::specta]
pub async fn web_unresolved_references(
window: Window,
state: State<'_, AppState>,
doc: DocId,
) -> IdeResult<Vec<FileReference>> {
let project = state.project(window.label())?;
let syntax = state.syntax();
blocking(move || {
let snap = project.documents.snapshot(doc)?;
if snap.large {
return Ok(Vec::new());
}
let (root, resources) = reference_roots(&project);
Ok(syntax
.web_file_references(&snap.language, &snap.text)
.into_iter()
.filter(|r| {
file_refs::is_file_reference(&r.value)
&& !file_refs::path_part(&r.value).is_empty()
&& file_refs::resolve(&r.value, &snap.path, Some(&root), &resources).is_none()
})
.collect())
})
.await
}