Skip to main content

gpui_navigator/
nested.rs

1//! Nested route resolution
2//!
3//! This module provides functionality for resolving child routes in nested routing scenarios.
4//! The cache functionality has been moved to the `cache` module (available with `cache` feature).
5
6use crate::route::Route;
7use crate::{trace_log, warn_log, RouteParams};
8use std::borrow::Cow;
9use std::sync::Arc;
10
11/// Resolved child route information
12///
13/// Contains the matched child route and merged parameters from parent and child.
14pub type ResolvedChildRoute = (Arc<Route>, RouteParams);
15
16pub fn resolve_child_route(
17    parent_route: &Arc<Route>,
18    current_path: &str,
19    parent_params: &RouteParams,
20    outlet_name: Option<&str>,
21) -> Option<ResolvedChildRoute> {
22    trace_log!(
23        "resolve_child_route: parent='{}', current_path='{}', children={}, outlet_name={:?}",
24        parent_route.config.path,
25        current_path,
26        parent_route.get_children().len(),
27        outlet_name
28    );
29
30    // Get the children for this outlet (named or default)
31    let children = if let Some(name) = outlet_name {
32        // Named outlet - get children from named_children map
33        match parent_route.get_named_children(name) {
34            Some(named_children) => {
35                trace_log!(
36                    "Using named outlet '{}' with {} children",
37                    name,
38                    named_children.len()
39                );
40                named_children
41            }
42            None => {
43                warn_log!(
44                    "Named outlet '{}' not found in route '{}'",
45                    name,
46                    parent_route.config.path
47                );
48                return None;
49            }
50        }
51    } else {
52        // Default outlet - use regular children
53        parent_route.get_children()
54    };
55
56    if children.is_empty() {
57        trace_log!("No children found for outlet {:?}", outlet_name);
58        return None;
59    }
60
61    // Extract parent path from the route config
62    let parent_path = &parent_route.config.path;
63
64    // Normalize paths for comparison
65    let parent_path_normalized = parent_path.trim_end_matches('/');
66    let current_path_normalized = current_path.trim_start_matches('/');
67
68    // Check if current path starts with parent path
69    if !current_path_normalized.starts_with(parent_path_normalized.trim_start_matches('/')) {
70        return None;
71    }
72
73    // Get the remaining path after parent
74    let remaining = if parent_path_normalized.is_empty() || parent_path_normalized == "/" {
75        current_path_normalized
76    } else {
77        current_path_normalized
78            .strip_prefix(parent_path_normalized.trim_start_matches('/'))
79            .unwrap_or("")
80            .trim_start_matches('/')
81    };
82
83    trace_log!(
84        "  normalized: parent='{}', current='{}', remaining='{}'",
85        parent_path_normalized,
86        current_path_normalized,
87        remaining
88    );
89
90    if remaining.is_empty() {
91        // No child path, look for index route
92        return find_index_route(children, parent_params.clone());
93    }
94
95    // Split remaining path into segments
96    let segments: Vec<&str> = remaining.split('/').filter(|s| !s.is_empty()).collect();
97    if segments.is_empty() {
98        return find_index_route(children, parent_params.clone());
99    }
100
101    let first_segment = segments[0];
102    trace_log!("  first_segment: '{}'", first_segment);
103
104    // Try to match first segment against child routes
105    for child in children {
106        let child_path = child.config.path.trim_start_matches('/');
107
108        // Check for exact match or parameter match
109        if child_path == first_segment || child_path.starts_with(':') {
110            trace_log!("  matched: '{}'", child_path);
111            // Found matching child!
112            let mut combined_params = parent_params.clone();
113
114            // If this is a parameter route, extract the parameter
115            if child_path.starts_with(':') {
116                let param_name = child_path.trim_start_matches(':');
117                combined_params.insert(param_name.to_string(), first_segment.to_string());
118            }
119
120            // TODO: Handle nested parameters in deeper child paths
121
122            return Some((Arc::clone(child), combined_params));
123        }
124    }
125
126    None
127}
128
129/// Find an index route (default child route when no specific child is selected)
130fn find_index_route(children: &[Arc<Route>], params: RouteParams) -> Option<ResolvedChildRoute> {
131    // Look for a child with empty path, "/" or "index"
132    for child in children {
133        let child_path = child.config.path.trim_start_matches('/');
134
135        if child_path.is_empty() || child_path == "/" || child_path == "index" {
136            return Some((Arc::clone(child), params));
137        }
138    }
139
140    None
141}
142
143/// Build the full path for a child route
144///
145/// Combines parent and child paths into a complete route path.
146///
147/// Returns `Cow<str>` to avoid unnecessary allocations when possible.
148/// Uses borrowed string when no modification is needed.
149///
150/// # Example
151///
152/// ```
153/// use gpui_navigator::build_child_path;
154///
155/// let full_path = build_child_path("/dashboard", "settings");
156/// assert_eq!(full_path, "/dashboard/settings");
157/// ```
158pub fn build_child_path<'a>(parent_path: &'a str, child_path: &'a str) -> Cow<'a, str> {
159    let parent = parent_path.trim_end_matches('/');
160    let child = child_path.trim_start_matches('/').trim_end_matches('/');
161
162    if child.is_empty() {
163        // Return parent as-is if child is empty (avoid allocation)
164        if parent == parent_path {
165            Cow::Borrowed(parent_path)
166        } else {
167            Cow::Owned(parent.to_string())
168        }
169    } else if parent.is_empty() || parent == "/" {
170        Cow::Owned(format!("/{}", child))
171    } else {
172        Cow::Owned(format!("{}/{}", parent, child))
173    }
174}