@@ -23,6 +23,9 @@ use crate::markdown::code_execution::{
2323use crate :: markdown:: parser:: {
2424 CalloutType , HeadingLevel , ListType , MarkdownNode , MarkdownNodeType , TableAlignment ,
2525} ;
26+ use crate :: path_utils:: {
27+ open_in_file_manager, resolve_openable_link_target, OpenableLinkTarget ,
28+ } ;
2629use crate :: terminal:: TerminalTheme ;
2730use crate :: ui:: phosphor_icons:: {
2831 phosphor_rich_text, ARROWS_CLOCKWISE , ARROWS_LEFT_RIGHT , BUILDINGS , CALENDAR , CARET_DOWN ,
@@ -4596,6 +4599,10 @@ pub struct RenderedLinkWidget<'a> {
45964599 colors : Option < WidgetColors > ,
45974600 /// Unique ID for this link
45984601 id : Option < egui:: Id > ,
4602+ /// Current file directory for resolving relative local links.
4603+ current_dir : Option < std:: path:: PathBuf > ,
4604+ /// Workspace root for resolving relative local links.
4605+ workspace_root : Option < std:: path:: PathBuf > ,
45994606}
46004607
46014608impl < ' a > RenderedLinkWidget < ' a > {
@@ -4607,6 +4614,8 @@ impl<'a> RenderedLinkWidget<'a> {
46074614 font_size : 14.0 ,
46084615 colors : None ,
46094616 id : None ,
4617+ current_dir : None ,
4618+ workspace_root : None ,
46104619 }
46114620 }
46124621
@@ -4631,11 +4640,25 @@ impl<'a> RenderedLinkWidget<'a> {
46314640 self
46324641 }
46334642
4643+ /// Provide path resolution context for local markdown links.
4644+ #[ must_use]
4645+ pub fn resolution_context (
4646+ mut self ,
4647+ current_dir : Option < std:: path:: PathBuf > ,
4648+ workspace_root : Option < std:: path:: PathBuf > ,
4649+ ) -> Self {
4650+ self . current_dir = current_dir;
4651+ self . workspace_root = workspace_root;
4652+ self
4653+ }
4654+
46344655 /// Show the link widget and return the output.
46354656 pub fn show ( self , ui : & mut Ui ) -> RenderedLinkOutput {
46364657 let colors = self
46374658 . colors
46384659 . unwrap_or_else ( || WidgetColors :: resolved ( ui, Theme :: System ) ) ;
4660+ let current_dir = self . current_dir . as_deref ( ) ;
4661+ let workspace_root = self . workspace_root . as_deref ( ) ;
46394662
46404663 let link_id = self . id . expect ( "RenderedLinkWidget requires an explicit ID" ) ;
46414664
@@ -4683,34 +4706,31 @@ impl<'a> RenderedLinkWidget<'a> {
46834706
46844707 // Track whether we consumed a click (to prevent parent from entering edit mode)
46854708 let mut click_consumed = false ;
4709+ let open_target =
4710+ resolve_openable_link_target ( & self . state . edit_url , current_dir, workspace_root) ;
46864711
46874712 // Handle click interactions
4688- // Check for middle-click first (always opens in browser )
4713+ // Check for middle-click first (always opens the target directly )
46894714 if link_response. middle_clicked ( ) {
46904715 click_consumed = true ;
4691- let can_open = self . state . edit_url . starts_with ( "http://" )
4692- || self . state . edit_url . starts_with ( "https://" ) ;
4693- if can_open {
4694- if let Err ( e) = open:: that ( & self . state . edit_url ) {
4695- log:: error!( "Failed to open URL: {}" , e) ;
4716+ if let Some ( target) = open_target. as_ref ( ) {
4717+ if let Err ( e) = open_link_target ( target) {
4718+ log:: error!( "Failed to open link target: {}" , e) ;
46964719 } else {
4697- log:: debug!( "Opened URL via middle-click: {}" , self . state . edit_url ) ;
4720+ log:: debug!( "Opened link target via middle-click: {:? }" , target ) ;
46984721 }
46994722 }
47004723 } else if clicked_on_link {
47014724 click_consumed = true ;
47024725 // Check if Ctrl/Cmd was held during the click
4703- let open_in_browser = modifiers. ctrl || modifiers. command ;
4704-
4705- if open_in_browser {
4706- // Ctrl+Click / Cmd+Click: Open URL in default browser
4707- let can_open = self . state . edit_url . starts_with ( "http://" )
4708- || self . state . edit_url . starts_with ( "https://" ) ;
4709- if can_open {
4710- if let Err ( e) = open:: that ( & self . state . edit_url ) {
4711- log:: error!( "Failed to open URL: {}" , e) ;
4726+ let open_directly = modifiers. ctrl || modifiers. command ;
4727+
4728+ if open_directly {
4729+ if let Some ( target) = open_target. as_ref ( ) {
4730+ if let Err ( e) = open_link_target ( target) {
4731+ log:: error!( "Failed to open link target: {}" , e) ;
47124732 } else {
4713- log:: debug!( "Opened URL via Ctrl+Click : {}" , self . state . edit_url ) ;
4733+ log:: debug!( "Opened link target via modifier-click : {:? }" , target ) ;
47144734 }
47154735 }
47164736 } else {
@@ -4721,15 +4741,16 @@ impl<'a> RenderedLinkWidget<'a> {
47214741
47224742 // Show tooltip with URL and interaction hint when hovering (if popup not open)
47234743 if link_response. hovered ( ) && !self . state . popup_open {
4724- let can_open = self . state . edit_url . starts_with ( "http://" )
4725- || self . state . edit_url . starts_with ( "https://" ) ;
4726- let tooltip = if can_open {
4727- format ! (
4744+ let tooltip = match open_target. as_ref ( ) {
4745+ Some ( OpenableLinkTarget :: WebUrl ( _) ) => format ! (
47284746 "{}\n \n Click to edit • Ctrl+Click to open in browser" ,
47294747 self . state. edit_url
4730- )
4731- } else {
4732- format ! ( "{}\n \n Click to edit" , self . state. edit_url)
4748+ ) ,
4749+ Some ( OpenableLinkTarget :: LocalPath ( _) ) => format ! (
4750+ "{}\n \n Click to edit • Ctrl+Click to reveal in file manager" ,
4751+ self . state. edit_url
4752+ ) ,
4753+ None => format ! ( "{}\n \n Click to edit" , self . state. edit_url) ,
47334754 } ;
47344755 link_response. on_hover_text ( tooltip) ;
47354756 }
@@ -4818,31 +4839,37 @@ impl<'a> RenderedLinkWidget<'a> {
48184839
48194840 // Action buttons
48204841 ui. horizontal ( |ui| {
4821- // Open Link button
4822- let can_open = self . state . edit_url . starts_with ( "http://" )
4823- || self . state . edit_url . starts_with ( "https://" ) ;
4842+ let popup_open_target = resolve_openable_link_target (
4843+ & self . state . edit_url ,
4844+ current_dir,
4845+ workspace_root,
4846+ ) ;
48244847
48254848 let open_button = ui. add_enabled (
4826- can_open ,
4849+ popup_open_target . is_some ( ) ,
48274850 egui:: Button :: new ( t ! ( "widgets.link.open" ) . to_string ( ) ) ,
48284851 ) ;
48294852
48304853 // Store clicked state before consuming response
48314854 let open_clicked = open_button. clicked ( ) ;
48324855
48334856 // Show appropriate hover text
4834- let hover_text = if can_open {
4835- "Open URL in browser"
4836- } else {
4837- "Only http/https URLs can be opened"
4857+ let hover_text = match popup_open_target. as_ref ( ) {
4858+ Some ( OpenableLinkTarget :: WebUrl ( _) ) => "Open URL in browser" ,
4859+ Some ( OpenableLinkTarget :: LocalPath ( _) ) => {
4860+ "Reveal path in file manager"
4861+ }
4862+ None => "Only resolvable web or local paths can be opened" ,
48384863 } ;
48394864 open_button. on_hover_text ( hover_text) ;
48404865
4841- if open_clicked && can_open {
4842- if let Err ( e) = open:: that ( & self . state . edit_url ) {
4843- log:: error!( "Failed to open URL: {}" , e) ;
4844- } else {
4845- log:: debug!( "Opened URL: {}" , self . state. edit_url) ;
4866+ if open_clicked {
4867+ if let Some ( target) = popup_open_target. as_ref ( ) {
4868+ if let Err ( e) = open_link_target ( target) {
4869+ log:: error!( "Failed to open link target: {}" , e) ;
4870+ } else {
4871+ log:: debug!( "Opened link target from popup: {:?}" , target) ;
4872+ }
48464873 }
48474874 }
48484875
@@ -4896,6 +4923,13 @@ impl<'a> RenderedLinkWidget<'a> {
48964923 }
48974924}
48984925
4926+ fn open_link_target ( target : & OpenableLinkTarget ) -> std:: io:: Result < ( ) > {
4927+ match target {
4928+ OpenableLinkTarget :: WebUrl ( url) => open:: that ( url) ,
4929+ OpenableLinkTarget :: LocalPath ( path) => open_in_file_manager ( path) ,
4930+ }
4931+ }
4932+
48994933// ─────────────────────────────────────────────────────────────────────────────
49004934// Mermaid Diagram Widget
49014935// ─────────────────────────────────────────────────────────────────────────────
0 commit comments