1use crate::core::{ParamKind, RouteSpec, RouteTable};
2use gpui::{AnyView, Context, IntoElement, Render, SharedString, Window, div, prelude::*, px, rgb};
3
4#[derive(Debug, Clone)]
5pub struct RouteMatch {
6 pub spec: RouteSpec,
7 pub params: Vec<(String, String)>,
8}
9
10pub struct RouterView {
11 table: RouteTable<AnyView>,
12 current_route: SharedString,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum NavigateError {
17 RouteIdNotFound(u64),
18 MissingParam { name: String },
19 UnsupportedWildcard,
20}
21
22impl RouterView {
23 pub fn new(table: RouteTable<AnyView>, initial_route: impl Into<SharedString>) -> Self {
24 Self {
25 table,
26 current_route: initial_route.into(),
27 }
28 }
29
30 pub fn route(&self) -> &str {
31 &self.current_route
32 }
33
34 pub fn navigate(&mut self, route: impl Into<SharedString>, cx: &mut Context<Self>) {
35 self.current_route = route.into();
36 cx.notify();
37 }
38
39 pub fn set_route(&mut self, route: impl Into<SharedString>, cx: &mut Context<Self>) {
40 self.navigate(route, cx);
41 }
42
43 pub fn navigate_by_id(
44 &mut self,
45 route_id: u64,
46 params: &[(&str, &str)],
47 cx: &mut Context<Self>,
48 ) -> Result<(), NavigateError> {
49 let (spec, _) = self
50 .table
51 .routes
52 .iter()
53 .find(|(spec, _)| spec.id == route_id)
54 .ok_or(NavigateError::RouteIdNotFound(route_id))?;
55
56 let route = build_path(spec, params)?;
57 self.navigate(route, cx);
58 Ok(())
59 }
60
61 pub fn table(&self) -> &RouteTable<AnyView> {
62 &self.table
63 }
64
65 pub fn table_mut(&mut self) -> &mut RouteTable<AnyView> {
66 &mut self.table
67 }
68}
69
70impl Render for RouterView {
71 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
72 let resolved = resolve_any_view(&self.table, &self.current_route);
73
74 let (matched_view, matched_meta) = match resolved {
75 Some(resolved) => {
76 let view = Some(resolved.view);
77 let meta = Some(resolved.matched);
78 (view, meta)
79 }
80 None => (self.table.fallback.clone(), None),
81 };
82
83 let header = render_debug_header(&self.current_route, matched_meta.as_ref(), &self.table);
84
85 div()
86 .flex()
87 .flex_col()
88 .size_full()
89 .child(header)
90 .child(
91 div()
92 .flex_1()
93 .size_full()
94 .bg(rgb(0xffffff))
95 .when_some(matched_view, |d, view| d.child(view)),
96 )
97 }
98}
99
100struct ResolvedAnyView {
101 matched: RouteMatch,
102 view: AnyView,
103}
104
105fn resolve_any_view(table: &RouteTable<AnyView>, path: &str) -> Option<ResolvedAnyView> {
106 let mut best: Option<(usize, RouteMatch, AnyView)> = None;
107
108 for (spec, view) in &table.routes {
109 let Some(matched) = match_route(spec, path) else {
110 continue;
111 };
112
113 let score = score_spec(spec);
114 match &best {
115 Some((best_score, _, _)) if *best_score >= score => {}
116 _ => best = Some((score, matched, view.clone())),
117 }
118 }
119
120 best.map(|(_, matched, view)| ResolvedAnyView { matched, view })
121}
122
123fn score_spec(spec: &RouteSpec) -> usize {
124 let tokens = tokenize_pattern(&spec.path, &spec.params);
125 tokens
126 .iter()
127 .map(|t| match t {
128 Token::Static(_) => 100,
129 Token::ParamSingle(_) => 10,
130 Token::ParamCatchAll(_) => 1,
131 Token::Wildcard => 0,
132 })
133 .sum::<usize>()
134 + tokens.len()
135}
136
137pub fn build_path(spec: &RouteSpec, params: &[(&str, &str)]) -> Result<String, NavigateError> {
138 let tokens = tokenize_pattern(&spec.path, &spec.params);
139
140 let mut result = String::new();
141 for token in tokens {
142 match token {
143 Token::Static(s) => {
144 if s != "/" {
145 result.push('/');
146 result.push_str(&s);
147 }
148 }
149 Token::ParamSingle(name) => {
150 let value = params
151 .iter()
152 .find(|(k, _)| *k == name)
153 .map(|(_, v)| *v)
154 .ok_or_else(|| NavigateError::MissingParam { name })?;
155
156 result.push('/');
157 result.push_str(value);
158 }
159 Token::ParamCatchAll(name) => {
160 let value = params
161 .iter()
162 .find(|(k, _)| *k == name)
163 .map(|(_, v)| *v)
164 .ok_or_else(|| NavigateError::MissingParam { name })?;
165
166 let value = value.trim_matches('/');
167 if !value.is_empty() {
168 result.push('/');
169 result.push_str(value);
170 }
171 }
172 Token::Wildcard => return Err(NavigateError::UnsupportedWildcard),
173 }
174 }
175
176 if result.is_empty() {
177 result.push('/');
178 }
179
180 Ok(result)
181}
182
183#[derive(Debug, Clone, PartialEq, Eq)]
184enum Token {
185 Static(String),
186 ParamSingle(String),
187 ParamCatchAll(String),
188 Wildcard,
189}
190
191fn tokenize_pattern(pattern: &str, params: &[crate::core::ParamSpec]) -> Vec<Token> {
192 let catchall_params: std::collections::HashSet<&str> = params
193 .iter()
194 .filter(|p| p.kind == ParamKind::CatchAll)
195 .map(|p| p.name.as_str())
196 .collect();
197
198 let mut segments: Vec<&str> = pattern
199 .split('/')
200 .filter(|s| !s.is_empty())
201 .collect();
202
203 if segments.is_empty() {
204 segments.push("/");
205 }
206
207 let mut tokens = Vec::new();
208 let mut i = 0;
209 while i < segments.len() {
210 let seg = segments[i];
211
212 if seg == "*" {
213 tokens.push(Token::Wildcard);
214 i += 1;
215 continue;
216 }
217
218 if let Some(name) = seg.strip_prefix(':') {
219 let is_catchall = catchall_params.contains(name);
220
221 if is_catchall && segments.get(i + 1).copied() == Some("*") {
222 tokens.push(Token::ParamCatchAll(name.to_string()));
223 i += 2;
224 continue;
225 }
226
227 tokens.push(Token::ParamSingle(name.to_string()));
228 i += 1;
229 continue;
230 }
231
232 tokens.push(Token::Static(seg.to_string()));
233 i += 1;
234 }
235
236 tokens
237}
238
239pub fn match_route(spec: &RouteSpec, path: &str) -> Option<RouteMatch> {
240 let tokens = tokenize_pattern(&spec.path, &spec.params);
241
242 let path_segments: Vec<&str> = path
243 .split('/')
244 .filter(|s| !s.is_empty())
245 .collect();
246
247 let mut params: Vec<(String, String)> = Vec::new();
248
249 if match_tokens(&tokens, &path_segments, 0, 0, &mut params) {
250 Some(RouteMatch {
251 spec: spec.clone(),
252 params,
253 })
254 } else {
255 None
256 }
257}
258
259fn match_tokens(
260 tokens: &[Token],
261 path_segments: &[&str],
262 token_index: usize,
263 path_index: usize,
264 params: &mut Vec<(String, String)>,
265) -> bool {
266 if token_index == tokens.len() {
267 return path_index == path_segments.len();
268 }
269
270 match &tokens[token_index] {
271 Token::Static(expected) => {
272 let Some(actual) = path_segments.get(path_index).copied() else {
273 return false;
274 };
275 if actual != expected {
276 return false;
277 }
278 match_tokens(tokens, path_segments, token_index + 1, path_index + 1, params)
279 }
280 Token::ParamSingle(name) => {
281 let Some(value) = path_segments.get(path_index).copied() else {
282 return false;
283 };
284 params.push((name.clone(), value.to_string()));
285 let ok = match_tokens(tokens, path_segments, token_index + 1, path_index + 1, params);
286 if !ok {
287 params.pop();
288 }
289 ok
290 }
291 Token::ParamCatchAll(name) => {
292 for end in (path_index + 1..=path_segments.len()).rev() {
293 let captured = path_segments[path_index..end].join("/");
294 params.push((name.clone(), captured));
295 let ok = match_tokens(tokens, path_segments, token_index + 1, end, params);
296 if ok {
297 return true;
298 }
299 params.pop();
300 }
301 false
302 }
303 Token::Wildcard => {
304 if token_index + 1 == tokens.len() {
305 return true;
306 }
307
308 for end in path_index..=path_segments.len() {
309 if match_tokens(tokens, path_segments, token_index + 1, end, params) {
310 return true;
311 }
312 }
313 false
314 }
315 }
316}
317
318fn render_debug_header(
319 current_route: &str,
320 matched: Option<&RouteMatch>,
321 table: &RouteTable<AnyView>,
322) -> impl IntoElement {
323 let matched_path = matched.map(|m| m.spec.path.as_str()).unwrap_or("(none)");
324 let matched_source = matched
325 .map(|m| m.spec.source.as_str())
326 .unwrap_or("(none)");
327
328 let matched_kind = matched
329 .map(|m| format!("{:?}", m.spec.kind))
330 .unwrap_or_else(|| "(none)".to_string());
331
332 let params_string = matched
333 .map(|m| {
334 if m.params.is_empty() {
335 "(none)".to_string()
336 } else {
337 m.params
338 .iter()
339 .map(|(k, v)| format!("{k}={v}"))
340 .collect::<Vec<_>>()
341 .join(", ")
342 }
343 })
344 .unwrap_or_else(|| "(none)".to_string());
345
346 div()
347 .flex()
348 .flex_col()
349 .gap_1()
350 .p_2()
351 .bg(rgb(0x161616))
352 .text_color(rgb(0xf0f0f0))
353 .text_sm()
354 .child(format!("route: {current_route}"))
355 .child(format!("matched: {matched_path} kind: {matched_kind}"))
356 .child(format!("params: {params_string}"))
357 .child(format!("source: {matched_source}"))
358 .child(format!(
359 "routes: {} fallback: {}",
360 table.routes.len(),
361 if table.fallback.is_some() { "yes" } else { "no" }
362 ))
363 .child(div().h(px(1.0)).bg(rgb(0x2a2a2a)))
364}