refactor: complete Rust 2024 let-chain migration

Migrate all remaining collapsible_if patterns to Rust 2024 let-chain
syntax across the entire codebase. This modernizes conditional logic
by replacing nested if statements with single-level expressions using
the && operator with let patterns.

Changes:
- storage.rs: 2 let-chain conversions (database dir creation, legacy archiving)
- session.rs: 3 let-chain conversions (empty content check, ledger dir creation, consent flow)
- ollama.rs: 8 let-chain conversions (socket parsing, cloud validation, model caching, capabilities)
- main.rs: 2 let-chain conversions (API key validation, provider enablement)
- owlen-tui: ~50 let-chain conversions across app/mod.rs, chat_app.rs, ui.rs, highlight.rs, and state modules

Test fixes:
- prompt_server.rs: Add missing .await on async RemoteMcpClient::new_with_config
- presets.rs, prompt_server.rs: Add missing rpc_timeout_secs field to McpServerConfig
- file_write.rs: Update error assertion to accept new "escapes workspace boundary" message

Verification:
- cargo build --all:  succeeds
- cargo clippy --all -- -D clippy::collapsible_if:  zero warnings
- cargo test --all:  109+ tests pass

Net result: -46 lines of code, improved readability and maintainability.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-29 14:10:12 +01:00
parent a84c8a425d
commit 4935a64a13
14 changed files with 414 additions and 460 deletions

View File

@@ -223,13 +223,12 @@ fn run_config_doctor() -> Result<()> {
if needs_env_update { if needs_env_update {
cloud.api_key_env = Some(OLLAMA_API_KEY_ENV.to_string()); cloud.api_key_env = Some(OLLAMA_API_KEY_ENV.to_string());
} }
if let Some(ref value) = original_api_key_env { if let Some(ref value) = original_api_key_env
if value.eq_ignore_ascii_case(LEGACY_OLLAMA_CLOUD_API_KEY_ENV) && (value.eq_ignore_ascii_case(LEGACY_OLLAMA_CLOUD_API_KEY_ENV)
|| value.eq_ignore_ascii_case(LEGACY_OWLEN_OLLAMA_CLOUD_API_KEY_ENV) || value.eq_ignore_ascii_case(LEGACY_OWLEN_OLLAMA_CLOUD_API_KEY_ENV))
{ {
cloud.api_key_env = Some(OLLAMA_API_KEY_ENV.to_string()); cloud.api_key_env = Some(OLLAMA_API_KEY_ENV.to_string());
} }
}
if cloud.api_key_env != original_api_key_env { if cloud.api_key_env != original_api_key_env {
changes changes
.push("updated providers.ollama_cloud.api_key_env to 'OLLAMA_API_KEY'".to_string()); .push("updated providers.ollama_cloud.api_key_env to 'OLLAMA_API_KEY'".to_string());
@@ -338,8 +337,7 @@ fn run_config_doctor() -> Result<()> {
if ensure_default_enabled { if ensure_default_enabled {
let default_id = config.general.default_provider.clone(); let default_id = config.general.default_provider.clone();
if let Some(default_cfg) = config.providers.get(&default_id) { if let Some(default_cfg) = config.providers.get(&default_id) && !default_cfg.enabled {
if !default_cfg.enabled {
if let Some(new_default) = config if let Some(new_default) = config
.providers .providers
.iter() .iter()
@@ -371,7 +369,6 @@ fn run_config_doctor() -> Result<()> {
} }
} }
} }
}
match config.mcp.mode { match config.mcp.mode {
McpMode::Legacy => { McpMode::Legacy => {

View File

@@ -361,12 +361,10 @@ fn probe_default_local_daemon(timeout: Duration) -> bool {
} }
for target in LOCAL_PROBE_TARGETS { for target in LOCAL_PROBE_TARGETS {
if let Ok(address) = target.parse::<SocketAddr>() { if let Ok(address) = target.parse::<SocketAddr>() && TcpStream::connect_timeout(&address, timeout).is_ok() {
if TcpStream::connect_timeout(&address, timeout).is_ok() {
return true; return true;
} }
} }
}
false false
} }
@@ -453,14 +451,12 @@ impl OllamaProvider {
ProviderVariant::Cloud => OllamaMode::Cloud, ProviderVariant::Cloud => OllamaMode::Cloud,
}; };
if matches!(variant, ProviderVariant::Cloud) { if matches!(variant, ProviderVariant::Cloud) && !api_key_present {
if !api_key_present {
return Err(Error::Config( return Err(Error::Config(
"Ollama Cloud API key not configured. Set providers.ollama_cloud.api_key or export OLLAMA_API_KEY (legacy: OLLAMA_CLOUD_API_KEY / OWLEN_OLLAMA_CLOUD_API_KEY)." "Ollama Cloud API key not configured. Set providers.ollama_cloud.api_key or export OLLAMA_API_KEY (legacy: OLLAMA_CLOUD_API_KEY / OWLEN_OLLAMA_CLOUD_API_KEY)."
.into(), .into(),
)); ));
} }
}
let base_candidate = match mode { let base_candidate = match mode {
OllamaMode::Local => base_url, OllamaMode::Local => base_url,
@@ -642,11 +638,9 @@ impl OllamaProvider {
return None; return None;
} }
if let Some(ts) = entry.fetched_at { if let Some(ts) = entry.fetched_at && ts.elapsed() < self.model_cache_ttl {
if ts.elapsed() < self.model_cache_ttl {
return Some(entry.models.clone()); return Some(entry.models.clone());
} }
}
// Fallback to last good models even if stale; UI will mark as degraded // Fallback to last good models even if stale; UI will mark as degraded
Some(entry.models.clone()) Some(entry.models.clone())
@@ -903,11 +897,9 @@ impl OllamaProvider {
} }
} }
if combined.is_empty() { if combined.is_empty() && let Some(err) = errors.pop() {
if let Some(err) = errors.pop() {
return Err(err); return Err(err);
} }
}
self.annotate_scope_status(&mut combined).await; self.annotate_scope_status(&mut combined).await;
combined.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())); combined.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
@@ -1312,11 +1304,9 @@ impl OllamaProvider {
provider_meta.insert("model".into(), Value::String(model)); provider_meta.insert("model".into(), Value::String(model));
provider_meta.insert("created_at".into(), Value::String(created_at)); provider_meta.insert("created_at".into(), Value::String(created_at));
if let Some(ref final_block) = final_data { if let Some(ref final_block) = final_data && let Ok(value) = serde_json::to_value(final_block) {
if let Ok(value) = serde_json::to_value(final_block) {
provider_meta.insert("final_data".into(), value); provider_meta.insert("final_data".into(), value);
} }
}
message message
.metadata .metadata
@@ -1855,15 +1845,13 @@ fn model_supports_tools(
capabilities: &[String], capabilities: &[String],
detail: Option<&OllamaModelInfo>, detail: Option<&OllamaModelInfo>,
) -> bool { ) -> bool {
if let Some(info) = detail { if let Some(info) = detail && info
if info
.capabilities .capabilities
.iter() .iter()
.any(|capability| capability_implies_tools(capability)) .any(|capability| capability_implies_tools(capability))
{ {
return true; return true;
} }
}
if capabilities if capabilities
.iter() .iter()
@@ -2113,11 +2101,9 @@ fn normalize_base_url(
return Err(format!("URL '{candidate}' cannot be used as a base URL")); return Err(format!("URL '{candidate}' cannot be used as a base URL"));
} }
if mode_hint == OllamaMode::Cloud && url.scheme() != "https" { if mode_hint == OllamaMode::Cloud && url.scheme() != "https" && std::env::var("OWLEN_ALLOW_INSECURE_CLOUD").is_err() {
if std::env::var("OWLEN_ALLOW_INSECURE_CLOUD").is_err() {
return Err("Ollama Cloud requires https:// base URLs".to_string()); return Err("Ollama Cloud requires https:// base URLs".to_string());
} }
}
let path = url.path().trim_end_matches('/'); let path = url.path().trim_end_matches('/');
match path { match path {
@@ -2130,14 +2116,10 @@ fn normalize_base_url(
} }
} }
if mode_hint == OllamaMode::Cloud { if mode_hint == OllamaMode::Cloud && let Some(host) = url.host_str() && host.eq_ignore_ascii_case("api.ollama.com") {
if let Some(host) = url.host_str() {
if host.eq_ignore_ascii_case("api.ollama.com") {
url.set_host(Some("ollama.com")) url.set_host(Some("ollama.com"))
.map_err(|err| format!("Failed to normalise Ollama Cloud host: {err}"))?; .map_err(|err| format!("Failed to normalise Ollama Cloud host: {err}"))?;
} }
}
}
url.set_query(None); url.set_query(None);
url.set_fragment(None); url.set_fragment(None);

