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
2 changes: 2 additions & 0 deletions bifrost_searchtools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
SearchToolsError,
SymbolKindFilter,
code_query_variant_inventory,
extensions_for_paths,
tool_descriptors,
)
from .models import (
Expand Down Expand Up @@ -273,6 +274,7 @@
"DiffEndpoints",
"DirectoryListingEntry",
"EditedSymbolPair",
"extensions_for_paths",
"NavigationOperation",
"FileContent",
"FileChange",
Expand Down
16 changes: 16 additions & 0 deletions bifrost_searchtools/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1186,3 +1186,19 @@ def code_query_variant_inventory(
"Native code-query inventory call did not return a JSON object"
)
return decoded


def extensions_for_paths(
paths: list[str],
*,
library_path: Path | str | None = None,
) -> list[str]:
"""Every file extension for the language(s) present among `paths`, including reference-only
siblings (e.g. TypeScript/JavaScript's `.vue`/`.svelte`). Derived from bifrost's own language
table, not a caller-maintained copy of it. Pure -- opens no workspace -- so a caller can use it
to scope a `SearchToolsClient`'s `sources` to a diff's own language(s) before paying the cost
of indexing anything else, e.g. a backend-only diff need not index an unrelated frontend."""
native = _load_native_module(
Path(library_path).expanduser().resolve() if library_path is not None else None
)
return list(native.extensions_for_paths(list(paths)))
35 changes: 35 additions & 0 deletions crates/bifrost-core/src/analyzer/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,20 @@ impl Language {
}
}

/// Every analyzable language whose source registry contains `extension`,
/// including reference-only files such as `.vue` and `.razor`. A shared
/// file can belong to more than one language: `.vue` belongs to both
/// JavaScript and TypeScript.
pub fn languages_for_source_extension(extension: &str) -> impl Iterator<Item = Self> {
let normalized = extension.trim_start_matches('.').to_ascii_lowercase();
Self::ANALYZABLE.into_iter().filter(move |language| {
language.extensions().contains(&normalized.as_str())
|| language
.reference_only_sibling_extensions()
.contains(&normalized.as_str())
})
}

pub fn is_source_extension(extension: &str) -> bool {
let normalized = extension.trim_start_matches('.').to_ascii_lowercase();
Self::ANALYZABLE.iter().any(|language| {
Expand Down Expand Up @@ -357,6 +371,27 @@ mod language_dialect_tests {
}
}

#[cfg(test)]
mod language_source_extension_tests {
use super::*;

#[test]
fn source_extension_classification_includes_reference_only_siblings() {
assert_eq!(
Language::languages_for_source_extension(".vue").collect::<Vec<_>>(),
vec![Language::JavaScript, Language::TypeScript]
);
assert_eq!(
Language::languages_for_source_extension("RAZOR").collect::<Vec<_>>(),
vec![Language::CSharp]
);
assert_eq!(
Language::languages_for_source_extension(".unknown").count(),
0
);
}
}

/// Coarse declaration categories used across analyzers, lookup, usages, and
/// serialized state. Keep this enum lean and language-agnostic: prefer mapping
/// syntax-specific distinctions onto an existing high-level kind unless callers
Expand Down
59 changes: 58 additions & 1 deletion src/python_module.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use crate::{
SearchToolsService, SearchToolsServiceError, SearchToolsServiceErrorCode,
Language, SearchToolsService, SearchToolsServiceError, SearchToolsServiceErrorCode,
mcp_common::McpRenderOptions, mcp_registry::resolve_server_spec_for_render_options,
scoped_project::create_scoped_service, searchtools_render::RenderOptions,
};
use pyo3::exceptions::{PyRuntimeError, PyValueError};
use pyo3::prelude::*;
use std::collections::BTreeSet;
use std::path::PathBuf;

#[pyclass(name = "SearchToolsNativeSession")]
Expand Down Expand Up @@ -142,10 +143,66 @@ fn code_query_variant_inventory_json() -> PyResult<String> {
serde_json::to_string(&inventory).map_err(|err| PyRuntimeError::new_err(err.to_string()))
}

/// Every file extension for the language(s) present among `paths`, including reference-only
/// siblings such as TS/JS's `.vue`/`.svelte`, derived from Bifrost's own source-extension registry
/// rather than a caller-maintained copy of it. Pure and does not open a workspace: a caller can
/// scope a
/// [`SearchToolsNativeSession`]'s `sources` to a diff's own language(s) before paying the cost of
/// indexing anything -- e.g. skip a large unrelated frontend when a diff only touches backend code.
#[pyfunction]
fn extensions_for_paths(paths: Vec<String>) -> Vec<String> {
let mut extensions = BTreeSet::new();
for path in &paths {
let Some(extension) = std::path::Path::new(path)
.extension()
.and_then(|value| value.to_str())
else {
continue;
};
extensions.extend(
Language::languages_for_source_extension(extension).flat_map(|language| {
language
.extensions()
.iter()
.copied()
.chain(language.reference_only_sibling_extensions().iter().copied())
}),
);
}
extensions.into_iter().map(String::from).collect()
}

#[pymodule]
fn _native(module: &Bound<'_, PyModule>) -> PyResult<()> {
module.add_class::<SearchToolsNativeSession>()?;
module.add_function(wrap_pyfunction!(tool_descriptors_json, module)?)?;
module.add_function(wrap_pyfunction!(code_query_variant_inventory_json, module)?)?;
module.add_function(wrap_pyfunction!(extensions_for_paths, module)?)?;
Ok(())
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn extensions_for_paths_expands_reference_only_siblings() {
assert_eq!(
extensions_for_paths(vec![
"src/components/App.vue".to_string(),
"src/pages/Home.razor".to_string()
]),
vec![
"cjs", "cs", "cshtml", "js", "jsx", "mjs", "razor", "svelte", "ts", "tsx", "vue"
]
);
}

#[test]
fn extensions_for_paths_ignores_unknown_extensions() {
assert_eq!(
extensions_for_paths(vec!["artifact.unknown".to_string(), "LICENSE".to_string()]),
Vec::<String>::new()
);
}
}
Loading