Skip to content
Draft
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
1 change: 1 addition & 0 deletions dashboard/code-diagnostics/src/CodeDiagnostics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type {
const STATE_LABELS: Record<EngineState, string> = {
unavailable: "Unavailable",
disabled: "Disabled",
inactive: "Inactive",
starting: "Starting",
indexing: "Indexing",
ready: "Ready",
Expand Down
3 changes: 2 additions & 1 deletion dashboard/code-diagnostics/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@
color: var(--color-destructive);
}

.tdcd-state-disabled {
.tdcd-state-disabled,
.tdcd-state-inactive {
color: var(--color-muted-foreground);
}

Expand Down
1 change: 1 addition & 0 deletions dashboard/code-diagnostics/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export type IdleBackfillMode = "off" | "idle";
export type EngineState =
| "unavailable"
| "disabled"
| "inactive"
| "starting"
| "indexing"
| "ready"
Expand Down
18 changes: 18 additions & 0 deletions src/automation/fact_proposals.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ pub async fn record_session_fact_proposals(
.ok_or_else(|| config_error("accepted fact proposal missing add_fact_request"))?;
let add_fact_request = serde_json::from_value::<AddFactRequest>(add_fact_request)
.map_err(|e| config_error(format!("invalid accepted fact add_fact_request: {e}")))?;
if pending_add_fact_request_exists(&store, &add_fact_request) {
continue;
}
let proposal = value.get("proposal").cloned();
let validation = value.get("validation").cloned();
let record = FactProposalRecord {
Expand Down Expand Up @@ -214,6 +217,21 @@ pub async fn record_session_fact_proposals(
Ok(records)
}

fn pending_add_fact_request_exists(store: &FactProposalStore, request: &AddFactRequest) -> bool {
let content = normalize_fact_content(&request.content);
store.proposals.iter().any(|proposal| {
proposal.state == FactProposalState::PendingApproval
&& proposal.add_fact_request.as_ref().is_some_and(|existing| {
existing.category == request.category
&& normalize_fact_content(&existing.content) == content
})
})
}

fn normalize_fact_content(content: &str) -> String {
content.split_whitespace().collect::<Vec<_>>().join(" ")
}

pub async fn apply_fact_proposal(
dashboard_root: &Path,
conn: &Connection,
Expand Down
85 changes: 34 additions & 51 deletions src/dashboard/code_diagnostics_api.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use std::collections::{BTreeMap, BTreeSet};
use std::path::Path;
use std::sync::atomic::Ordering;
use std::time::Duration;

Expand All @@ -11,8 +10,9 @@ use serde_json::{json, Value};

use super::util::{http_detail, JsonError};
use super::DashboardState;
use crate::diagnostics::lsp::activity::{active_languages_for_files, documents_for_adapter};
use crate::diagnostics::lsp::adapters::LspAdapterDefinition;
use crate::diagnostics::lsp::client::LspDocument;
use crate::diagnostics::lsp::broker::EngineState;
use crate::diagnostics::lsp::settings::{save_settings, IdleBackfillMode};

type ApiResult = std::result::Result<Json<Value>, JsonError>;
Expand Down Expand Up @@ -44,6 +44,7 @@ enum CommandOverridePatch {
}

pub(crate) async fn overview(State(state): State<DashboardState>) -> ApiResult {
sync_project_language_activity(&state).await?;
maybe_spawn_idle_backfill(&state).await;
let snapshot = state.code_diagnostics.read().await.snapshot();
Ok(Json(json!(snapshot)))
Expand Down Expand Up @@ -85,17 +86,22 @@ pub(crate) async fn patch_settings(
let mut broker = state.code_diagnostics.write().await;
broker.update_adapters(adapters);
broker.update_settings(settings);
drop(broker);
sync_project_language_activity(&state).await?;
let broker = state.code_diagnostics.read().await;
Ok(Json(json!(broker.snapshot())))
}

pub(crate) async fn refresh_all(State(state): State<DashboardState>) -> ApiResult {
sync_project_language_activity(&state).await?;
let languages: Vec<String> = state
.code_diagnostics
.read()
.await
.snapshot()
.engines
.into_iter()
.filter(|engine| engine.state != EngineState::Inactive)
.map(|engine| engine.language)
.collect();
for language in languages {
Expand All @@ -109,6 +115,7 @@ pub(crate) async fn refresh_language(
State(state): State<DashboardState>,
AxumPath(language): AxumPath<String>,
) -> ApiResult {
sync_project_language_activity(&state).await?;
refresh_one(&state, &language).await?;
let snapshot = state.code_diagnostics.read().await.snapshot();
Ok(Json(json!(snapshot)))
Expand Down Expand Up @@ -218,7 +225,7 @@ async fn maybe_spawn_idle_backfill(state: &DashboardState) {
.snapshot()
.engines
.into_iter()
.filter(|engine| engine.enabled)
.filter(|engine| engine.enabled && engine.state != EngineState::Inactive)
.map(|engine| engine.language)
.collect();
for language in languages {
Expand All @@ -228,6 +235,30 @@ async fn maybe_spawn_idle_backfill(state: &DashboardState) {
});
}

async fn sync_project_language_activity(
state: &DashboardState,
) -> std::result::Result<(), JsonError> {
let files = indexed_files(&state.graph_conn)
.await
.map_err(|err| internal_error(&err))?;
let adapters = {
let broker = state.code_diagnostics.read().await;
broker
.snapshot()
.engines
.into_iter()
.filter_map(|engine| broker.adapter_for(&engine.language))
.collect::<Vec<_>>()
};
let active_languages = active_languages_for_files(&state.project_root, &adapters, &files);
state
.code_diagnostics
.write()
.await
.update_project_languages(active_languages);
Ok(())
}

async fn indexed_files(conn: &libsql::Connection) -> crate::errors::Result<Vec<String>> {
let mut rows = conn
.query("SELECT path FROM files ORDER BY path ASC", ())
Expand All @@ -241,54 +272,6 @@ async fn indexed_files(conn: &libsql::Connection) -> crate::errors::Result<Vec<S
Ok(files)
}

async fn documents_for_adapter(
project_root: &Path,
adapter: &LspAdapterDefinition,
files: Vec<String>,
) -> crate::errors::Result<Vec<LspDocument>> {
let mut documents = Vec::new();
for file in files {
if !matches_adapter_extension(adapter, &file) {
continue;
}
let path = project_root.join(&file);
let Ok(text) = tokio::fs::read_to_string(&path).await else {
continue;
};
documents.push(LspDocument {
language: adapter.language.clone(),
language_id: language_id_for_file(adapter, &file),
relative_path: file,
text,
});
}
Ok(documents)
}

fn language_id_for_file(adapter: &LspAdapterDefinition, file: &str) -> String {
let extension = Path::new(file)
.extension()
.and_then(|extension| extension.to_str())
.unwrap_or_default();
match (adapter.language.as_str(), extension) {
("typescript", "tsx") => "typescriptreact".to_string(),
("javascript", "jsx") => "javascriptreact".to_string(),
_ => adapter.language_id.clone(),
}
}

fn matches_adapter_extension(adapter: &LspAdapterDefinition, file: &str) -> bool {
Path::new(file)
.extension()
.and_then(|extension| extension.to_str())
.is_some_and(|extension| {
adapter
.extensions
.iter()
.any(|candidate| candidate == extension)
})
}

fn bad_request(err: &impl ToString) -> JsonError {
(
StatusCode::BAD_REQUEST,
Expand Down
78 changes: 78 additions & 0 deletions src/db/search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use libsql::params;
use super::connection::Database;
use super::rows::row_to_node;
use super::sql::{build_qmark_placeholders, collect_rows};
use crate::dependency_imports::{candidates_from_type_only_import, DependencyImportCandidate};
use crate::errors::{Result, TraceDecayError};
use crate::types::*;

Expand Down Expand Up @@ -201,6 +202,83 @@ impl Database {
collect_rows(&mut rows, row_to_node, "search_nodes_by_exact_name").await
}

pub async fn dependency_import_candidates(
&self,
query: &str,
limit: usize,
) -> Result<Vec<DependencyImportCandidate>> {
let query = query.trim();
if query.is_empty() || limit == 0 {
return Ok(Vec::new());
}
let query_lower = query.to_ascii_lowercase();
let like_pattern = format!("%{query}%");
let mut rows = self
.conn()
.query(
"SELECT name, signature, file_path, start_line
FROM nodes
WHERE kind = 'use'
AND signature LIKE ?1
AND name NOT LIKE './%'
AND name NOT LIKE '../%'
AND name NOT LIKE '/%'
ORDER BY file_path ASC, start_line ASC
LIMIT ?2",
params![
like_pattern.as_str(),
limit.saturating_mul(4).max(limit) as i64
],
)
.await
.map_err(|e| TraceDecayError::Database {
message: format!("failed to query dependency import candidates: {e}"),
operation: "dependency_import_candidates".to_string(),
})?;

let mut candidates = Vec::new();
while let Some(row) = rows.next().await.map_err(|e| TraceDecayError::Database {
message: format!("failed to read dependency import candidate: {e}"),
operation: "dependency_import_candidates".to_string(),
})? {
let module = row
.get::<String>(0)
.map_err(|e| TraceDecayError::Database {
message: format!("failed to read dependency import module: {e}"),
operation: "dependency_import_candidates".to_string(),
})?;
let signature = row
.get::<String>(1)
.map_err(|e| TraceDecayError::Database {
message: format!("failed to read dependency import signature: {e}"),
operation: "dependency_import_candidates".to_string(),
})?;
let file_path = row
.get::<String>(2)
.map_err(|e| TraceDecayError::Database {
message: format!("failed to read dependency import file path: {e}"),
operation: "dependency_import_candidates".to_string(),
})?;
let line = row.get::<u32>(3).map_err(|e| TraceDecayError::Database {
message: format!("failed to read dependency import line: {e}"),
operation: "dependency_import_candidates".to_string(),
})?;
candidates.extend(
candidates_from_type_only_import(&signature, &module, &file_path, line)
.into_iter()
.filter(|candidate| {
candidate.symbol.to_ascii_lowercase().contains(&query_lower)
|| candidate.module.to_ascii_lowercase().contains(&query_lower)
}),
);
if candidates.len() >= limit {
candidates.truncate(limit);
break;
}
}
Ok(candidates)
}

/// Returns `true` if the error indicates `SQLite` database corruption.
pub fn is_corruption_error(e: &TraceDecayError) -> bool {
match e {
Expand Down
Loading
Loading