first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
+260 -122
View File
@@ -9,71 +9,66 @@ use std::{
time::Duration,
};
use ai::diff_validation::DiffType;
use futures::stream::AbortHandle;
use galaxy_core::{features::FeatureFlag, ui::appearance::Appearance};
use galaxy_editor::{
content::{buffer::InitialBufferState, text::IndentUnit},
render::model::{Decoration, LineCount},
};
use galaxy_util::{
content_version::ContentVersion,
file::{FileId, FileLoadError, FileSaveError},
path::to_relative_path,
};
use galaxyui::{
elements::{
Border, ChildAnchor, ChildView, ClippedScrollStateHandle, ConstrainedBox, Container,
CornerRadius, CrossAxisAlignment, DropShadow, Flex, Hoverable, MainAxisAlignment,
MainAxisSize, MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement,
ParentOffsetBounds, Radius, Rect, Shrinkable, Stack, Text,
},
keymap::{macros::*, FixedBinding},
text::point::Point,
ui_components::{
button::ButtonVariant,
components::{Coords, UiComponent, UiComponentStyles},
},
AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle,
WindowId,
};
use galaxyui::{platform::SaveFilePickerConfiguration, ModelHandle};
use lsp::types::FileLocation;
use lsp::{
types::FileLocation, LanguageId, LanguageServerId, LspEvent, LspManagerModel,
LspManagerModelEvent, LspServerModel, ReferenceLocation,
LanguageId, LanguageServerId, LspEvent, LspManagerModel, LspManagerModelEvent, LspServerModel,
ReferenceLocation,
};
use lsp_types::FormattingOptions;
use markdown_parser::FormattedText;
use num_traits::SaturatingSub;
use pathfinder_geometry::{rect::RectF, vector::Vector2F};
use string_offset::CharOffset;
use vec1::Vec1;
use crate::menu::{Event, Menu, MenuItem, MenuItemFields};
use crate::{
code::{
editor::model::HoverableLink,
footer::{CodeFooterView, CodeFooterViewEvent},
global_buffer_model::{BufferState, GlobalBufferModel},
SaveOutcome, ShowFindReferencesCardProvider,
},
debounce::debounce,
settings::AISettings,
terminal::TerminalView,
util::sync::Condition,
};
use crate::{
code::{editor::EditorReviewComment, global_buffer_model::GlobalBufferModelEvent},
code_review::comments::CommentId,
};
use ai::diff_validation::DiffType;
use galaxy_core::ui::icons::Icon;
use pathfinder_color::ColorU;
use pathfinder_geometry::rect::RectF;
use pathfinder_geometry::vector::Vector2F;
use remote_server::manager::RemoteServerManager;
#[cfg(feature = "local_fs")]
use repo_metadata::repositories::DetectedRepositories;
use string_offset::CharOffset;
use vec1::Vec1;
use vim::vim::{MotionType, VimMode};
use galaxy_core::features::FeatureFlag;
use galaxy_core::r#async::debounce;
use galaxy_core::ui::appearance::Appearance;
use galaxy_core::ui::icons::Icon;
use galaxy_editor::content::buffer::InitialBufferState;
use galaxy_editor::content::text::IndentUnit;
use galaxy_editor::render::model::{Decoration, LineCount};
use galaxy_util::content_version::ContentVersion;
use galaxy_util::file::{FileId, FileLoadError, FileSaveError};
#[cfg(feature = "local_fs")]
use galaxy_util::local_or_remote_path::LocalOrRemotePath;
use galaxy_util::path::to_relative_path;
use galaxy_util::sync::Condition;
use galaxyui::elements::{
Border, ChildAnchor, ChildView, ClippedScrollStateHandle, ConstrainedBox, Container,
CornerRadius, CrossAxisAlignment, DropShadow, Flex, Hoverable, MainAxisAlignment, MainAxisSize,
MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds, Radius,
Rect, Shrinkable, Stack, Text,
};
use galaxyui::keymap::macros::*;
use galaxyui::keymap::FixedBinding;
use galaxyui::platform::SaveFilePickerConfiguration;
use galaxyui::text::point::Point;
use galaxyui::ui_components::button::ButtonVariant;
use galaxyui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
use galaxyui::{
AppContext, Element, Entity, ModelHandle, SingletonEntity, TypedActionView, View, ViewContext,
ViewHandle, WindowId,
};
use crate::ai::persisted_workspace::{PersistedWorkspace, PersistedWorkspaceEvent};
use crate::code::buffer_location::LocalOrRemotePath as BufferFileLocation;
use crate::code::editor::model::HoverableLink;
use crate::code::editor::EditorReviewComment;
use crate::code::footer::{CodeFooterView, CodeFooterViewEvent};
use crate::code::global_buffer_model::{BufferState, GlobalBufferModel, GlobalBufferModelEvent};
use crate::code::{SaveOutcome, ShowFindReferencesCardProvider};
use crate::code_review::comments::CommentId;
use crate::menu::{Event, Menu, MenuItem, MenuItemFields};
use crate::settings::{AISettings, CodeSettings};
use crate::terminal::TerminalView;
use crate::workspace::WorkspaceAction;
const DROP_SHADOW_COLOR: ColorU = ColorU {
@@ -85,20 +80,17 @@ const DROP_SHADOW_COLOR: ColorU = ColorU {
const HOVER_DEBOUNCE_PERIOD: Duration = Duration::from_millis(500);
use super::code_actions::{CodeActionsState, CODE_ACTIONS_DEBOUNCE_PERIOD};
use super::completion::{CompletionState, COMPLETION_DEBOUNCE_PERIOD};
use galaxy_core::send_telemetry_from_ctx;
use super::diff_viewer::DiffViewer;
use super::editor::{
scroll::{ScrollPosition, ScrollTrigger},
view::{CodeEditorEvent, CodeEditorView},
};
use super::editor::scroll::{ScrollPosition, ScrollTrigger};
use super::editor::view::{CodeEditorEvent, CodeEditorView};
use super::find_references_view::{FindReferencesView, FindReferencesViewEvent};
use super::language_server_extension::ProcessedDiagnostic;
use super::lsp_telemetry::LspTelemetryEvent;
use super::rename::RenameState;
use super::signature_help::SignatureHelpState;
use super::ImmediateSaveError;
use galaxy_core::send_telemetry_from_ctx;
type SaveCallback =
Box<dyn FnOnce(SaveOutcome, &mut ViewContext<LocalCodeEditorView>) + Send + Sync + 'static>;
@@ -189,9 +181,9 @@ pub enum LocalCodeEditorEvent {
/// Metadata about a file that is opened in the code view.
#[derive(Debug, Clone)]
enum LoadedFileMetadata {
/// Normal file with both FileId and path (for files that are actually opened)
LocalFile { id: FileId, path: PathBuf },
struct LoadedFileMetadata {
id: FileId,
location: BufferFileLocation,
}
pub use super::diff_viewer::DisplayMode;
@@ -310,6 +302,9 @@ pub struct LocalCodeEditorView {
was_edited: bool,
/// Content version of the base file state.
base_content_version: Option<ContentVersion>,
/// Set to `true` when a `RemoteBufferConflict` event fires for this
/// editor's buffer. Cleared when the user discards or overwrites.
has_remote_conflict: bool,
conflict_banner_mouse_states: ConflictResolutionBannerMouseStates,
/// Default directory to use for save dialogs when creating new files
default_directory: Option<PathBuf>,
@@ -565,6 +560,7 @@ impl LocalCodeEditorView {
selection_as_context_tooltip: None,
was_edited: false,
base_content_version: None,
has_remote_conflict: false,
conflict_banner_mouse_states: Default::default(),
default_directory: None,
lsp_server: None,
@@ -1083,6 +1079,14 @@ impl LocalCodeEditorView {
}
fn format_and_save(&mut self, file_id: FileId, ctx: &mut ViewContext<Self>) {
// Respect the user's format-on-save setting. When disabled, save without
// requesting LSP document formatting; all other LSP features (hover,
// go-to-definition, references, diagnostics) are unaffected.
if !*CodeSettings::as_ref(ctx).format_on_save {
self.perform_save(file_id, ctx);
return;
}
let Some(lsp_server) = &self.lsp_server else {
self.perform_save(file_id, ctx);
return;
@@ -1256,9 +1260,13 @@ impl LocalCodeEditorView {
GlobalBufferModel::as_ref(ctx).buffer_loaded(file_id)
}
/// Construct a new local editor view with a shared buffer.
/// Construct a new editor view with a shared buffer backed by the given location.
///
/// For local files, sets the language from the file path and wires up LSP.
/// For remote files, sets the language from the extension and skips
/// local-only wiring (LSP, footer).
pub fn new_with_global_buffer<T>(
path: &Path,
location: BufferFileLocation,
editor_constructor: T,
enable_diff_nav_by_default: bool,
display_mode: Option<DisplayMode>,
@@ -1268,24 +1276,35 @@ impl LocalCodeEditorView {
T: FnOnce(BufferState, &mut ViewContext<Self>) -> ViewHandle<CodeEditorView>,
{
let buffer_state = GlobalBufferModel::handle(ctx)
.update(ctx, |model, ctx| model.open(path.to_path_buf(), ctx));
.update(ctx, |model, ctx| model.open(location.clone(), ctx));
let file_id = buffer_state.file_id;
let editor = editor_constructor(buffer_state, ctx);
editor.update(ctx, |editor, ctx| {
editor.set_language_with_path(path, ctx);
// Rebuild layout and bootstrap syntax highlighting for the editor with existing buffer content.
editor.model.update(ctx, |model, ctx| {
model.rebuild_layout_with_syntax_highlighting(ctx)
});
});
match &location {
BufferFileLocation::Local(path) => {
editor.update(ctx, |editor, ctx| {
editor.set_language_with_local_path(path, ctx);
editor.model.update(ctx, |model, ctx| {
model.rebuild_layout_with_syntax_highlighting(ctx)
});
});
}
BufferFileLocation::Remote(remote_path) => {
editor.update(ctx, |editor, ctx| {
editor.set_language_with_path(&remote_path.path, ctx);
editor.model.update(ctx, |model, ctx| {
model.rebuild_layout_with_syntax_highlighting(ctx)
});
});
}
}
let mut local_editor =
Self::new(editor, None, enable_diff_nav_by_default, display_mode, ctx);
local_editor.metadata = Some(LoadedFileMetadata::LocalFile {
local_editor.metadata = Some(LoadedFileMetadata {
id: file_id,
path: path.to_path_buf(),
location,
});
Self::subscribe_to_global_buffer_events(file_id, ctx);
@@ -1453,7 +1472,6 @@ impl LocalCodeEditorView {
/// 5. Starting the LSP server via PersistedWorkspace
#[cfg(feature = "local_fs")]
fn enable_lsp_for_path(path: &Path, ctx: &mut ViewContext<Self>) {
use crate::ai::persisted_workspace::LspTask;
// Get the language ID from the file path
let Some(language_id) = LanguageId::from_path(path) else {
@@ -1472,7 +1490,10 @@ impl LocalCodeEditorView {
{
Some(workspace_root.to_path_buf())
} else {
match DetectedRepositories::as_ref(ctx).get_root_for_path(path) {
match DetectedRepositories::as_ref(ctx)
.get_root_for_path(&LocalOrRemotePath::Local(path.to_path_buf()))
.and_then(|r| PathBuf::try_from(r).ok())
{
Some(root) => Some(root),
None => path.parent().map(|s| s.to_path_buf()), // If we can't find root, treat the parent as the root.
}
@@ -1495,7 +1516,6 @@ impl LocalCodeEditorView {
/// and emits events that are handled by handle_persisted_workspace_event.
#[cfg(feature = "local_fs")]
fn install_and_enable_lsp_for_path(path: &Path, ctx: &mut ViewContext<Self>) {
use crate::ai::persisted_workspace::LspTask;
let Some(language_id) = LanguageId::from_path(path) else {
log::warn!("Install and enable lsp for path should only work for supported file paths");
@@ -1510,7 +1530,10 @@ impl LocalCodeEditorView {
{
Some(workspace_root.to_path_buf())
} else {
match DetectedRepositories::as_ref(ctx).get_root_for_path(&path) {
match DetectedRepositories::as_ref(ctx)
.get_root_for_path(&LocalOrRemotePath::Local(path.to_path_buf()))
.and_then(|r| PathBuf::try_from(r).ok())
{
Some(root) => Some(root),
None => path.parent().map(|s| s.to_path_buf()),
}
@@ -1580,19 +1603,24 @@ impl LocalCodeEditorView {
if event.file_id() != file_id {
return;
}
me.update_diff_hunk_gutter_buttons(ctx);
match event {
GlobalBufferModelEvent::BufferLoaded {
content_version, ..
} => {
// For a reopen (discard), base_content_version is already
// set from the initial load. Accept the new version and
// clear any conflict flag.
me.has_remote_conflict = false;
if me.base_content_version.is_some() {
return;
me.base_content_version = Some(*content_version);
ctx.notify();
} else {
me.base_content_version = Some(*content_version);
me.subscribe_to_lsp_manager_updates(ctx);
me.try_connect_lsp_server(ctx);
me.on_file_loaded(ctx);
ctx.emit(LocalCodeEditorEvent::FileLoaded);
}
me.base_content_version = Some(*content_version);
me.subscribe_to_lsp_manager_updates(ctx);
me.try_connect_lsp_server(ctx);
me.on_file_loaded(ctx);
ctx.emit(LocalCodeEditorEvent::FileLoaded);
}
GlobalBufferModelEvent::FailedToLoad { error, .. } => {
me.is_new_file = true;
@@ -1612,7 +1640,11 @@ impl LocalCodeEditorView {
me.base_content_version = Some(*content_version);
}
}
GlobalBufferModelEvent::FileSaved { .. } => {
GlobalBufferModelEvent::FileSaved {
content_version, ..
} => {
me.base_content_version = Some(*content_version);
me.has_remote_conflict = false;
ctx.emit(LocalCodeEditorEvent::FileSaved);
}
GlobalBufferModelEvent::FailedToSave { error, .. } => {
@@ -1621,21 +1653,53 @@ impl LocalCodeEditorView {
error: error.clone(),
});
}
GlobalBufferModelEvent::RemoteBufferConflict { .. } => {
me.has_remote_conflict = true;
ctx.notify();
}
GlobalBufferModelEvent::ServerLocalBufferUpdated { .. } => {
// Not relevant for local code editors.
}
}
me.update_diff_hunk_gutter_buttons(ctx);
});
}
pub fn has_version_conflicts(&self, app: &AppContext) -> bool {
// Remote buffers use SyncClock for conflict detection.
// The flag is set by the RemoteBufferConflict event handler.
if matches!(self.file_location(), Some(BufferFileLocation::Remote(_))) {
return self.has_remote_conflict;
}
let Some(file_id) = self.file_id() else {
return false;
};
self.has_unsaved_changes(app)
&& self.base_content_version != GlobalBufferModel::as_ref(app).base_version(file_id)
}
/// Save the file to the local file system.
/// Returns `true` when this editor is backed by a remote file whose
/// host no longer has any connected session. Derived on-the-fly from
/// `RemoteServerManager` so it is always in sync with actual
/// connection state.
pub fn is_remote_disconnected(&self, app: &AppContext) -> bool {
let Some(BufferFileLocation::Remote(remote_path)) = self.file_location() else {
return false;
};
RemoteServerManager::as_ref(app)
.client_for_host(&remote_path.host_id)
.is_none()
}
/// Save the file to the local file system (or remotely via the remote server).
/// This will only return an error immediately if there is a failure in the sync part of the call.
/// Other errors could be returned asynchronously via the FileModelEvent::FailedToSave event.
pub fn save_local(&mut self, ctx: &mut ViewContext<Self>) -> Result<(), ImmediateSaveError> {
if self.is_remote_disconnected(ctx) {
return Err(ImmediateSaveError::RemoteDisconnected);
}
let Some(file_id) = self.file_id() else {
return Err(ImmediateSaveError::NoFileId);
};
@@ -1682,15 +1746,15 @@ impl LocalCodeEditorView {
.update(ctx, |model, ctx| model.register(path.clone(), buffer, ctx));
let file_id = buffer_state.file_id;
me.metadata = Some(LoadedFileMetadata::LocalFile {
me.metadata = Some(LoadedFileMetadata {
id: file_id,
path: path.clone(),
location: BufferFileLocation::Local(path.clone()),
});
me.set_new_file(false);
me.editor.update(ctx, |editor, ctx| {
editor.set_language_with_path(&path, ctx);
editor.set_language_with_local_path(&path, ctx);
});
let content = me.editor.as_ref(ctx).text(ctx).into_string();
@@ -1749,15 +1813,18 @@ impl LocalCodeEditorView {
}
pub fn file_id(&self) -> Option<FileId> {
self.metadata.as_ref().map(|metadata| match metadata {
LoadedFileMetadata::LocalFile { id, .. } => *id,
})
self.metadata.as_ref().map(|m| m.id)
}
/// Returns the unified file location (local or remote).
pub fn file_location(&self) -> Option<&BufferFileLocation> {
self.metadata.as_ref().map(|m| &m.location)
}
/// Returns the local path if this editor is backed by a local file.
/// Returns `None` for remote files. Used by LSP and other local-only code paths.
pub fn file_path(&self) -> Option<&Path> {
self.metadata.as_ref().map(|metadata| match metadata {
LoadedFileMetadata::LocalFile { path, .. } => path.as_path(),
})
self.file_location().and_then(|loc| loc.to_local_path())
}
/// Update this editor's file identity after a `GlobalBufferModel::rename`.
@@ -1771,13 +1838,13 @@ impl LocalCodeEditorView {
ctx: &mut ViewContext<Self>,
) {
let file_id = buffer_state.file_id;
self.metadata = Some(LoadedFileMetadata::LocalFile {
self.metadata = Some(LoadedFileMetadata {
id: file_id,
path: new_path.to_path_buf(),
location: BufferFileLocation::Local(new_path.to_path_buf()),
});
self.editor.update(ctx, |editor, ctx| {
editor.set_language_with_path(new_path, ctx);
editor.set_language_with_local_path(new_path, ctx);
});
// Re-subscribe to GlobalBufferModel events for the new file_id.
@@ -2184,30 +2251,45 @@ impl View for LocalCodeEditorView {
}
fn render(&self, app: &AppContext) -> Box<dyn galaxyui::Element> {
// Rendering the version conflict banner.
let base: Box<dyn Element> = if self.has_version_conflicts(app) {
let appearance = Appearance::as_ref(app);
let banner = render_unsaved_changes_banner(
appearance,
self.conflict_banner_mouse_states
.discard_mouse_state
.clone(),
self.conflict_banner_mouse_states
.overwrite_mouse_state
.clone(),
);
let mut col = Flex::column().with_child(banner);
// Rendering the remote disconnection banner or version conflict banner.
// Only show the disconnection banner if the file was successfully loaded;
// if it never loaded, the error/loading state handles that.
let base: Box<dyn Element> =
if self.base_content_version.is_some() && self.is_remote_disconnected(app) {
let appearance = Appearance::as_ref(app);
let banner = render_remote_disconnected_banner(appearance);
let mut col = Flex::column().with_child(banner);
let editor_view = ChildView::new(&self.editor).finish();
if self.editor.as_ref(app).needs_vertical_constraint() {
col.add_child(Shrinkable::new(1., editor_view).finish());
let editor_view = ChildView::new(&self.editor).finish();
if self.editor.as_ref(app).needs_vertical_constraint() {
col.add_child(Shrinkable::new(1., editor_view).finish());
} else {
col.add_child(editor_view);
}
col.finish()
} else if self.has_version_conflicts(app) {
let appearance = Appearance::as_ref(app);
let banner = render_unsaved_changes_banner(
appearance,
self.conflict_banner_mouse_states
.discard_mouse_state
.clone(),
self.conflict_banner_mouse_states
.overwrite_mouse_state
.clone(),
);
let mut col = Flex::column().with_child(banner);
let editor_view = ChildView::new(&self.editor).finish();
if self.editor.as_ref(app).needs_vertical_constraint() {
col.add_child(Shrinkable::new(1., editor_view).finish());
} else {
col.add_child(editor_view);
}
col.finish()
} else {
col.add_child(editor_view);
}
col.finish()
} else {
ChildView::new(&self.editor).finish()
};
ChildView::new(&self.editor).finish()
};
let base_with_handler =
Hoverable::new(self.context_menu_state.mouse_state.clone(), |_| base)
@@ -2344,6 +2426,17 @@ impl TypedActionView for LocalCodeEditorView {
if let Some(path) = self.file_path().map(Path::to_path_buf) {
self.base_content_version = Some(self.editor().as_ref(ctx).version(ctx));
ctx.emit(LocalCodeEditorEvent::DiscardUnsavedChanges { path });
} else if self.has_remote_conflict {
// Remote file: re-open the buffer from the server to get
// the latest on-disk content. The BufferLoaded event will
// clear has_remote_conflict and update base_content_version.
// If the re-open fails, has_remote_conflict stays true and
// the banner remains visible so the user can retry.
if let Some(file_id) = self.file_id() {
GlobalBufferModel::handle(ctx).update(ctx, |model, ctx| {
model.reopen_remote_buffer(file_id, ctx);
});
}
}
}
LocalCodeEditorAction::NavigateToTarget(location) => {
@@ -2506,6 +2599,51 @@ pub fn render_unsaved_changes_banner(
.finish()
}
/// Renders a banner indicating that the remote SSH session is disconnected
/// and save / auto-reload are unavailable.
pub fn render_remote_disconnected_banner(appearance: &Appearance) -> Box<dyn Element> {
let row = Flex::row()
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_main_axis_size(MainAxisSize::Max)
.with_child(
Container::new(
ConstrainedBox::new(
Icon::Warning
.to_warpui_icon(appearance.theme().active_ui_text_color())
.finish(),
)
.with_height(16.)
.with_width(16.)
.finish(),
)
.with_margin_right(8.)
.finish(),
)
.with_child(
Shrinkable::new(
1.,
Text::new(
"Remote host disconnected. You will not be able to see updates and save changes.",
appearance.ui_font_family(),
appearance.ui_font_size(),
)
.with_color(appearance.theme().active_ui_text_color().into())
.soft_wrap(true)
.finish(),
)
.finish(),
)
.finish();
Container::new(row)
.with_background(appearance.theme().text_selection_as_context_color())
.with_padding_top(8.)
.with_padding_bottom(8.)
.with_padding_left(12.)
.with_padding_right(12.)
.finish()
}
/// Renders a small yellow circle with tooltip indicating unsaved changes
pub fn render_unsaved_circle_with_tooltip(
mouse_state: MouseStateHandle,