Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion app/src/app_state_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ fn test_code_pane_snapshot_single_tab() {
}],
active_tab_index: 0,
source: Some(CodeSource::FileTree {
path: PathBuf::from("/tmp/test.rs"),
location: crate::code::buffer_location::FileLocation::Local(PathBuf::from(
"/tmp/test.rs",
)),
}),
};
let CodePaneSnapShot::Local {
Expand Down
46 changes: 42 additions & 4 deletions app/src/code/buffer_location.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,55 @@
use std::path::PathBuf;
use std::path::{Path, PathBuf};

use serde::{Deserialize, Serialize};
use warp_util::content_version::ContentVersion;
use warp_util::remote_path::RemotePath;

/// Uniquely identifies where a buffer's content lives.
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub enum BufferLocation {
/// Uniquely identifies where a file lives — either on the local filesystem
/// or on a remote host. Used across both the buffer model and the
/// editor/view layers as the canonical file-identity type.
#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub enum FileLocation {
/// File on the local filesystem.
Local(PathBuf),
/// File on a remote host, identified by host + path.
Remote(RemotePath),
}

impl FileLocation {
/// Returns the file name component for display (e.g. tab titles).
pub fn display_name(&self) -> &str {
match self {
FileLocation::Local(path) => path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or_default(),
FileLocation::Remote(remote) => remote.path.file_name().unwrap_or_default(),
}
}

/// Returns the local path if this is a `Local` location, `None` for `Remote`.
/// Callers that only work with local files (LSP, save-to-disk, reveal-in-finder)
/// should use this to gate their behavior.
pub fn to_local_path(&self) -> Option<&Path> {
match self {
FileLocation::Local(path) => Some(path.as_path()),
FileLocation::Remote(_) => None,
}
}
}

impl From<PathBuf> for FileLocation {
fn from(path: PathBuf) -> Self {
FileLocation::Local(path)
}
}

impl From<RemotePath> for FileLocation {
fn from(remote: RemotePath) -> Self {
FileLocation::Remote(remote)
}
}

/// Tracks sync state between client and server for a single remote buffer.
///
/// Uses a version vector with two components:
Expand Down
45 changes: 41 additions & 4 deletions app/src/code/editor_management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::{
path::{Path, PathBuf},
};

use super::buffer_location::FileLocation;
use crate::ai::skills::SkillOpenOrigin;
use ai::skills::SkillReference;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -118,8 +119,8 @@ pub enum CodeSource {
AIAction { id: AIAgentActionId },
/// Opened from project rules (WARP.md) file.
ProjectRules { path: PathBuf },
/// Opened from file tree.
FileTree { path: PathBuf },
/// Opened from file tree (local or remote).
FileTree { location: FileLocation },
/// Opened from macOS Finder via "Open With".
Finder { path: PathBuf },
/// Opened from a skill.
Expand Down Expand Up @@ -148,14 +149,41 @@ impl CodeSource {
pub fn path(&self) -> Option<PathBuf> {
match self {
Self::New { .. } | Self::AIAction { .. } => None,
Self::FileTree { location, .. } => match location {
FileLocation::Local(path) => Some(path.clone()),
FileLocation::Remote(_) => None,
},
Self::Link { path, .. }
| Self::ProjectRules { path }
| Self::FileTree { path }
| Self::Finder { path }
| Self::Skill { path, .. } => Some(path.clone()),
}
}

/// Returns the `FileLocation` for file tree sources.
pub fn file_location(&self) -> Option<&FileLocation> {
match self {
Self::FileTree { location } => Some(location),
_ => None,
}
}

/// Returns the `FileLocation` for any source that has a backing file.
///
/// Unlike `path()` (which only returns local paths) and `file_location()`
/// (which only covers `FileTree`), this covers every variant that maps to
/// a file — local or remote.
pub fn location(&self) -> Option<FileLocation> {
match self {
Self::New { .. } | Self::AIAction { .. } => None,
Self::FileTree { location } => Some(location.clone()),
Self::Link { path, .. }
| Self::ProjectRules { path }
| Self::Finder { path }
| Self::Skill { path, .. } => Some(FileLocation::Local(path.clone())),
}
}

/// Returns true if this is a bundled skill that should be read-only.
pub fn is_bundled_skill(&self) -> bool {
matches!(
Expand Down Expand Up @@ -186,6 +214,9 @@ impl CodeSource {
Self::Link { .. } => "link",
Self::AIAction { .. } => "ai_action",
Self::ProjectRules { .. } => "project_rules",
Self::FileTree {
location: FileLocation::Remote(_),
} => "remote_file_tree",
Self::FileTree { .. } => "file_tree",
Self::Finder { .. } => "finder",
Self::Skill { .. } => "skill",
Expand All @@ -197,7 +228,13 @@ impl CodeSource {
/// `AIAction` is ephemeral (tied to a live conversation) and should not
/// be restored.
pub fn is_restorable(&self) -> bool {
!matches!(self, Self::AIAction { .. })
!matches!(
self,
Self::AIAction { .. }
| Self::FileTree {
location: FileLocation::Remote(_),
}
)
}
}

Expand Down
54 changes: 20 additions & 34 deletions app/src/code/file_tree/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ use warpui::{
use warpui::{BlurContext, ModelHandle};

use crate::code::active_file::{ActiveFileEvent, ActiveFileModel};
use crate::code::buffer_location::FileLocation;
use crate::coding_panel_enablement_state::CodingPanelEnablementState;
use crate::editor::{EditorOptions, EditorView, TextOptions};
#[cfg(feature = "local_fs")]
Expand All @@ -67,7 +68,6 @@ use crate::{
use warp_core::features::FeatureFlag;
use warp_core::ui::theme::{color::internal_colors, Fill};
use warp_core::HostId;
use warpui::ui_components::components::UiComponent;

mod editing;
mod render;
Expand Down Expand Up @@ -1978,7 +1978,6 @@ impl FileTreeView {
let is_selected = self.selected_item.as_ref() == Some(id);
let is_expanded = self.is_item_expanded(&id.root, item);
let render_state = item.to_render_state(is_expanded, appearance);
let is_remote_file = root_dir.is_remote() && matches!(item, FileTreeItem::File { .. });

let item_display_name = render_state.display_name.clone();
let item_position_id = format!("file_tree_item:{item_display_name}");
Expand All @@ -1997,34 +1996,14 @@ impl FileTreeView {
let id_for_context = id.clone();
let id_for_drop = id.clone();
let id_for_drag = id.clone();
let ui_builder = appearance.ui_builder();
let hoverable = Hoverable::new(render_state.mouse_state.clone(), move |mouse_state| {
let item_highlight_state = ItemHighlightState::new(is_selected, mouse_state);
let element = Self::render_item_with_hover(
Self::render_item_with_hover(
render_state,
appearance,
item_highlight_state,
editor_view,
);

if is_remote_file && mouse_state.is_hovered() {
let tooltip = ui_builder
.tool_tip("Opening files is unavailable for remote sessions".to_string())
.build()
.finish();
let offset = OffsetPositioning::offset_from_parent(
Vector2F::new(0., 4.),
ParentOffsetBounds::WindowByPosition,
ParentAnchor::BottomLeft,
ChildAnchor::TopLeft,
);
Stack::new()
.with_child(element)
.with_positioned_overlay_child(tooltip, offset)
.finish()
} else {
element
}
)
})
.on_click(
move |event_ctx: &mut EventContext, _app_ctx: &AppContext, _position| {
Expand All @@ -2047,12 +2026,7 @@ impl FileTreeView {
});
},
)
// Remote files can't be opened in the editor, so use the default cursor.
.with_cursor(if is_remote_file {
Cursor::Arrow
} else {
Cursor::PointingHand
})
.with_cursor(Cursor::PointingHand)
.finish();

let draggable = Draggable::new(draggable_state, hoverable)
Expand Down Expand Up @@ -2251,7 +2225,7 @@ impl FileTreeView {
);

ctx.emit(FileTreeEvent::OpenFile {
path: path.to_path_buf(),
path: FileLocation::Local(path.to_path_buf()),
target,
line_col: None,
});
Expand All @@ -2273,8 +2247,20 @@ impl FileTreeView {

match item {
FileTreeItem::File { metadata, .. } => {
// Remote file trees don't support opening files in the editor.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very niceeeee

if !is_remote {
if is_remote {
// Emit a remote open event if we have a host ID.
if let Some(host_id) = &root_dir.remote_host_id {
let remote_path = warp_util::remote_path::RemotePath::new(
host_id.clone(),
(*metadata.path).clone(),
);
ctx.emit(FileTreeEvent::OpenFile {
path: FileLocation::Remote(remote_path),
target: FileTarget::CodeEditor(EditorLayout::SplitPane),
line_col: None,
});
}
} else {
let path = metadata.path.to_local_path_lossy();
self.open_file(&path, None, ctx);
}
Expand Down Expand Up @@ -2898,7 +2884,7 @@ pub enum FileTreeEvent {
AttachAsContext { path: PathBuf },
#[cfg_attr(not(feature = "local_fs"), allow(dead_code))]
OpenFile {
path: PathBuf,
path: FileLocation,
target: FileTarget,
line_col: Option<LineAndColumnArg>,
},
Expand Down
Loading
Loading