View File

@@ -101,11 +101,9 @@ fn build_transcript(messages: &[Message]) -> String {
Role::Tool => "Tool", Role::Tool => "Tool",
}; };
let snippet = sanitize_snippet(&message.content); let snippet = sanitize_snippet(&message.content);
if snippet.is_empty() { if snippet.is_empty() && message.attachments.is_empty() {
if message.attachments.is_empty() {
continue; continue;
} }
}
transcript.push_str(&format!("{role}: {snippet}\n")); transcript.push_str(&format!("{role}: {snippet}\n"));
if !message.attachments.is_empty() { if !message.attachments.is_empty() {
for attachment in &message.attachments { for attachment in &message.attachments {
@@ -1072,8 +1070,7 @@ impl SessionController {
} }
async fn persist_usage_serialized(path: PathBuf, serialized: String) { async fn persist_usage_serialized(path: PathBuf, serialized: String) {
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() && let Err(err) = fs::create_dir_all(parent).await {
if let Err(err) = fs::create_dir_all(parent).await {
warn!( warn!(
"Failed to create usage ledger directory {}: {}", "Failed to create usage ledger directory {}: {}",
parent.display(), parent.display(),
@@ -1081,7 +1078,6 @@ impl SessionController {
); );
return; return;
} }
}
if let Err(err) = fs::write(&path, serialized).await { if let Err(err) = fs::write(&path, serialized).await {
warn!("Failed to write usage ledger {}: {}", path.display(), err); warn!("Failed to write usage ledger {}: {}", path.display(), err);
@@ -2274,8 +2270,7 @@ impl SessionController {
.pending_tool_requests .pending_tool_requests
.values() .values()
.any(|pending| pending.message_id == message_id) .any(|pending| pending.message_id == message_id)
{ && let Some((tool_name, data_types, endpoints)) =
if let Some((tool_name, data_types, endpoints)) =
self.check_tools_consent_needed(&calls).into_iter().next() self.check_tools_consent_needed(&calls).into_iter().next()
{ {
let request_id = Uuid::new_v4(); let request_id = Uuid::new_v4();
@@ -2299,7 +2294,6 @@ impl SessionController {
}); });
} }
} }
}
Some(calls) Some(calls)
} }

View File

@@ -53,15 +53,13 @@ impl StorageManager {
/// Create a storage manager using the provided database path /// Create a storage manager using the provided database path
pub async fn with_database_path(database_path: PathBuf) -> Result<Self> { pub async fn with_database_path(database_path: PathBuf) -> Result<Self> {
if let Some(parent) = database_path.parent() { if let Some(parent) = database_path.parent() && !parent.exists() {
if !parent.exists() {
std::fs::create_dir_all(parent).map_err(|e| { std::fs::create_dir_all(parent).map_err(|e| {
Error::Storage(format!( Error::Storage(format!(
"Failed to create database directory {parent:?}: {e}" "Failed to create database directory {parent:?}: {e}"
)) ))
})?; })?;
} }
}
let options = SqliteConnectOptions::from_str(&format!( let options = SqliteConnectOptions::from_str(&format!(
"sqlite://{}", "sqlite://{}",
@@ -434,14 +432,12 @@ impl StorageManager {
} }
} }
if migrated > 0 { if migrated > 0 && let Err(err) = archive_legacy_directory(&legacy_dir) {
if let Err(err) = archive_legacy_directory(&legacy_dir) {
println!( println!(
"Warning: migrated sessions but failed to archive legacy directory: {}", "Warning: migrated sessions but failed to archive legacy directory: {}",
err err
); );
} }
}
println!("Migrated {} legacy sessions.", migrated); println!("Migrated {} legacy sessions.", migrated);
Ok(()) Ok(())

View File

@@ -60,7 +60,9 @@ async fn write_outside_root_is_rejected() {
// The server returns a Network error with path traversal message // The server returns a Network error with path traversal message
let err_str = format!("{err}"); let err_str = format!("{err}");
assert!( assert!(
err_str.contains("path traversal") || err_str.contains("Path traversal"), err_str.contains("path traversal")
|| err_str.contains("Path traversal")
|| err_str.contains("escapes workspace boundary"),
"Expected path traversal error, got: {}", "Expected path traversal error, got: {}",
err_str err_str
); );

View File

@@ -48,6 +48,7 @@ fn prune_preset_removes_extra_entries() {
transport: "stdio".into(), transport: "stdio".into(),
env: Default::default(), env: Default::default(),
oauth: None, oauth: None,
rpc_timeout_secs: None,
}); });
let report = apply_preset(&mut config, PresetTier::Standard, true).expect("apply preset"); let report = apply_preset(&mut config, PresetTier::Standard, true).expect("apply preset");

View File

@@ -45,9 +45,10 @@ async fn test_render_prompt_via_external_server() -> Result<()> {
transport: "stdio".into(), transport: "stdio".into(),
env: std::collections::HashMap::new(), env: std::collections::HashMap::new(),
oauth: None, oauth: None,
rpc_timeout_secs: None,
}; };
let client = match RemoteMcpClient::new_with_config(&config) { let client = match RemoteMcpClient::new_with_config(&config).await {
Ok(client) => client, Ok(client) => client,
Err(err) => { Err(err) => {
eprintln!( eprintln!(

View File

@@ -272,12 +272,12 @@ impl App {
match event::poll(poll_interval) { match event::poll(poll_interval) {
Ok(true) => match event::read() { Ok(true) => match event::read() {
Ok(raw_event) => { Ok(raw_event) => {
if let Some(ui_event) = events::from_crossterm_event(raw_event) { if let Some(ui_event) = events::from_crossterm_event(raw_event)
if sender.send(AppEvent::Ui(ui_event)).is_err() { && sender.send(AppEvent::Ui(ui_event)).is_err()
{
break; break;
} }
} }
}
Err(_) => continue, Err(_) => continue,
}, },
Ok(false) => {} Ok(false) => {}
@@ -369,11 +369,11 @@ impl FrameRequester {
.expect("frame sender poisoned") .expect("frame sender poisoned")
.clone() .clone()
}; };
if let Some(tx) = sender { if let Some(tx) = sender
if tx.send(AppEvent::RedrawRequested).is_ok() { && tx.send(AppEvent::RedrawRequested).is_ok()
{
return; return;
} }
}
// Failed to dispatch; clear pending flag so future attempts can retry. // Failed to dispatch; clear pending flag so future attempts can retry.
self.inner.pending.store(false, Ordering::SeqCst); self.inner.pending.store(false, Ordering::SeqCst);

View File

@@ -346,61 +346,61 @@ impl LayoutSnapshot {
} }
fn region_at(&self, column: u16, row: u16) -> Option<UiRegion> { fn region_at(&self, column: u16, row: u16) -> Option<UiRegion> {
if let Some(rect) = self.model_info_panel { if let Some(rect) = self.model_info_panel
if Self::contains(rect, column, row) { && Self::contains(rect, column, row)
{
return Some(UiRegion::ModelInfo); return Some(UiRegion::ModelInfo);
} }
} if let Some(rect) = self.header_panel
if let Some(rect) = self.header_panel { && Self::contains(rect, column, row)
if Self::contains(rect, column, row) { {
return Some(UiRegion::Header); return Some(UiRegion::Header);
} }
} if let Some(rect) = self.code_panel
if let Some(rect) = self.code_panel { && Self::contains(rect, column, row)
if Self::contains(rect, column, row) { {
return Some(UiRegion::Code); return Some(UiRegion::Code);
} }
} if let Some(rect) = self.file_panel
if let Some(rect) = self.file_panel { && Self::contains(rect, column, row)
if Self::contains(rect, column, row) { {
return Some(UiRegion::FileTree); return Some(UiRegion::FileTree);
} }
} if let Some(rect) = self.input_panel
if let Some(rect) = self.input_panel { && Self::contains(rect, column, row)
if Self::contains(rect, column, row) { {
return Some(UiRegion::Input); return Some(UiRegion::Input);
} }
} if let Some(rect) = self.system_panel
if let Some(rect) = self.system_panel { && Self::contains(rect, column, row)
if Self::contains(rect, column, row) { {
return Some(UiRegion::System); return Some(UiRegion::System);
} }
} if let Some(rect) = self.status_panel
if let Some(rect) = self.status_panel { && Self::contains(rect, column, row)
if Self::contains(rect, column, row) { {
return Some(UiRegion::Status); return Some(UiRegion::Status);
} }
} if let Some(rect) = self.actions_panel
if let Some(rect) = self.actions_panel { && Self::contains(rect, column, row)
if Self::contains(rect, column, row) { {
return Some(UiRegion::Actions); return Some(UiRegion::Actions);
} }
} if let Some(rect) = self.attachments_panel
if let Some(rect) = self.attachments_panel { && Self::contains(rect, column, row)
if Self::contains(rect, column, row) { {
return Some(UiRegion::Attachments); return Some(UiRegion::Attachments);
} }
} if let Some(rect) = self.thinking_panel
if let Some(rect) = self.thinking_panel { && Self::contains(rect, column, row)
if Self::contains(rect, column, row) { {
return Some(UiRegion::Thinking); return Some(UiRegion::Thinking);
} }
} if let Some(rect) = self.chat_panel
if let Some(rect) = self.chat_panel { && Self::contains(rect, column, row)
if Self::contains(rect, column, row) { {
return Some(UiRegion::Chat); return Some(UiRegion::Chat);
} }
}
if Self::contains(self.content, column, row) { if Self::contains(self.content, column, row) {
Some(UiRegion::Content) Some(UiRegion::Content)
} else if Self::contains(self.frame, column, row) { } else if Self::contains(self.frame, column, row) {
@@ -661,11 +661,11 @@ fn search_candidate(candidate: &str, query: &str) -> Option<((usize, usize), Hig
return Some(((2, start_byte), HighlightMask::new(mask))); return Some(((2, start_byte), HighlightMask::new(mask)));
} }
if let Some(subsequence_mask) = subsequence_highlight(&lower_graphemes, &query_graphemes) { if let Some(subsequence_mask) = subsequence_highlight(&lower_graphemes, &query_graphemes)
if subsequence_mask.iter().any(|b| *b) { && subsequence_mask.iter().any(|b| *b)
{
return Some(((3, candidate.len()), HighlightMask::new(subsequence_mask))); return Some(((3, candidate.len()), HighlightMask::new(subsequence_mask)));
} }
}
None None
} }
@@ -2377,11 +2377,10 @@ impl ChatApp {
.model_info_panel .model_info_panel
.current_model_name() .current_model_name()
.map(|s| s.to_string()) .map(|s| s.to_string())
&& let Some(updated) = self.model_details_cache.get(&current).cloned()
{ {
if let Some(updated) = self.model_details_cache.get(&current).cloned() {
self.model_info_panel.set_model_info(updated); self.model_info_panel.set_model_info(updated);
} }
}
let total = self.model_details_cache.len(); let total = self.model_details_cache.len();
self.status = format!("Cached model details for {} model(s)", total); self.status = format!("Cached model details for {} model(s)", total);
self.error = None; self.error = None;
@@ -2604,11 +2603,11 @@ impl ChatApp {
}); });
self.guidance_settings = guidance_settings; self.guidance_settings = guidance_settings;
if dirty { if dirty
if let Err(err) = self.with_config(config::save_config) { && let Err(err) = self.with_config(config::save_config)
{
eprintln!("Warning: Failed to persist guidance settings: {err}"); eprintln!("Warning: Failed to persist guidance settings: {err}");
} }
}
if completed { if completed {
self.status = "Cheat sheet ready — press Esc when done".to_string(); self.status = "Cheat sheet ready — press Esc when done".to_string();
@@ -3366,8 +3365,9 @@ impl ChatApp {
latest_summary = Some((entry.level, clipped)); latest_summary = Some((entry.level, clipped));
} }
if !self.debug_log.is_visible() { if !self.debug_log.is_visible()
if let Some((level, message)) = latest_summary { && let Some((level, message)) = latest_summary
{
let level_label = match level { let level_label = match level {
Level::Error => "Error", Level::Error => "Error",
Level::Warn => "Warning", Level::Warn => "Warning",
@@ -3377,7 +3377,6 @@ impl ChatApp {
self.error = None; self.error = None;
} }
} }
}
fn ellipsize(message: &str, max_len: usize) -> String { fn ellipsize(message: &str, max_len: usize) -> String {
if message.chars().count() <= max_len { if message.chars().count() <= max_len {
@@ -3553,22 +3552,22 @@ impl ChatApp {
const CANDIDATES: [&str; 6] = const CANDIDATES: [&str; 6] =
["rendered", "text", "content", "value", "message", "body"]; ["rendered", "text", "content", "value", "message", "body"];
for key in CANDIDATES { for key in CANDIDATES {
if let Some(Value::String(text)) = map.get(key) { if let Some(Value::String(text)) = map.get(key)
if !text.trim().is_empty() { && !text.trim().is_empty()
{
return Some(text.clone()); return Some(text.clone());
} }
} }
}
if let Some(Value::Array(items)) = map.get("lines") { if let Some(Value::Array(items)) = map.get("lines") {
let mut collected = Vec::new(); let mut collected = Vec::new();
for item in items { for item in items {
if let Some(segment) = item.as_str() { if let Some(segment) = item.as_str()
if !segment.trim().is_empty() { && !segment.trim().is_empty()
{
collected.push(segment.trim()); collected.push(segment.trim());
} }
} }
}
if !collected.is_empty() { if !collected.is_empty() {
return Some(collected.join("\n")); return Some(collected.join("\n"));
} }
@@ -3579,13 +3578,12 @@ impl ChatApp {
} }
fn extract_mcp_error(value: &Value) -> Option<String> { fn extract_mcp_error(value: &Value) -> Option<String> {
if let Value::Object(map) = value { if let Value::Object(map) = value
if let Some(Value::String(message)) = map.get("error") { && let Some(Value::String(message)) = map.get("error")
if !message.trim().is_empty() { && !message.trim().is_empty()
{
return Some(message.clone()); return Some(message.clone());
} }
}
}
None None
} }
@@ -3836,20 +3834,18 @@ impl ChatApp {
} }
if attachment.is_image() { if attachment.is_image() {
if let Some(data) = attachment.base64_data() { if let Some(data) = attachment.base64_data()
if let Ok(bytes) = BASE64_STANDARD.decode(data) { && let Ok(bytes) = BASE64_STANDARD.decode(data)
if let Some(lines) = Self::preview_lines_for_image(&bytes) { && let Some(lines) = Self::preview_lines_for_image(&bytes)
{
return lines; return lines;
} } else if let Some(path) = attachment.source_path.as_ref()
} && let Ok(bytes) = fs::read(path)
} else if let Some(path) = attachment.source_path.as_ref() { && let Some(lines) = Self::preview_lines_for_image(&bytes)
if let Ok(bytes) = fs::read(path) { {
if let Some(lines) = Self::preview_lines_for_image(&bytes) {
return lines; return lines;
} }
} }
}
}
Vec::new() Vec::new()
} }
@@ -4346,9 +4342,9 @@ impl ChatApp {
let content_hash = let content_hash =
Self::message_content_hash(&role, &content, &tool_signature, &attachment_signature); Self::message_content_hash(&role, &content, &tool_signature, &attachment_signature);
if !is_streaming { if !is_streaming
if let Some(entry) = self.message_line_cache.get(&message_id) { && let Some(entry) = self.message_line_cache.get(&message_id)
if entry.wrap_width == card_width && entry.wrap_width == card_width
&& entry.role_label_mode == role_label_mode && entry.role_label_mode == role_label_mode
&& entry.syntax_highlighting == syntax_highlighting && entry.syntax_highlighting == syntax_highlighting
&& entry.render_markdown == render_markdown && entry.render_markdown == render_markdown
@@ -4360,8 +4356,6 @@ impl ChatApp {
{ {
return entry.lines.clone(); return entry.lines.clone();
} }
}
}
let mut rendered: Vec<Line<'static>> = Vec::new(); let mut rendered: Vec<Line<'static>> = Vec::new();
let content_style = Self::content_style(theme, &role); let content_style = Self::content_style(theme, &role);
@@ -8412,14 +8406,13 @@ impl ChatApp {
} }
FocusedPanel::Chat | FocusedPanel::Thinking => { FocusedPanel::Chat | FocusedPanel::Thinking => {
// Move selection forward by word // Move selection forward by word
if let Some((row, col)) = self.visual_end { if let Some((row, col)) = self.visual_end
if let Some(new_col) = && let Some(new_col) =
self.find_next_word_boundary(row, col) self.find_next_word_boundary(row, col)
{ {
self.visual_end = Some((row, new_col)); self.visual_end = Some((row, new_col));
} }
} }
}
FocusedPanel::Files => {} FocusedPanel::Files => {}
FocusedPanel::Code => {} FocusedPanel::Code => {}
} }
@@ -8432,14 +8425,13 @@ impl ChatApp {
} }
FocusedPanel::Chat | FocusedPanel::Thinking => { FocusedPanel::Chat | FocusedPanel::Thinking => {
// Move selection backward by word // Move selection backward by word
if let Some((row, col)) = self.visual_end { if let Some((row, col)) = self.visual_end
if let Some(new_col) = && let Some(new_col) =
self.find_prev_word_boundary(row, col) self.find_prev_word_boundary(row, col)
{ {
self.visual_end = Some((row, new_col)); self.visual_end = Some((row, new_col));
} }
} }
}
FocusedPanel::Files => {} FocusedPanel::Files => {}
FocusedPanel::Code => {} FocusedPanel::Code => {}
} }
@@ -8466,13 +8458,13 @@ impl ChatApp {
} }
FocusedPanel::Chat | FocusedPanel::Thinking => { FocusedPanel::Chat | FocusedPanel::Thinking => {
// Move selection to end of line // Move selection to end of line
if let Some((row, _)) = self.visual_end { if let Some((row, _)) = self.visual_end
if let Some(line) = self.get_line_at_row(row) { && let Some(line) = self.get_line_at_row(row)
{
let line_len = line.chars().count(); let line_len = line.chars().count();
self.visual_end = Some((row, line_len)); self.visual_end = Some((row, line_len));
} }
} }
}
FocusedPanel::Files => {} FocusedPanel::Files => {}
FocusedPanel::Code => {} FocusedPanel::Code => {}
} }
@@ -9424,8 +9416,9 @@ impl ChatApp {
); );
} }
} else { } else {
if self.available_providers.is_empty() { if self.available_providers.is_empty()
if let Err(err) = self.refresh_models().await { && let Err(err) = self.refresh_models().await
{
self.error = Some(format!( self.error = Some(format!(
"Failed to refresh providers: {}", "Failed to refresh providers: {}",
err err
@@ -9433,7 +9426,6 @@ impl ChatApp {
self.status = self.status =
"Unable to refresh providers".to_string(); "Unable to refresh providers".to_string();
} }
}
let filter = provider_query; let filter = provider_query;
if let Some(provider) = if let Some(provider) =
@@ -10436,8 +10428,8 @@ impl ChatApp {
} }
} }
KeyCode::Char(' ') => { KeyCode::Char(' ') => {
if let Some(item) = self.current_model_selector_item() { if let Some(item) = self.current_model_selector_item()
if let ModelSelectorItemKind::Header { && let ModelSelectorItemKind::Header {
provider, expanded, .. provider, expanded, ..
} = item.kind() } = item.kind()
{ {
@@ -10455,7 +10447,6 @@ impl ChatApp {
self.error = None; self.error = None;
} }
} }
}
KeyCode::Backspace => { KeyCode::Backspace => {
self.pop_model_search_char(); self.pop_model_search_char();
} }
@@ -10506,12 +10497,12 @@ impl ChatApp {
} }
} }
KeyCode::Char(ch) if ch.is_ascii_digit() => { KeyCode::Char(ch) if ch.is_ascii_digit() => {
if let Some(idx) = ch.to_digit(10) { if let Some(idx) = ch.to_digit(10)
if idx >= 1 && (idx as usize) <= HELP_TAB_COUNT { && idx >= 1 && (idx as usize) <= HELP_TAB_COUNT
{
self.help_tab_index = (idx - 1) as usize; self.help_tab_index = (idx - 1) as usize;
} }
} }
}
_ => {} _ => {}
}, },
}, },
@@ -10728,14 +10719,14 @@ impl ChatApp {
} }
} }
UiRegion::Attachments => { UiRegion::Attachments => {
if let Some(rect) = self.last_layout.attachments_panel { if let Some(rect) = self.last_layout.attachments_panel
if row > rect.y + 1 { && row > rect.y + 1
{
let list_index = row.saturating_sub(rect.y + 1) as usize; let list_index = row.saturating_sub(rect.y + 1) as usize;
if list_index < self.attachment_preview_entries.len() { if list_index < self.attachment_preview_entries.len() {
self.attachment_preview_selection = list_index; self.attachment_preview_selection = list_index;
} }
} }
}
self.shift_attachment_selection(0); self.shift_attachment_selection(0);
} }
UiRegion::Code => { UiRegion::Code => {
@@ -10746,14 +10737,14 @@ impl ChatApp {
UiRegion::Input => { UiRegion::Input => {
self.focus_panel(FocusedPanel::Input); self.focus_panel(FocusedPanel::Input);
self.set_input_mode(InputMode::Editing); self.set_input_mode(InputMode::Editing);
if let Some(rect) = self.last_layout.input_panel { if let Some(rect) = self.last_layout.input_panel
if let Some((line, column)) = self.input_cursor_from_point(rect, column, row) { && let Some((line, column)) = self.input_cursor_from_point(rect, column, row)
{
let line = line.min(u16::MAX as usize) as u16; let line = line.min(u16::MAX as usize) as u16;
let column = column.min(u16::MAX as usize) as u16; let column = column.min(u16::MAX as usize) as u16;
self.textarea.move_cursor(CursorMove::Jump(line, column)); self.textarea.move_cursor(CursorMove::Jump(line, column));
} }
} }
}
UiRegion::ModelInfo => { UiRegion::ModelInfo => {
self.set_input_mode(InputMode::Normal); self.set_input_mode(InputMode::Normal);
} }
@@ -11992,11 +11983,11 @@ impl ChatApp {
status_entry.state, status_entry.state,
)); ));
if status_entry.state != ModelAvailabilityState::Available if (status_entry.state != ModelAvailabilityState::Available
|| status_entry.is_stale || status_entry.is_stale
|| status_entry.message.is_some() || status_entry.message.is_some())
&& let Some(summary) = Self::scope_status_summary(&status_entry)
{ {
if let Some(summary) = Self::scope_status_summary(&status_entry) {
provider_block.push(ModelSelectorItem::empty( provider_block.push(ModelSelectorItem::empty(
provider.clone(), provider.clone(),
Some(summary), Some(summary),
@@ -12004,7 +11995,6 @@ impl ChatApp {
)); ));
rendered_body = true; rendered_body = true;
} }
}
let scope_allowed = self.filter_scope_allows_models(&scope, &status_entry); let scope_allowed = self.filter_scope_allows_models(&scope, &status_entry);
@@ -12113,8 +12103,8 @@ impl ChatApp {
.map(|item| matches!(item.kind(), ModelSelectorItemKind::Model { .. })) .map(|item| matches!(item.kind(), ModelSelectorItemKind::Model { .. }))
.unwrap_or(false); .unwrap_or(false);
if !current_is_model { if !current_is_model
if let Some((idx, _)) = self && let Some((idx, _)) = self
.model_selector_items .model_selector_items
.iter() .iter()
.enumerate() .enumerate()
@@ -12124,7 +12114,6 @@ impl ChatApp {
} }
} }
} }
}
fn evaluate_model_search( fn evaluate_model_search(
&self, &self,
@@ -12136,15 +12125,15 @@ impl ChatApp {
let mut best: Option<(usize, usize)> = None; let mut best: Option<(usize, usize)> = None;
let mut consider = |candidate: Option<&str>, target: &mut Option<HighlightMask>| { let mut consider = |candidate: Option<&str>, target: &mut Option<HighlightMask>| {
if let Some(text) = candidate { if let Some(text) = candidate
if let Some((score, mask)) = search_candidate(text, query) { && let Some((score, mask)) = search_candidate(text, query)
{
let replace = best.is_none_or(|current| score < current); let replace = best.is_none_or(|current| score < current);
if replace { if replace {
best = Some(score); best = Some(score);
} }
*target = Some(mask); *target = Some(mask);
} }
}
}; };
let display_name = Self::display_name_for_model(model); let display_name = Self::display_name_for_model(model);
@@ -12766,15 +12755,15 @@ impl ChatApp {
return Err(err); return Err(err);
} }
if provider.eq_ignore_ascii_case(&self.selected_provider) { if provider.eq_ignore_ascii_case(&self.selected_provider)
if let Err(err) = self.refresh_models().await { && let Err(err) = self.refresh_models().await
{
self.error = Some(format!( self.error = Some(format!(
"Provider mode updated but refreshing models failed: {}", "Provider mode updated but refreshing models failed: {}",
err err
)); ));
return Err(err); return Err(err);
} }
}
self.error = None; self.error = None;
self.status = format!( self.status = format!(
@@ -12868,35 +12857,30 @@ impl ChatApp {
.api_key .api_key
.clone() .clone()
.filter(|value| !value.trim().is_empty()); .filter(|value| !value.trim().is_empty());
if resolved_api_key.is_none() { if resolved_api_key.is_none()
if let Some(existing) = existing_plain_api_key.as_ref() { && let Some(existing) = existing_plain_api_key.as_ref()
if !existing.trim().is_empty() { && !existing.trim().is_empty()
{
resolved_api_key = Some(existing.clone()); resolved_api_key = Some(existing.clone());
} }
}
}
if resolved_api_key.is_none() && credential_manager.is_some() { if resolved_api_key.is_none() && credential_manager.is_some()
if let Some(manager) = credential_manager.clone() { && let Some(manager) = credential_manager.clone()
if let Some(credentials) = manager && let Some(credentials) = manager
.get_credentials(OLLAMA_CLOUD_CREDENTIAL_ID) .get_credentials(OLLAMA_CLOUD_CREDENTIAL_ID)
.await .await
.with_context(|| "Failed to load stored Ollama Cloud credentials")? .with_context(|| "Failed to load stored Ollama Cloud credentials")?
&& !credentials.api_key.trim().is_empty()
{ {
if !credentials.api_key.trim().is_empty() {
resolved_api_key = Some(credentials.api_key); resolved_api_key = Some(credentials.api_key);
} }
}
}
}
if resolved_api_key.is_none() { if resolved_api_key.is_none()
if let Ok(env_key) = std::env::var("OLLAMA_API_KEY") { && let Ok(env_key) = std::env::var("OLLAMA_API_KEY")
if !env_key.trim().is_empty() { && !env_key.trim().is_empty()
{
resolved_api_key = Some(env_key); resolved_api_key = Some(env_key);
} }
}
}
if resolved_api_key.is_none() { if resolved_api_key.is_none() {
return Err(anyhow!( return Err(anyhow!(
@@ -13091,12 +13075,12 @@ impl ChatApp {
)); ));
} }
if let Some(idx) = self.best_model_match_index(query) { if let Some(idx) = self.best_model_match_index(query)
if let Some(model) = self.models.get(idx).cloned() { && let Some(model) = self.models.get(idx).cloned()
{
self.apply_model_selection(model).await?; self.apply_model_selection(model).await?;
return Ok(()); return Ok(());
} }
}
Err(anyhow!(format!( Err(anyhow!(format!(
"No model matching '{}'. Use :model to browse available models.", "No model matching '{}'. Use :model to browse available models.",
@@ -14183,9 +14167,10 @@ impl ChatApp {
let mut summary = segments.next()?.to_string(); let mut summary = segments.next()?.to_string();
if summary.chars().count() < 120 { if summary.chars().count() < 120
if let Some(next) = segments.next() { && let Some(next) = segments.next()
if !next.is_empty() { && !next.is_empty()
{
if !summary.ends_with('.') && !summary.ends_with('!') && !summary.ends_with('?') if !summary.ends_with('.') && !summary.ends_with('!') && !summary.ends_with('?')
{ {
summary.push('.'); summary.push('.');
@@ -14193,8 +14178,6 @@ impl ChatApp {
summary.push(' '); summary.push(' ');
summary.push_str(next); summary.push_str(next);
} }
}
}
if summary.chars().count() > 160 { if summary.chars().count() > 160 {
summary = Self::truncate_to_chars(&summary, 160); summary = Self::truncate_to_chars(&summary, 160);

View File

@@ -17,21 +17,23 @@ static THEME: Lazy<Theme> = Lazy::new(|| {
}); });
fn select_syntax(path_hint: Option<&Path>) -> &'static SyntaxReference { fn select_syntax(path_hint: Option<&Path>) -> &'static SyntaxReference {
if let Some(path) = path_hint { if let Some(path) = path_hint
if let Ok(Some(syntax)) = SYNTAX_SET.find_syntax_for_file(path) { && let Ok(Some(syntax)) = SYNTAX_SET.find_syntax_for_file(path)
{
return syntax; return syntax;
} }
if let Some(ext) = path.extension().and_then(|ext| ext.to_str()) { if let Some(path) = path_hint
if let Some(syntax) = SYNTAX_SET.find_syntax_by_extension(ext) { && let Some(ext) = path.extension().and_then(|ext| ext.to_str())
&& let Some(syntax) = SYNTAX_SET.find_syntax_by_extension(ext)
{
return syntax; return syntax;
} }
} if let Some(path) = path_hint
if let Some(name) = path.file_name().and_then(|name| name.to_str()) { && let Some(name) = path.file_name().and_then(|name| name.to_str())
if let Some(syntax) = SYNTAX_SET.find_syntax_by_token(name) { && let Some(syntax) = SYNTAX_SET.find_syntax_by_token(name)
{
return syntax; return syntax;
} }
}
}
SYNTAX_SET.find_syntax_plain_text() SYNTAX_SET.find_syntax_plain_text()
} }

View File

@@ -302,8 +302,8 @@ impl FileTreeState {
return; return;
} }
if let Some(rel) = diff_paths(path, &self.root) { if let Some(rel) = diff_paths(path, &self.root)
if let Some(index) = self && let Some(index) = self
.nodes .nodes
.iter() .iter()
.position(|node| node.path == rel || node.path == path) .position(|node| node.path == rel || node.path == path)
@@ -316,7 +316,6 @@ impl FileTreeState {
} }
} }
} }
}
fn expand_to(&mut self, index: usize) { fn expand_to(&mut self, index: usize) {
let mut current = Some(index); let mut current = Some(index);
@@ -563,11 +562,11 @@ fn build_nodes(
node.is_expanded = node.should_default_expand(); node.is_expanded = node.should_default_expand();
let index = nodes.len(); let index = nodes.len();
if let Some(parent_idx) = parent { if let Some(parent_idx) = parent
if let Some(parent_node) = nodes.get_mut(parent_idx) { && let Some(parent_node) = nodes.get_mut(parent_idx)
{
parent_node.children.push(index); parent_node.children.push(index);
} }
}
index_by_path.insert(relative, index); index_by_path.insert(relative, index);
nodes.push(node); nodes.push(node);

View File

@@ -289,12 +289,12 @@ impl KeymapState {
} }
fn expire_if_needed(&mut self, now: Instant) { fn expire_if_needed(&mut self, now: Instant) {
if let Some(deadline) = self.deadline { if let Some(deadline) = self.deadline
if now > deadline { && now > deadline
{
self.reset(); self.reset();
} }
} }
}
pub fn sequence_tokens(&self) -> Vec<String> { pub fn sequence_tokens(&self) -> Vec<String> {
self.sequence self.sequence
@@ -719,13 +719,13 @@ impl KeymapLoader {
return; return;
} }
if let Some(path) = path { if let Some(path) = path
if let Ok(text) = fs::read_to_string(&path) { && let Ok(text) = fs::read_to_string(&path)
{
self.default_path_content = Some(text); self.default_path_content = Some(text);
self.active = KeymapProfile::Custom; self.active = KeymapProfile::Custom;
} }
} }
}
fn with_embedded(&mut self, fallback: String) { fn with_embedded(&mut self, fallback: String) {
if self.explicit.is_some() || self.default_path_content.is_some() { if self.explicit.is_some() || self.default_path_content.is_some() {

View File

@@ -182,15 +182,14 @@ impl RepoSearchState {
if matches!( if matches!(
self.rows[self.selected_row].kind, self.rows[self.selected_row].kind,
RepoSearchRowKind::FileHeader RepoSearchRowKind::FileHeader
) { )
if let Some(idx) = self && let Some(idx) = self
.rows .rows
.iter() .iter()
.position(|row| matches!(row.kind, RepoSearchRowKind::Match { .. })) .position(|row| matches!(row.kind, RepoSearchRowKind::Match { .. }))
{ {
self.selected_row = idx; self.selected_row = idx;
} }
}
self.ensure_selection_visible(); self.ensure_selection_visible();
} }
} }

View File

@@ -1675,12 +1675,12 @@ fn collect_unsaved_relative_paths(app: &ChatApp, root: &Path) -> HashSet<PathBuf
if !pane.is_dirty { if !pane.is_dirty {
continue; continue;
} }
if let Some(abs) = pane.absolute_path() { if let Some(abs) = pane.absolute_path()
if let Some(rel) = diff_paths(abs, root) { && let Some(rel) = diff_paths(abs, root)
{
set.insert(rel); set.insert(rel);
continue; continue;
} }
}
if let Some(display) = pane.display_path() { if let Some(display) = pane.display_path() {
let display_path = PathBuf::from(display); let display_path = PathBuf::from(display);
if display_path.is_relative() { if display_path.is_relative() {
@@ -1694,12 +1694,12 @@ fn collect_unsaved_relative_paths(app: &ChatApp, root: &Path) -> HashSet<PathBuf
fn build_breadcrumbs(repo_name: &str, path: &Path) -> String { fn build_breadcrumbs(repo_name: &str, path: &Path) -> String {
let mut parts = vec![repo_name.to_string()]; let mut parts = vec![repo_name.to_string()];
for component in path.components() { for component in path.components() {
if let Component::Normal(segment) = component { if let Component::Normal(segment) = component
if !segment.is_empty() { && !segment.is_empty()
{
parts.push(segment.to_string_lossy().into_owned()); parts.push(segment.to_string_lossy().into_owned());
} }
} }
}
parts.join(" > ") parts.join(" > ")
} }
@@ -2243,14 +2243,14 @@ fn compute_cursor_metrics(
break; break;
} }
if !cursor_found { if !cursor_found
if let Some(last_segment) = segments.last() { && let Some(last_segment) = segments.last()
{
cursor_visual_row = segment_base_row + segments.len().saturating_sub(1); cursor_visual_row = segment_base_row + segments.len().saturating_sub(1);
cursor_col_width = UnicodeWidthStr::width(last_segment.as_str()); cursor_col_width = UnicodeWidthStr::width(last_segment.as_str());
cursor_found = true; cursor_found = true;
} }
} }
}
total_visual_rows += segments.len(); total_visual_rows += segments.len();
} }
@@ -2620,11 +2620,10 @@ fn render_messages(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
// Apply visual selection highlighting if in visual mode and Chat panel is focused // Apply visual selection highlighting if in visual mode and Chat panel is focused
if matches!(app.mode(), InputMode::Visual) && matches!(app.focused_panel(), FocusedPanel::Chat) if matches!(app.mode(), InputMode::Visual) && matches!(app.focused_panel(), FocusedPanel::Chat)
&& let Some(selection) = app.visual_selection()
{ {
if let Some(selection) = app.visual_selection() {
lines = apply_visual_selection(lines, Some(selection), &theme); lines = apply_visual_selection(lines, Some(selection), &theme);
} }
}
// Update AutoScroll state with accurate content length // Update AutoScroll state with accurate content length
let auto_scroll = app.auto_scroll_mut(); let auto_scroll = app.auto_scroll_mut();
@@ -2780,11 +2779,10 @@ fn render_thinking(frame: &mut Frame<'_>, area: Rect, app: &mut ChatApp) {
// Apply visual selection highlighting if in visual mode and Thinking panel is focused // Apply visual selection highlighting if in visual mode and Thinking panel is focused
if matches!(app.mode(), InputMode::Visual) if matches!(app.mode(), InputMode::Visual)
&& matches!(app.focused_panel(), FocusedPanel::Thinking) && matches!(app.focused_panel(), FocusedPanel::Thinking)
&& let Some(selection) = app.visual_selection()
{ {
if let Some(selection) = app.visual_selection() {
lines = apply_visual_selection(lines, Some(selection), &theme); lines = apply_visual_selection(lines, Some(selection), &theme);
} }
}
// Update AutoScroll state with accurate content length // Update AutoScroll state with accurate content length
let thinking_scroll = app.thinking_scroll_mut(); let thinking_scroll = app.thinking_scroll_mut();
@@ -2928,8 +2926,9 @@ fn render_attachment_preview(frame: &mut Frame<'_>, area: Rect, app: &mut ChatAp
lines.push(Line::from(spans)); lines.push(Line::from(spans));
} }
if let Some(entry) = entries.get(selected) { if let Some(entry) = entries.get(selected)
if !entry.preview_lines.is_empty() { && !entry.preview_lines.is_empty()
{
lines.push(Line::from(vec![Span::styled( lines.push(Line::from(vec![Span::styled(
"", "",
Style::default() Style::default()
@@ -2949,7 +2948,6 @@ fn render_attachment_preview(frame: &mut Frame<'_>, area: Rect, app: &mut ChatAp
)])); )]));
} }
} }
}
lines.push(Line::from(vec![Span::styled( lines.push(Line::from(vec![Span::styled(
"Commands: :attachments next · :attachments prev · :attachments remove <n>", "Commands: :attachments next · :attachments prev · :attachments remove <n>",
@@ -5662,8 +5660,9 @@ fn binding_variants(
break; break;
} }
} }
if direct.is_none() { if direct.is_none()
if let Some(desc) = candidates.first() { && let Some(desc) = candidates.first()
{
let seq = format_sequence(&desc.sequence); let seq = format_sequence(&desc.sequence);
if !desc if !desc
.sequence .sequence
@@ -5674,18 +5673,17 @@ fn binding_variants(
direct = Some(seq.clone()); direct = Some(seq.clone());
} }
} }
} if leader_binding.is_none()
if leader_binding.is_none() { && let Some(desc) = candidates.iter().find(|candidate| {
if let Some(desc) = candidates.iter().find(|candidate| {
candidate candidate
.sequence .sequence
.first() .first()
.map(|token| token.eq_ignore_ascii_case(leader)) .map(|token| token.eq_ignore_ascii_case(leader))
.unwrap_or(false) .unwrap_or(false)
}) { })
{
leader_binding = Some(format_sequence(&desc.sequence)); leader_binding = Some(format_sequence(&desc.sequence));
} }
}
(direct, leader_binding) (direct, leader_binding)
} }
@@ -6166,8 +6164,9 @@ fn render_theme_browser(frame: &mut Frame<'_>, app: &ChatApp) {
if columns.len() >= 2 { if columns.len() >= 2 {
let preview_area = columns[1]; let preview_area = columns[1];
if preview_area.width > 0 && preview_area.height > 0 { if preview_area.width > 0 && preview_area.height > 0
if let Some(selected_name) = themes.get(selected_index) { && let Some(selected_name) = themes.get(selected_index)
{
let preview_theme = all_themes let preview_theme = all_themes
.get(selected_name.as_str()) .get(selected_name.as_str())
.cloned() .cloned()
@@ -6182,7 +6181,6 @@ fn render_theme_browser(frame: &mut Frame<'_>, app: &ChatApp) {
); );
} }
} }
}
let footer = Paragraph::new(Line::from(Span::styled( let footer = Paragraph::new(Line::from(Span::styled(
"↑/↓ or j/k: Navigate · Enter: Apply theme · g/G: Top/Bottom · Esc: Cancel", "↑/↓ or j/k: Navigate · Enter: Apply theme · g/G: Top/Bottom · Esc: Cancel",