1#[cfg(feature = "cache")]
31use crate::cache::{CacheStats, RouteCache};
32use crate::error::{ErrorHandlers, NavigationResult};
33use crate::history::{HistoryEntry, HistoryState};
34use crate::lifecycle::NavigationAction;
35use crate::nested::trim_slashes;
36use crate::resolve::{resolve_match_stack, MatchStack};
37use crate::route::NamedRouteRegistry;
38#[cfg(feature = "transition")]
39use crate::transition::Transition;
40use crate::{
41 debug_log, error_log, info_log, trace_log, warn_log, IntoRoute, Route, RouteParams, RouterState,
42};
43use gpui::{AnyView, App, BorrowAppContext, Global};
44use std::borrow::BorrowMut;
45use std::collections::HashMap;
46use std::sync::Arc;
47
48const MAX_REDIRECT_DEPTH: usize = 5;
50
51const MAX_COMPONENT_CACHE: usize = 128;
53
54#[must_use]
72pub struct NavigationRequest {
73 pub from: Option<String>,
75
76 pub to: String,
78
79 pub params: RouteParams,
81}
82
83impl NavigationRequest {
84 pub fn new(to: String) -> Self {
86 Self {
87 from: None,
88 to,
89 params: RouteParams::new(),
90 }
91 }
92
93 pub fn with_from(to: String, from: String) -> Self {
95 Self {
96 from: Some(from),
97 to,
98 params: RouteParams::new(),
99 }
100 }
101
102 pub fn with_params(mut self, params: RouteParams) -> Self {
104 self.params = params;
105 self
106 }
107}
108
109impl std::fmt::Debug for NavigationRequest {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 f.debug_struct("NavigationRequest")
112 .field("from", &self.from)
113 .field("to", &self.to)
114 .field("params", &self.params)
115 .finish_non_exhaustive()
116 }
117}
118
119#[derive(Clone)]
129pub struct GlobalRouter {
130 state: RouterState,
131 match_stack: MatchStack,
134 #[cfg(feature = "transition")]
136 previous_stack: Option<MatchStack>,
137 #[cfg(feature = "cache")]
138 nested_cache: RouteCache,
139 named_routes: NamedRouteRegistry,
140 #[cfg(feature = "transition")]
141 next_transition: Option<Transition>,
142 component_cache: HashMap<String, AnyView>,
150 component_cache_order: std::collections::VecDeque<String>,
152 error_handlers: ErrorHandlers,
154}
155
156impl GlobalRouter {
157 #[must_use]
159 pub fn new() -> Self {
160 Self::default()
161 }
162
163 #[must_use]
168 pub const fn match_stack(&self) -> &MatchStack {
169 &self.match_stack
170 }
171
172 #[cfg(feature = "transition")]
174 #[must_use]
175 pub const fn previous_stack(&self) -> Option<&MatchStack> {
176 self.previous_stack.as_ref()
177 }
178
179 fn re_resolve(&mut self) {
181 self.match_stack = resolve_match_stack(self.state.routes(), self.state.current_path());
182 }
183
184 pub fn add_route(&mut self, route: Route) {
190 if let Some(name) = &route.config.name {
191 info_log!(
192 "Registered route '{}' (name: '{}')",
193 route.config.path,
194 name
195 );
196 self.named_routes
197 .register(name.clone(), route.config.path.clone());
198 } else {
199 info_log!("Registered route '{}'", route.config.path);
200 }
201 self.state.add_route(route);
202 #[cfg(feature = "cache")]
203 self.nested_cache.clear();
204 self.re_resolve();
206 }
207
208 pub fn push(&mut self, path: String, cx: &App) -> NavigationResult {
221 self.navigate_with_pipeline(path, cx, NavigateOp::Push, 0)
222 }
223
224 pub fn replace(&mut self, path: String, cx: &App) -> NavigationResult {
226 self.navigate_with_pipeline(path, cx, NavigateOp::Replace, 0)
227 }
228
229 pub fn back(&mut self, cx: &App) -> Option<NavigationResult> {
231 let target = self.state.peek_back_path()?.to_string();
232 Some(self.navigate_with_pipeline(target, cx, NavigateOp::Back, 0))
233 }
234
235 pub fn forward(&mut self, cx: &App) -> Option<NavigationResult> {
237 let target = self.state.peek_forward_path()?.to_string();
238 Some(self.navigate_with_pipeline(target, cx, NavigateOp::Forward, 0))
239 }
240
241 pub fn push_with_state(
247 &mut self,
248 path: String,
249 state: HistoryState,
250 cx: &App,
251 ) -> NavigationResult {
252 let result = self.navigate_with_pipeline(path, cx, NavigateOp::Push, 0);
255 if matches!(result, NavigationResult::Success { .. }) {
256 let current_path = self.state.current_path().to_string();
258 self.state.replace_with_state(current_path, state);
259 }
260 result
261 }
262
263 pub fn replace_with_state(
265 &mut self,
266 path: String,
267 state: HistoryState,
268 cx: &App,
269 ) -> NavigationResult {
270 let result = self.navigate_with_pipeline(path, cx, NavigateOp::Replace, 0);
271 if matches!(result, NavigationResult::Success { .. }) {
272 let current_path = self.state.current_path().to_string();
273 self.state.replace_with_state(current_path, state);
274 }
275 result
276 }
277
278 #[must_use]
280 pub fn current_entry(&self) -> &HistoryEntry {
281 self.state.current_entry()
282 }
283
284 fn navigate_with_pipeline(
286 &mut self,
287 path: String,
288 cx: &App,
289 op: NavigateOp,
290 redirect_depth: usize,
291 ) -> NavigationResult {
292 if redirect_depth >= MAX_REDIRECT_DEPTH {
293 error_log!(
294 "Redirect loop detected (depth {}) navigating to '{}'",
295 redirect_depth,
296 path
297 );
298 return NavigationResult::Blocked {
299 reason: format!("Redirect loop detected (depth {redirect_depth}): target '{path}'"),
300 redirect: None,
301 };
302 }
303
304 let from = self.current_path().to_string();
305 info_log!("Navigation {:?}: '{}' → '{}'", op, from, path);
306
307 let request = NavigationRequest::with_from(path.clone(), from.clone());
309
310 #[cfg(feature = "guard")]
312 {
313 let guard_result = self.run_guards(cx, &request);
314 match guard_result {
315 NavigationAction::Continue => {}
316 NavigationAction::Deny { reason } => {
317 warn_log!("Navigation to '{}' blocked: {}", path, reason);
318 return NavigationResult::Blocked {
319 reason,
320 redirect: None,
321 };
322 }
323 NavigationAction::Redirect { to, reason } => {
324 debug_log!(
325 "Guard redirecting from '{}' to '{}': {:?}",
326 path,
327 to,
328 reason
329 );
330 return self.navigate_with_pipeline(
331 to,
332 cx,
333 NavigateOp::Push,
334 redirect_depth + 1,
335 );
336 }
337 }
338 }
339
340 match self.run_lifecycle_can_deactivate(cx) {
342 NavigationAction::Continue => {}
343 NavigationAction::Deny { reason } => {
344 warn_log!(
345 "Lifecycle can_deactivate blocked leaving '{}': {}",
346 from,
347 reason
348 );
349 return NavigationResult::Blocked {
350 reason,
351 redirect: None,
352 };
353 }
354 NavigationAction::Redirect { to, .. } => {
355 return self.navigate_with_pipeline(to, cx, NavigateOp::Push, redirect_depth + 1);
356 }
357 }
358
359 #[cfg(feature = "middleware")]
361 self.run_middleware_before(cx, &request);
362
363 if let NavigationAction::Deny { reason } = self.run_lifecycle_on_exit(cx) {
365 warn_log!("Lifecycle on_exit blocked leaving '{}': {}", from, reason);
366 return NavigationResult::Blocked {
367 reason,
368 redirect: None,
369 };
370 }
371
372 let event = match self.perform_navigation(path, op) {
374 Ok(event) => event,
375 Err(result) => return result,
376 };
377
378 match self.run_lifecycle_on_enter(cx, &request) {
380 NavigationAction::Continue => {}
381 NavigationAction::Deny { reason } => {
382 warn_log!(
384 "Lifecycle on_enter denied entry to '{}': {}",
385 event.to,
386 reason
387 );
388 }
389 NavigationAction::Redirect { to, .. } => {
390 return self.navigate_with_pipeline(to, cx, NavigateOp::Push, redirect_depth + 1);
391 }
392 }
393
394 #[cfg(feature = "middleware")]
396 self.run_middleware_after(cx, &request);
397
398 info_log!(
399 "Navigation complete: '{}' (stack depth: {})",
400 event.to,
401 self.match_stack.len()
402 );
403 NavigationResult::Success { path: event.to }
404 }
405
406 fn perform_navigation(
415 &mut self,
416 path: String,
417 op: NavigateOp,
418 ) -> Result<crate::RouteChangeEvent, NavigationResult> {
419 #[cfg(feature = "cache")]
420 self.nested_cache.clear();
421
422 #[cfg(feature = "transition")]
423 {
424 self.previous_stack = Some(self.match_stack.clone());
425 }
426
427 let event = match op {
428 NavigateOp::Push => self.state.push(path),
429 NavigateOp::Replace => self.state.replace(path),
430 NavigateOp::Back => self.state.back().ok_or_else(|| {
431 error_log!("back() returned None after peek succeeded");
432 NavigationResult::Error(crate::error::NavigationError::NavigationFailed {
433 message: "History back failed unexpectedly".into(),
434 })
435 })?,
436 NavigateOp::Forward => self.state.forward().ok_or_else(|| {
437 error_log!("forward() returned None after peek succeeded");
438 NavigationResult::Error(crate::error::NavigationError::NavigationFailed {
439 message: "History forward failed unexpectedly".into(),
440 })
441 })?,
442 };
443
444 self.match_stack = resolve_match_stack(self.state.routes(), self.state.current_path());
445 Ok(event)
446 }
447
448 fn run_lifecycle_can_deactivate(&self, cx: &App) -> NavigationAction {
454 if let Some(current_route) = self.state.current_route() {
455 if let Some(ref lifecycle) = current_route.lifecycle {
456 return lifecycle.can_deactivate(cx);
457 }
458 }
459 NavigationAction::Continue
460 }
461
462 fn run_lifecycle_on_exit(&self, cx: &App) -> NavigationAction {
464 if let Some(current_route) = self.state.current_route() {
465 if let Some(ref lifecycle) = current_route.lifecycle {
466 return lifecycle.on_exit(cx);
467 }
468 }
469 NavigationAction::Continue
470 }
471
472 fn run_lifecycle_on_enter(&self, cx: &App, request: &NavigationRequest) -> NavigationAction {
474 if let Some(leaf) = self.match_stack.leaf() {
475 if let Some(ref lifecycle) = leaf.route.lifecycle {
476 return lifecycle.on_enter(cx, request);
477 }
478 }
479 NavigationAction::Continue
480 }
481
482 #[cfg(feature = "guard")]
488 fn run_guards(&self, cx: &App, request: &NavigationRequest) -> NavigationAction {
489 let path = trim_slashes(&request.to);
490 let mut guards: Vec<(&dyn crate::guards::RouteGuard, i32)> = Vec::new();
491
492 for route in self.state.routes() {
494 Self::collect_guards_recursive(route, path, "", &mut guards);
495 }
496
497 guards.sort_by_key(|(_, prio)| std::cmp::Reverse(*prio));
499
500 debug_log!("Collected {} guards for '{}'", guards.len(), path);
501
502 for (guard, prio) in &guards {
504 let result = guard.check(cx, request);
505 trace_log!(
506 "Guard '{}' (priority {}) → {:?}",
507 guard.name(),
508 prio,
509 result
510 );
511 if !matches!(result, NavigationAction::Continue) {
512 debug_log!(
513 "Guard '{}' blocked navigation to '{}'",
514 guard.name(),
515 request.to
516 );
517 return result;
518 }
519 }
520
521 NavigationAction::Continue
522 }
523
524 #[cfg(feature = "guard")]
527 fn collect_guards_recursive<'a>(
528 route: &'a Arc<Route>,
529 path: &str,
530 accumulated: &str,
531 out: &mut Vec<(&'a dyn crate::guards::RouteGuard, i32)>,
532 ) {
533 walk_matching_routes(route, path, accumulated, &mut |r, _full| {
534 for guard in &r.guards {
535 out.push((guard.as_ref(), guard.priority()));
536 }
537 });
538 }
539
540 #[cfg(feature = "middleware")]
542 fn run_middleware_before(&self, cx: &App, request: &NavigationRequest) {
543 let path = trim_slashes(&request.to);
544 let mut middleware: Vec<(&dyn crate::middleware::RouteMiddleware, i32)> = Vec::new();
545
546 for route in self.state.routes() {
547 Self::collect_middleware_recursive(route, path, "", &mut middleware);
548 }
549
550 middleware.sort_by_key(|(_, prio)| std::cmp::Reverse(*prio));
552
553 debug_log!(
554 "Running {} before-middleware for '{}'",
555 middleware.len(),
556 request.to
557 );
558 for (mw, _) in &middleware {
559 trace_log!(
560 "Middleware '{}' before_navigation for '{}'",
561 mw.name(),
562 request.to
563 );
564 mw.before_navigation(cx, request);
565 }
566 }
567
568 #[cfg(feature = "middleware")]
570 fn run_middleware_after(&self, cx: &App, request: &NavigationRequest) {
571 let path = trim_slashes(&request.to);
572 let mut middleware: Vec<(&dyn crate::middleware::RouteMiddleware, i32)> = Vec::new();
573
574 for route in self.state.routes() {
575 Self::collect_middleware_recursive(route, path, "", &mut middleware);
576 }
577
578 middleware.sort_by_key(|(_, prio)| *prio);
580
581 debug_log!(
582 "Running {} after-middleware for '{}'",
583 middleware.len(),
584 request.to
585 );
586 for (mw, _) in &middleware {
587 trace_log!(
588 "Middleware '{}' after_navigation for '{}'",
589 mw.name(),
590 request.to
591 );
592 mw.after_navigation(cx, request);
593 }
594 }
595
596 #[cfg(feature = "middleware")]
598 fn collect_middleware_recursive<'a>(
599 route: &'a Arc<Route>,
600 path: &str,
601 accumulated: &str,
602 out: &mut Vec<(&'a dyn crate::middleware::RouteMiddleware, i32)>,
603 ) {
604 walk_matching_routes(route, path, accumulated, &mut |r, _full| {
605 for mw in &r.middleware {
606 out.push((mw.as_ref(), mw.priority()));
607 }
608 });
609 }
610
611 pub fn push_named(
619 &mut self,
620 name: &str,
621 params: &RouteParams,
622 cx: &App,
623 ) -> Option<NavigationResult> {
624 let url = if let Some(url) = self.named_routes.url_for(name, params) {
625 debug_log!("Named route '{}' resolved to '{}'", name, url);
626 url
627 } else {
628 warn_log!("Named route '{}' not found in registry", name);
629 return None;
630 };
631 Some(self.push(url, cx))
632 }
633
634 #[must_use]
638 pub fn url_for(&self, name: &str, params: &RouteParams) -> Option<String> {
639 self.named_routes.url_for(name, params)
640 }
641
642 #[must_use]
648 pub fn current_path(&self) -> &str {
649 self.state.current_path()
650 }
651
652 pub fn current_match(&mut self) -> Option<crate::RouteMatch> {
654 self.state.current_match()
655 }
656
657 #[must_use]
659 pub fn current_match_immutable(&self) -> Option<crate::RouteMatch> {
660 self.state.current_match_immutable()
661 }
662
663 #[must_use]
665 pub fn current_route(&self) -> Option<&Arc<crate::route::Route>> {
666 self.state.current_route()
667 }
668
669 #[must_use]
671 pub const fn can_go_back(&self) -> bool {
672 self.state.can_go_back()
673 }
674
675 #[must_use]
677 pub fn can_go_forward(&self) -> bool {
678 self.state.can_go_forward()
679 }
680
681 pub fn state_mut(&mut self) -> &mut RouterState {
683 &mut self.state
684 }
685
686 #[must_use]
688 pub const fn state(&self) -> &RouterState {
689 &self.state
690 }
691
692 #[cfg(feature = "cache")]
694 pub fn nested_cache_mut(&mut self) -> &mut RouteCache {
695 &mut self.nested_cache
696 }
697
698 #[cfg(feature = "cache")]
700 #[must_use]
701 pub const fn cache_stats(&self) -> &CacheStats {
702 self.nested_cache.stats()
703 }
704
705 pub fn set_error_handlers(&mut self, handlers: ErrorHandlers) {
711 self.error_handlers = handlers;
712 }
713
714 pub const fn error_handlers(&self) -> &ErrorHandlers {
716 &self.error_handlers
717 }
718
719 #[must_use]
725 pub fn get_cached_component(&self, key: &str) -> Option<&AnyView> {
726 self.component_cache.get(key)
727 }
728
729 pub fn cache_component(&mut self, key: String, view: AnyView) {
731 if !self.component_cache.contains_key(&key) {
732 while self.component_cache.len() >= MAX_COMPONENT_CACHE {
734 if let Some(oldest_key) = self.component_cache_order.pop_front() {
735 self.component_cache.remove(&oldest_key);
736 } else {
737 break;
738 }
739 }
740 self.component_cache_order.push_back(key.clone());
741 }
742 self.component_cache.insert(key, view);
743 }
744
745 #[cfg(feature = "transition")]
751 pub fn set_next_transition(&mut self, transition: Transition) {
752 self.next_transition = Some(transition);
753 }
754
755 #[cfg(feature = "transition")]
757 pub fn take_next_transition(&mut self) -> Option<Transition> {
758 self.next_transition.take()
759 }
760
761 #[cfg(feature = "transition")]
763 #[must_use]
764 pub const fn has_next_transition(&self) -> bool {
765 self.next_transition.is_some()
766 }
767
768 #[cfg(feature = "transition")]
770 pub fn clear_next_transition(&mut self) {
771 self.next_transition = None;
772 }
773
774 #[cfg(feature = "transition")]
776 pub fn push_with_transition(
777 &mut self,
778 path: String,
779 transition: Transition,
780 cx: &App,
781 ) -> NavigationResult {
782 self.set_next_transition(transition);
783 self.push(path, cx)
784 }
785
786 #[cfg(feature = "transition")]
788 pub fn replace_with_transition(
789 &mut self,
790 path: String,
791 transition: Transition,
792 cx: &App,
793 ) -> NavigationResult {
794 self.set_next_transition(transition);
795 self.replace(path, cx)
796 }
797}
798
799impl Default for GlobalRouter {
800 fn default() -> Self {
801 Self {
802 state: RouterState::new(),
803 match_stack: MatchStack::new(),
804 #[cfg(feature = "transition")]
805 previous_stack: None,
806 #[cfg(feature = "cache")]
807 nested_cache: RouteCache::new(),
808 named_routes: NamedRouteRegistry::new(),
809 #[cfg(feature = "transition")]
810 next_transition: None,
811 component_cache: HashMap::new(),
812 component_cache_order: std::collections::VecDeque::new(),
813 error_handlers: ErrorHandlers::new(),
814 }
815 }
816}
817
818impl Global for GlobalRouter {}
819
820fn walk_matching_routes<'a>(
831 route: &'a Arc<Route>,
832 target_path: &str,
833 accumulated: &str,
834 visitor: &mut dyn FnMut(&'a Route, &str),
835) {
836 let route_path = trim_slashes(&route.config.path);
837
838 let full: std::borrow::Cow<'_, str> = if accumulated.is_empty() {
840 std::borrow::Cow::Borrowed(route_path)
841 } else if route_path.is_empty() {
842 std::borrow::Cow::Borrowed(accumulated)
843 } else {
844 std::borrow::Cow::Owned(format!("{accumulated}/{route_path}"))
845 };
846
847 if !full.is_empty() && !path_matches_prefix(target_path, &full) {
848 return;
849 }
850
851 visitor(route, &full);
852
853 for child in route.get_children() {
854 walk_matching_routes(child, target_path, &full, visitor);
855 }
856}
857
858fn path_matches_prefix(path: &str, prefix: &str) -> bool {
868 let mut path_segs = path.split('/').filter(|s| !s.is_empty());
869 let prefix_segs = prefix.split('/').filter(|s| !s.is_empty());
870
871 for pfs in prefix_segs {
872 let Some(ps) = path_segs.next() else {
873 return false;
875 };
876 if pfs.starts_with(':') {
877 continue;
878 }
879 if ps != pfs {
880 return false;
881 }
882 }
883
884 true
885}
886
887#[derive(Debug, Clone, Copy)]
893enum NavigateOp {
894 Push,
895 Replace,
896 Back,
897 Forward,
898}
899
900pub trait UseRouter {
906 fn router(&self) -> &GlobalRouter;
908
909 fn update_router<F, R>(&mut self, f: F) -> R
911 where
912 F: FnOnce(&mut GlobalRouter, &mut App) -> R;
913}
914
915impl UseRouter for App {
916 fn router(&self) -> &GlobalRouter {
917 self.global::<GlobalRouter>()
918 }
919
920 fn update_router<F, R>(&mut self, f: F) -> R
921 where
922 F: FnOnce(&mut GlobalRouter, &mut Self) -> R,
923 {
924 self.update_global(f)
925 }
926}
927
928pub fn init_router<F>(cx: &mut App, configure: F)
945where
946 F: FnOnce(&mut GlobalRouter),
947{
948 let mut router = GlobalRouter::new();
949 configure(&mut router);
950 cx.set_global(router);
951}
952
953pub fn navigate(cx: &mut App, path: impl Into<String>) {
958 let path = path.into();
959 cx.update_global::<GlobalRouter, _>(|router, cx| {
960 router.push(path, cx);
961 });
962 cx.refresh_windows();
963}
964
965pub fn current_path(cx: &App) -> String {
967 cx.router().current_path().to_string()
968}
969
970#[must_use]
984pub struct NavigatorHandle<'a, C: BorrowAppContext> {
985 cx: &'a mut C,
986}
987
988impl<C: BorrowAppContext + BorrowMut<App>> NavigatorHandle<'_, C> {
989 pub fn push(self, route: impl IntoRoute) -> Self {
991 let descriptor = route.into_route();
992 self.cx.update_global::<GlobalRouter, _>(|router, cx| {
993 let app: &App = cx.borrow_mut();
994 router.push(descriptor.path, app);
995 });
996 self.cx.borrow_mut().refresh_windows();
997 self
998 }
999
1000 pub fn replace(self, route: impl IntoRoute) -> Self {
1002 let descriptor = route.into_route();
1003 self.cx.update_global::<GlobalRouter, _>(|router, cx| {
1004 let app: &App = cx.borrow_mut();
1005 router.replace(descriptor.path, app);
1006 });
1007 self.cx.borrow_mut().refresh_windows();
1008 self
1009 }
1010
1011 pub fn pop(self) -> Self {
1013 self.cx.update_global::<GlobalRouter, _>(|router, cx| {
1014 let app: &App = cx.borrow_mut();
1015 router.back(app);
1016 });
1017 self.cx.borrow_mut().refresh_windows();
1018 self
1019 }
1020
1021 pub fn forward(self) -> Self {
1023 self.cx.update_global::<GlobalRouter, _>(|router, cx| {
1024 let app: &App = cx.borrow_mut();
1025 router.forward(app);
1026 });
1027 self.cx.borrow_mut().refresh_windows();
1028 self
1029 }
1030}
1031
1032pub struct Navigator;
1055
1056impl Navigator {
1057 pub fn of<C: BorrowAppContext + BorrowMut<App>>(cx: &mut C) -> NavigatorHandle<'_, C> {
1059 NavigatorHandle { cx }
1060 }
1061
1062 pub fn push(cx: &mut (impl BorrowAppContext + BorrowMut<App>), route: impl IntoRoute) {
1064 let descriptor = route.into_route();
1065 debug_log!("Navigator::push: pushing path '{}'", descriptor.path);
1066 cx.update_global::<GlobalRouter, _>(|router, cx| {
1067 let app: &App = cx.borrow_mut();
1068 router.push(descriptor.path, app);
1069 });
1070 cx.borrow_mut().refresh_windows();
1071 }
1072
1073 pub fn replace(cx: &mut (impl BorrowAppContext + BorrowMut<App>), route: impl IntoRoute) {
1075 let descriptor = route.into_route();
1076 cx.update_global::<GlobalRouter, _>(|router, cx| {
1077 let app: &App = cx.borrow_mut();
1078 router.replace(descriptor.path, app);
1079 });
1080 cx.borrow_mut().refresh_windows();
1081 }
1082
1083 pub fn push_with_state(
1085 cx: &mut (impl BorrowAppContext + BorrowMut<App>),
1086 route: impl IntoRoute,
1087 state: HistoryState,
1088 ) {
1089 let descriptor = route.into_route();
1090 cx.update_global::<GlobalRouter, _>(|router, cx| {
1091 let app: &App = cx.borrow_mut();
1092 router.push_with_state(descriptor.path, state, app);
1093 });
1094 cx.borrow_mut().refresh_windows();
1095 }
1096
1097 pub fn replace_with_state(
1099 cx: &mut (impl BorrowAppContext + BorrowMut<App>),
1100 route: impl IntoRoute,
1101 state: HistoryState,
1102 ) {
1103 let descriptor = route.into_route();
1104 cx.update_global::<GlobalRouter, _>(|router, cx| {
1105 let app: &App = cx.borrow_mut();
1106 router.replace_with_state(descriptor.path, state, app);
1107 });
1108 cx.borrow_mut().refresh_windows();
1109 }
1110
1111 pub fn current_entry(cx: &App) -> HistoryEntry {
1113 cx.global::<GlobalRouter>().current_entry().clone()
1114 }
1115
1116 pub fn pop(cx: &mut (impl BorrowAppContext + BorrowMut<App>)) {
1118 cx.update_global::<GlobalRouter, _>(|router, cx| {
1119 let app: &App = cx.borrow_mut();
1120 router.back(app);
1121 });
1122 cx.borrow_mut().refresh_windows();
1123 }
1124
1125 pub fn back(cx: &mut (impl BorrowAppContext + BorrowMut<App>)) {
1127 Self::pop(cx);
1128 }
1129
1130 pub fn forward(cx: &mut (impl BorrowAppContext + BorrowMut<App>)) {
1132 cx.update_global::<GlobalRouter, _>(|router, cx| {
1133 let app: &App = cx.borrow_mut();
1134 router.forward(app);
1135 });
1136 cx.borrow_mut().refresh_windows();
1137 }
1138
1139 pub fn current_path(cx: &App) -> String {
1141 cx.global::<GlobalRouter>().current_path().to_string()
1142 }
1143
1144 pub fn can_pop(cx: &App) -> bool {
1146 cx.global::<GlobalRouter>().can_go_back()
1147 }
1148
1149 pub fn can_go_back(cx: &App) -> bool {
1151 Self::can_pop(cx)
1152 }
1153
1154 pub fn can_go_forward(cx: &App) -> bool {
1156 cx.global::<GlobalRouter>().can_go_forward()
1157 }
1158
1159 pub fn push_named(
1161 cx: &mut (impl BorrowAppContext + BorrowMut<App>),
1162 name: &str,
1163 params: &RouteParams,
1164 ) {
1165 let name = name.to_string();
1166 let params = params.clone();
1167 cx.update_global::<GlobalRouter, _>(|router, cx| {
1168 let app: &App = cx.borrow_mut();
1169 router.push_named(&name, ¶ms, app);
1170 });
1171 cx.borrow_mut().refresh_windows();
1172 }
1173
1174 pub fn url_for(cx: &App, name: &str, params: &RouteParams) -> Option<String> {
1176 cx.global::<GlobalRouter>().url_for(name, params)
1177 }
1178
1179 #[cfg(feature = "transition")]
1181 pub fn set_next_transition(cx: &mut impl BorrowAppContext, transition: Transition) {
1182 cx.update_global::<GlobalRouter, _>(|router, _| {
1183 router.set_next_transition(transition);
1184 });
1185 }
1186
1187 #[cfg(feature = "transition")]
1189 pub fn push_with_transition(
1190 cx: &mut (impl BorrowAppContext + BorrowMut<App>),
1191 route: impl IntoRoute,
1192 transition: Transition,
1193 ) {
1194 let descriptor = route.into_route();
1195 cx.update_global::<GlobalRouter, _>(|router, cx| {
1196 let app: &App = cx.borrow_mut();
1197 router.push_with_transition(descriptor.path, transition, app);
1198 });
1199 cx.borrow_mut().refresh_windows();
1200 }
1201
1202 #[cfg(feature = "transition")]
1204 pub fn replace_with_transition(
1205 cx: &mut (impl BorrowAppContext + BorrowMut<App>),
1206 route: impl IntoRoute,
1207 transition: Transition,
1208 ) {
1209 let descriptor = route.into_route();
1210 cx.update_global::<GlobalRouter, _>(|router, cx| {
1211 let app: &App = cx.borrow_mut();
1212 router.replace_with_transition(descriptor.path, transition, app);
1213 });
1214 cx.borrow_mut().refresh_windows();
1215 }
1216
1217 #[cfg(feature = "transition")]
1219 pub fn push_named_with_transition(
1220 cx: &mut (impl BorrowAppContext + BorrowMut<App>),
1221 name: &str,
1222 params: &RouteParams,
1223 transition: Transition,
1224 ) {
1225 let name = name.to_string();
1226 let params = params.clone();
1227 cx.update_global::<GlobalRouter, _>(|router, cx| {
1228 let app: &App = cx.borrow_mut();
1229 router.set_next_transition(transition);
1230 router.push_named(&name, ¶ms, app);
1231 });
1232 cx.borrow_mut().refresh_windows();
1233 }
1234}
1235
1236#[cfg(test)]
1241#[allow(clippy::needless_pass_by_ref_mut)]
1242mod tests {
1243 use super::*;
1244 use gpui::{IntoElement, TestAppContext};
1245
1246 #[gpui::test]
1247 fn test_nav_push(cx: &mut TestAppContext) {
1248 cx.update(|cx| {
1249 init_router(cx, |router| {
1250 router.add_route(Route::new("/", |_, _cx, _params| {
1251 gpui::div().into_any_element()
1252 }));
1253 router.add_route(Route::new("/users", |_, _cx, _params| {
1254 gpui::div().into_any_element()
1255 }));
1256 router.add_route(Route::new("/users/:id", |_, _cx, _params| {
1257 gpui::div().into_any_element()
1258 }));
1259 });
1260 });
1261
1262 let initial_path = cx.read(Navigator::current_path);
1263 assert_eq!(initial_path, "/");
1264
1265 cx.update(|cx| Navigator::push(cx, "/users"));
1266 assert_eq!(cx.read(Navigator::current_path), "/users");
1267
1268 cx.update(|cx| Navigator::push(cx, "/users/123"));
1269 assert_eq!(cx.read(Navigator::current_path), "/users/123");
1270 }
1271
1272 #[gpui::test]
1273 fn test_nav_back_forward(cx: &mut TestAppContext) {
1274 cx.update(|cx| {
1275 init_router(cx, |router| {
1276 router.add_route(Route::new("/", |_, _cx, _params| {
1277 gpui::div().into_any_element()
1278 }));
1279 router.add_route(Route::new("/page1", |_, _cx, _params| {
1280 gpui::div().into_any_element()
1281 }));
1282 router.add_route(Route::new("/page2", |_, _cx, _params| {
1283 gpui::div().into_any_element()
1284 }));
1285 });
1286 });
1287
1288 cx.update(|cx| {
1289 Navigator::push(cx, "/page1");
1290 Navigator::push(cx, "/page2");
1291 });
1292
1293 assert_eq!(cx.read(Navigator::current_path), "/page2");
1294 assert!(cx.read(Navigator::can_pop));
1295
1296 cx.update(Navigator::pop);
1297 assert_eq!(cx.read(Navigator::current_path), "/page1");
1298 assert!(cx.read(Navigator::can_pop));
1299 assert!(cx.read(Navigator::can_go_forward));
1300
1301 cx.update(Navigator::forward);
1302 assert_eq!(cx.read(Navigator::current_path), "/page2");
1303 assert!(!cx.read(Navigator::can_go_forward));
1304 }
1305
1306 #[gpui::test]
1307 fn test_nav_replace(cx: &mut TestAppContext) {
1308 cx.update(|cx| {
1309 init_router(cx, |router| {
1310 router.add_route(Route::new("/", |_, _cx, _params| {
1311 gpui::div().into_any_element()
1312 }));
1313 router.add_route(Route::new("/login", |_, _cx, _params| {
1314 gpui::div().into_any_element()
1315 }));
1316 router.add_route(Route::new("/home", |_, _cx, _params| {
1317 gpui::div().into_any_element()
1318 }));
1319 });
1320 });
1321
1322 cx.update(|cx| {
1323 Navigator::push(cx, "/login");
1324 Navigator::replace(cx, "/home");
1325 });
1326
1327 assert_eq!(cx.read(Navigator::current_path), "/home");
1328
1329 cx.update(Navigator::pop);
1330 assert_eq!(cx.read(Navigator::current_path), "/");
1331 }
1332
1333 #[gpui::test]
1334 fn test_nav_can_go_back_boundaries(cx: &mut TestAppContext) {
1335 cx.update(|cx| {
1336 init_router(cx, |router| {
1337 router.add_route(Route::new("/", |_, _cx, _params| {
1338 gpui::div().into_any_element()
1339 }));
1340 });
1341 });
1342
1343 assert!(!cx.read(Navigator::can_pop));
1344
1345 cx.update(|cx| Navigator::push(cx, "/page1"));
1346 assert!(cx.read(Navigator::can_pop));
1347
1348 cx.update(Navigator::pop);
1349 assert!(!cx.read(Navigator::can_pop));
1350 }
1351
1352 #[gpui::test]
1353 fn test_nav_multiple_pushes(cx: &mut TestAppContext) {
1354 cx.update(|cx| {
1355 init_router(cx, |router| {
1356 router.add_route(Route::new("/", |_, _cx, _params| {
1357 gpui::div().into_any_element()
1358 }));
1359 router.add_route(Route::new("/step1", |_, _cx, _params| {
1360 gpui::div().into_any_element()
1361 }));
1362 router.add_route(Route::new("/step2", |_, _cx, _params| {
1363 gpui::div().into_any_element()
1364 }));
1365 router.add_route(Route::new("/step3", |_, _cx, _params| {
1366 gpui::div().into_any_element()
1367 }));
1368 });
1369 });
1370
1371 cx.update(|cx| {
1372 Navigator::push(cx, "/step1");
1373 Navigator::push(cx, "/step2");
1374 Navigator::push(cx, "/step3");
1375 });
1376
1377 assert_eq!(cx.read(Navigator::current_path), "/step3");
1378
1379 cx.update(Navigator::pop);
1380 assert_eq!(cx.read(Navigator::current_path), "/step2");
1381
1382 cx.update(Navigator::pop);
1383 assert_eq!(cx.read(Navigator::current_path), "/step1");
1384
1385 cx.update(Navigator::pop);
1386 assert_eq!(cx.read(Navigator::current_path), "/");
1387 }
1388
1389 #[gpui::test]
1390 fn test_nav_with_route_parameters(cx: &mut TestAppContext) {
1391 cx.update(|cx| {
1392 init_router(cx, |router| {
1393 router.add_route(Route::new("/", |_, _cx, _params| {
1394 gpui::div().into_any_element()
1395 }));
1396 router.add_route(Route::new("/users/:id", |_, _cx, _params| {
1397 gpui::div().into_any_element()
1398 }));
1399 router.add_route(Route::new(
1400 "/posts/:id/comments/:commentId",
1401 |_, _cx, _params| gpui::div().into_any_element(),
1402 ));
1403 });
1404 });
1405
1406 cx.update(|cx| Navigator::push(cx, "/users/42"));
1407 assert_eq!(cx.read(Navigator::current_path), "/users/42");
1408
1409 cx.update(|cx| Navigator::push(cx, "/posts/123/comments/456"));
1410 assert_eq!(cx.read(Navigator::current_path), "/posts/123/comments/456");
1411 }
1412
1413 #[gpui::test]
1414 fn test_navigator_of_style(cx: &mut TestAppContext) {
1415 cx.update(|cx| {
1416 init_router(cx, |router| {
1417 router.add_route(Route::new("/", |_, _cx, _params| {
1418 gpui::div().into_any_element()
1419 }));
1420 router.add_route(Route::new("/home", |_, _cx, _params| {
1421 gpui::div().into_any_element()
1422 }));
1423 router.add_route(Route::new("/profile", |_, _cx, _params| {
1424 gpui::div().into_any_element()
1425 }));
1426 });
1427 });
1428
1429 cx.update(|cx| {
1430 let _ = Navigator::of(cx).push("/home");
1431 });
1432 assert_eq!(cx.read(Navigator::current_path), "/home");
1433
1434 cx.update(|cx| {
1435 let _ = Navigator::of(cx).push("/profile").pop();
1436 });
1437 assert_eq!(cx.read(Navigator::current_path), "/home");
1438
1439 cx.update(|cx| {
1440 let _ = Navigator::of(cx).replace("/profile");
1441 });
1442 assert_eq!(cx.read(Navigator::current_path), "/profile");
1443
1444 assert!(cx.read(Navigator::can_pop));
1445 cx.update(|cx| {
1446 let _ = Navigator::of(cx).pop();
1447 });
1448 assert_eq!(cx.read(Navigator::current_path), "/");
1449 assert!(!cx.read(Navigator::can_pop));
1450 }
1451
1452 #[gpui::test]
1453 fn test_string_into_route(cx: &mut TestAppContext) {
1454 cx.update(|cx| {
1455 init_router(cx, |router| {
1456 router.add_route(Route::new("/", |_, _cx, _params| {
1457 gpui::div().into_any_element()
1458 }));
1459 router.add_route(Route::new("/home", |_, _cx, _params| {
1460 gpui::div().into_any_element()
1461 }));
1462 });
1463 });
1464
1465 cx.update(|cx| Navigator::push(cx, "/home"));
1466 assert_eq!(cx.read(Navigator::current_path), "/home");
1467
1468 cx.update(|cx| Navigator::push(cx, String::from("/home")));
1469 assert_eq!(cx.read(Navigator::current_path), "/home");
1470 }
1471
1472 #[gpui::test]
1477 #[cfg(feature = "guard")]
1478 fn test_guard_blocks_navigation(cx: &mut TestAppContext) {
1479 use crate::AuthGuard;
1480
1481 cx.update(|cx| {
1482 init_router(cx, |router| {
1483 router.add_route(Route::new("/", |_, _cx, _params| {
1484 gpui::div().into_any_element()
1485 }));
1486 router.add_route(
1487 Route::new("/protected", |_, _cx, _params| {
1488 gpui::div().into_any_element()
1489 })
1490 .guard(AuthGuard::new(|_| false, "/login")),
1491 );
1492 router.add_route(Route::new("/login", |_, _cx, _params| {
1493 gpui::div().into_any_element()
1494 }));
1495 });
1496 });
1497
1498 cx.update(|cx| Navigator::push(cx, "/protected"));
1500
1501 assert_eq!(cx.read(Navigator::current_path), "/login");
1503 }
1504
1505 #[gpui::test]
1506 #[cfg(feature = "guard")]
1507 fn test_guard_allows_navigation(cx: &mut TestAppContext) {
1508 use crate::AuthGuard;
1509
1510 cx.update(|cx| {
1511 init_router(cx, |router| {
1512 router.add_route(Route::new("/", |_, _cx, _params| {
1513 gpui::div().into_any_element()
1514 }));
1515 router.add_route(
1516 Route::new("/dashboard", |_, _cx, _params| {
1517 gpui::div().into_any_element()
1518 })
1519 .guard(AuthGuard::new(|_| true, "/login")),
1520 );
1521 });
1522 });
1523
1524 cx.update(|cx| Navigator::push(cx, "/dashboard"));
1525 assert_eq!(cx.read(Navigator::current_path), "/dashboard");
1526 }
1527
1528 #[gpui::test]
1529 #[cfg(feature = "guard")]
1530 fn test_guard_denies_navigation(cx: &mut TestAppContext) {
1531 use crate::guard_fn;
1532
1533 cx.update(|cx| {
1534 init_router(cx, |router| {
1535 router.add_route(Route::new("/", |_, _cx, _params| {
1536 gpui::div().into_any_element()
1537 }));
1538 router.add_route(
1539 Route::new("/forbidden", |_, _cx, _params| {
1540 gpui::div().into_any_element()
1541 })
1542 .guard(guard_fn(|_, _| NavigationAction::deny("No access"))),
1543 );
1544 });
1545 });
1546
1547 cx.update(|cx| Navigator::push(cx, "/forbidden"));
1548 assert_eq!(cx.read(Navigator::current_path), "/");
1550 }
1551
1552 #[gpui::test]
1553 #[cfg(feature = "guard")]
1554 fn test_parent_guard_blocks_child(cx: &mut TestAppContext) {
1555 use crate::AuthGuard;
1556
1557 cx.update(|cx| {
1558 init_router(cx, |router| {
1559 router.add_route(Route::new("/", |_, _cx, _params| {
1560 gpui::div().into_any_element()
1561 }));
1562 router.add_route(
1563 Route::new("/dashboard", |_, _cx, _params| {
1564 gpui::div().into_any_element()
1565 })
1566 .guard(AuthGuard::new(|_| false, "/login"))
1567 .child(
1568 Route::new("settings", |_, _cx, _params| gpui::div().into_any_element())
1569 .into(),
1570 ),
1571 );
1572 router.add_route(Route::new("/login", |_, _cx, _params| {
1573 gpui::div().into_any_element()
1574 }));
1575 });
1576 });
1577
1578 cx.update(|cx| Navigator::push(cx, "/dashboard/settings"));
1580 assert_eq!(cx.read(Navigator::current_path), "/login");
1581 }
1582
1583 #[gpui::test]
1584 #[cfg(feature = "guard")]
1585 fn test_redirect_loop_protection(cx: &mut TestAppContext) {
1586 use crate::guard_fn;
1587
1588 cx.update(|cx| {
1589 init_router(cx, |router| {
1590 router.add_route(Route::new("/", |_, _cx, _params| {
1591 gpui::div().into_any_element()
1592 }));
1593 router.add_route(
1595 Route::new("/a", |_, _cx, _params| gpui::div().into_any_element())
1596 .guard(guard_fn(|_, _| NavigationAction::redirect("/b"))),
1597 );
1598 router.add_route(
1599 Route::new("/b", |_, _cx, _params| gpui::div().into_any_element())
1600 .guard(guard_fn(|_, _| NavigationAction::redirect("/a"))),
1601 );
1602 });
1603 });
1604
1605 cx.update(|cx| Navigator::push(cx, "/a"));
1607 assert_eq!(cx.read(Navigator::current_path), "/");
1609 }
1610
1611 #[gpui::test]
1616 #[cfg(feature = "middleware")]
1617 fn test_middleware_runs_during_navigation(cx: &mut TestAppContext) {
1618 use crate::middleware_fn;
1619 use std::sync::{Arc, Mutex};
1620
1621 let calls = Arc::new(Mutex::new(Vec::<String>::new()));
1622 let before_calls = calls.clone();
1623 let after_calls = calls.clone();
1624
1625 cx.update(|cx| {
1626 init_router(cx, |router| {
1627 router.add_route(Route::new("/", |_, _cx, _params| {
1628 gpui::div().into_any_element()
1629 }));
1630 router.add_route(
1631 Route::new("/page", |_, _cx, _params| gpui::div().into_any_element())
1632 .middleware(middleware_fn(
1633 move |_cx, req| {
1634 before_calls
1635 .lock()
1636 .unwrap()
1637 .push(format!("before:{}", req.to));
1638 },
1639 move |_cx, req| {
1640 after_calls
1641 .lock()
1642 .unwrap()
1643 .push(format!("after:{}", req.to));
1644 },
1645 )),
1646 );
1647 });
1648 });
1649
1650 cx.update(|cx| Navigator::push(cx, "/page"));
1651 assert_eq!(cx.read(Navigator::current_path), "/page");
1652
1653 let log = calls.lock().unwrap();
1654 assert_eq!(log.len(), 2);
1655 assert_eq!(log[0], "before:/page");
1656 assert_eq!(log[1], "after:/page");
1657 drop(log);
1658 }
1659
1660 #[test]
1665 fn test_path_matches_prefix_exact() {
1666 assert!(path_matches_prefix("dashboard", "dashboard"));
1667 }
1668
1669 #[test]
1670 fn test_path_matches_prefix_child() {
1671 assert!(path_matches_prefix("dashboard/settings", "dashboard"));
1672 }
1673
1674 #[test]
1675 fn test_path_matches_prefix_no_match() {
1676 assert!(!path_matches_prefix("other", "dashboard"));
1677 }
1678
1679 #[test]
1680 fn test_path_matches_prefix_with_param() {
1681 assert!(path_matches_prefix("users/123", "users/:id"));
1682 assert!(path_matches_prefix("users/123/posts", "users/:id"));
1683 }
1684
1685 #[test]
1686 fn test_path_matches_prefix_shorter_path() {
1687 assert!(!path_matches_prefix("users", "users/123"));
1688 }
1689}