use crate::app::file_open::SortMode;
use crate::model::event::{BufferId, ContainerId, LeafId, SplitDirection};
use crate::services::async_bridge::LspMessageType;
use ratatui::layout::Rect;
use rust_i18n::t;
use std::collections::{HashMap, HashSet};
use std::ops::Range;
use std::path::{Path, PathBuf};
pub const DEFAULT_BACKGROUND_FILE: &str = "scripts/landscape-wide.txt";
pub const FILE_EXPLORER_CONTEXT_MENU_WIDTH: u16 = 24;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct BufferGroupId(pub usize);
#[derive(Debug, Clone)]
pub enum GroupLayoutNode {
Scrollable {
id: String,
buffer_id: Option<BufferId>,
split_id: Option<LeafId>,
},
Fixed {
id: String,
height: u16,
buffer_id: Option<BufferId>,
split_id: Option<LeafId>,
},
Split {
direction: SplitDirection,
ratio: f32,
first: Box<GroupLayoutNode>,
second: Box<GroupLayoutNode>,
},
}
#[derive(Debug)]
pub struct BufferGroup {
pub id: BufferGroupId,
pub name: String,
pub mode: String,
pub layout: GroupLayoutNode,
pub panel_buffers: HashMap<String, BufferId>,
pub panel_splits: HashMap<String, LeafId>,
pub representative_split: Option<LeafId>,
}
#[derive(Debug, Clone, Default)]
pub(super) struct EventLineInfo {
pub start_line: usize,
pub end_line: usize,
pub line_delta: i32,
}
#[derive(Debug, Clone)]
pub(crate) struct SearchState {
pub query: String,
pub matches: Vec<usize>,
pub match_lengths: Vec<usize>,
pub current_match_index: Option<usize>,
pub wrap_search: bool,
pub search_range: Option<Range<usize>>,
#[allow(dead_code)]
pub capped: bool,
}
impl SearchState {
pub const MAX_MATCHES: usize = 100_000;
}
#[derive(Debug, Clone)]
pub(crate) struct InteractiveReplaceState {
pub search: String,
pub replacement: String,
pub current_match_pos: usize,
pub current_match_len: usize,
pub start_pos: usize,
pub has_wrapped: bool,
pub replacements_made: usize,
pub regex: Option<regex::bytes::Regex>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum BufferKind {
File {
path: PathBuf,
uri: Option<LspUri>,
},
Virtual {
mode: String,
},
}
#[derive(Debug, Clone)]
pub struct BufferMetadata {
pub kind: BufferKind,
pub display_name: String,
pub lsp_enabled: bool,
pub lsp_disabled_reason: Option<String>,
pub read_only: bool,
pub binary: bool,
pub lsp_opened_with: HashSet<u64>,
pub hidden_from_tabs: bool,
pub auto_revert_enabled: bool,
pub synthetic_placeholder: bool,
pub is_preview: bool,
pub recovery_id: Option<String>,
}
impl BufferMetadata {
pub fn file_path(&self) -> Option<&PathBuf> {
match &self.kind {
BufferKind::File { path, .. } => Some(path),
BufferKind::Virtual { .. } => None,
}
}
pub fn file_uri(&self) -> Option<&LspUri> {
match &self.kind {
BufferKind::File { uri, .. } => uri.as_ref(),
BufferKind::Virtual { .. } => None,
}
}
pub fn is_virtual(&self) -> bool {
matches!(self.kind, BufferKind::Virtual { .. })
}
pub fn virtual_mode(&self) -> Option<&str> {
match &self.kind {
BufferKind::Virtual { mode } => Some(mode),
BufferKind::File { .. } => None,
}
}
}
impl Default for BufferMetadata {
fn default() -> Self {
Self::new()
}
}
impl BufferMetadata {
pub fn new() -> Self {
Self {
kind: BufferKind::File {
path: PathBuf::new(),
uri: None,
},
display_name: t!("buffer.no_name").to_string(),
lsp_enabled: true,
lsp_disabled_reason: None,
read_only: false,
binary: false,
lsp_opened_with: HashSet::new(),
hidden_from_tabs: false,
auto_revert_enabled: true,
synthetic_placeholder: false,
is_preview: false,
recovery_id: None,
}
}
pub fn new_unnamed(display_name: String) -> Self {
Self {
kind: BufferKind::File {
path: PathBuf::new(),
uri: None,
},
display_name,
lsp_enabled: false, lsp_disabled_reason: Some(t!("lsp.disabled.unnamed").to_string()),
read_only: false,
binary: false,
lsp_opened_with: HashSet::new(),
auto_revert_enabled: true,
hidden_from_tabs: false,
synthetic_placeholder: false,
is_preview: false,
recovery_id: None,
}
}
pub fn with_file(
canonical_path: PathBuf,
display_path: &Path,
working_dir: &Path,
path_translation: Option<&crate::services::authority::PathTranslation>,
) -> Self {
let file_uri = LspUri::from_host_path(&canonical_path, path_translation);
let display_name = Self::display_name_for_path(&canonical_path, working_dir);
let is_library = Self::is_library_path(&canonical_path, working_dir)
&& Self::is_library_path(display_path, working_dir);
Self {
kind: BufferKind::File {
path: canonical_path,
uri: file_uri,
},
display_name,
lsp_enabled: true,
lsp_disabled_reason: None,
read_only: is_library,
binary: false,
auto_revert_enabled: true,
lsp_opened_with: HashSet::new(),
hidden_from_tabs: false,
synthetic_placeholder: false,
is_preview: false,
recovery_id: None,
}
}
pub fn with_container_file(container_path: PathBuf, uri: LspUri) -> Self {
let display_name = container_path
.file_name()
.and_then(|n| n.to_str())
.map(|n| n.to_string())
.unwrap_or_else(|| container_path.to_string_lossy().to_string());
Self {
kind: BufferKind::File {
path: container_path,
uri: Some(uri),
},
display_name,
lsp_enabled: true,
lsp_disabled_reason: None,
read_only: true,
auto_revert_enabled: true,
binary: false,
lsp_opened_with: HashSet::new(),
hidden_from_tabs: false,
synthetic_placeholder: false,
is_preview: false,
recovery_id: None,
}
}
pub fn is_library_path(path: &Path, _working_dir: &Path) -> bool {
let path_str = path.to_string_lossy();
if path_str.contains("/.cargo/registry/")
|| path_str.contains("\\.cargo\\registry\\")
|| path_str.contains("/.cargo/git/")
|| path_str.contains("\\.cargo\\git\\")
{
return true;
}
if path_str.contains("/rustup/toolchains/") || path_str.contains("\\rustup\\toolchains\\") {
return true;
}
if path_str.contains("/node_modules/") || path_str.contains("\\node_modules\\") {
return true;
}
if path_str.contains("/site-packages/")
|| path_str.contains("\\site-packages\\")
|| path_str.contains("/dist-packages/")
|| path_str.contains("\\dist-packages\\")
{
return true;
}
if path_str.contains("/pkg/mod/") || path_str.contains("\\pkg\\mod\\") {
return true;
}
if path_str.contains("/gems/") || path_str.contains("\\gems\\") {
return true;
}
if path_str.contains("/.gradle/") || path_str.contains("\\.gradle\\") {
return true;
}
if path_str.contains("/.m2/") || path_str.contains("\\.m2\\") {
return true;
}
if path_str.starts_with("/usr/include/") || path_str.starts_with("/usr/local/include/") {
return true;
}
if path_str.starts_with("/nix/store/") {
return true;
}
if path_str.starts_with("/opt/homebrew/Cellar/")
|| path_str.starts_with("/usr/local/Cellar/")
{
return true;
}
if path_str.contains("/.nuget/") || path_str.contains("\\.nuget\\") {
return true;
}
if path_str.contains("/Xcode.app/Contents/Developer/")
|| path_str.contains("/CommandLineTools/SDKs/")
{
return true;
}
false
}
pub fn display_name_for_path(path: &Path, working_dir: &Path) -> String {
let canonical_working_dir = working_dir
.canonicalize()
.unwrap_or_else(|_| working_dir.to_path_buf());
let absolute_path = if path.is_absolute() {
path.to_path_buf()
} else {
canonical_working_dir.join(path)
};
let canonical_path = absolute_path
.canonicalize()
.unwrap_or_else(|_| absolute_path.clone());
let relative = canonical_path
.strip_prefix(&canonical_working_dir)
.or_else(|_| path.strip_prefix(working_dir))
.ok()
.and_then(|rel| rel.to_str().map(|s| s.to_string()));
relative
.or_else(|| canonical_path.to_str().map(|s| s.to_string()))
.unwrap_or_else(|| t!("buffer.unknown").to_string())
}
pub fn virtual_buffer(name: String, mode: String, read_only: bool) -> Self {
Self {
kind: BufferKind::Virtual { mode },
display_name: name,
lsp_enabled: false, lsp_disabled_reason: Some(t!("lsp.disabled.virtual").to_string()),
auto_revert_enabled: true,
read_only,
binary: false,
lsp_opened_with: HashSet::new(),
hidden_from_tabs: false,
synthetic_placeholder: false,
is_preview: false,
recovery_id: None,
}
}
pub fn hidden_virtual_buffer(name: String, mode: String) -> Self {
Self {
kind: BufferKind::Virtual { mode },
display_name: name,
lsp_enabled: false,
auto_revert_enabled: true,
lsp_disabled_reason: Some(t!("lsp.disabled.virtual").to_string()),
read_only: true, binary: false,
lsp_opened_with: HashSet::new(),
hidden_from_tabs: true,
synthetic_placeholder: false,
is_preview: false,
recovery_id: None,
}
}
pub fn disable_lsp(&mut self, reason: String) {
self.lsp_enabled = false;
self.lsp_disabled_reason = Some(reason);
}
}
#[derive(Debug, Clone)]
pub(crate) struct LspProgressInfo {
pub language: String,
pub title: String,
pub message: Option<String>,
pub percentage: Option<u32>,
}
pub use fresh_core::api::LspMenuItem;
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub(crate) struct LspMessageEntry {
pub language: String,
pub message_type: LspMessageType,
pub message: String,
pub timestamp: std::time::Instant,
}
#[derive(Debug, Clone, PartialEq)]
pub enum HoverTarget {
SplitSeparator(ContainerId, SplitDirection),
ScrollbarThumb(LeafId),
ScrollbarTrack(LeafId, u16),
MenuBarItem(usize),
MenuDropdownItem(usize, usize),
SubmenuItem(usize, usize),
PopupListItem(usize, usize),
SuggestionItem(usize),
FileExplorerBorder,
FileBrowserNavShortcut(usize),
FileBrowserEntry(usize),
FileBrowserHeader(SortMode),
FileBrowserScrollbar,
FileBrowserShowHiddenCheckbox,
FileBrowserDetectEncodingCheckbox,
TabName(crate::view::split::TabTarget, LeafId),
TabCloseButton(crate::view::split::TabTarget, LeafId),
CloseSplitButton(LeafId),
MaximizeSplitButton(LeafId),
FileExplorerCloseButton,
FileExplorerStatusIndicator(std::path::PathBuf),
StatusBarLspIndicator,
StatusBarRemoteIndicator,
StatusBarWarningBadge,
StatusBarLineEndingIndicator,
StatusBarEncodingIndicator,
StatusBarLanguageIndicator,
SearchOptionCaseSensitive,
SearchOptionWholeWord,
SearchOptionRegex,
SearchOptionConfirmEach,
TabContextMenuItem(usize),
FileExplorerContextMenuItem(usize),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TabContextMenuItem {
Close,
CloseOthers,
CloseToRight,
CloseToLeft,
CloseAll,
CopyRelativePath,
CopyFullPath,
}
impl TabContextMenuItem {
pub fn all() -> &'static [Self] {
&[
Self::Close,
Self::CloseOthers,
Self::CloseToRight,
Self::CloseToLeft,
Self::CloseAll,
Self::CopyRelativePath,
Self::CopyFullPath,
]
}
pub fn label(&self) -> String {
match self {
Self::Close => t!("tab.close").to_string(),
Self::CloseOthers => t!("tab.close_others").to_string(),
Self::CloseToRight => t!("tab.close_to_right").to_string(),
Self::CloseToLeft => t!("tab.close_to_left").to_string(),
Self::CloseAll => t!("tab.close_all").to_string(),
Self::CopyRelativePath => t!("tab.copy_relative_path").to_string(),
Self::CopyFullPath => t!("tab.copy_full_path").to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct TabContextMenu {
pub buffer_id: BufferId,
pub split_id: LeafId,
pub position: (u16, u16),
pub highlighted: usize,
}
impl TabContextMenu {
pub fn new(buffer_id: BufferId, split_id: LeafId, x: u16, y: u16) -> Self {
Self {
buffer_id,
split_id,
position: (x, y),
highlighted: 0,
}
}
pub fn highlighted_item(&self) -> TabContextMenuItem {
TabContextMenuItem::all()[self.highlighted]
}
pub fn next_item(&mut self) {
let items = TabContextMenuItem::all();
self.highlighted = (self.highlighted + 1) % items.len();
}
pub fn prev_item(&mut self) {
let items = TabContextMenuItem::all();
self.highlighted = if self.highlighted == 0 {
items.len() - 1
} else {
self.highlighted - 1
};
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileExplorerContextMenuItem {
NewFile,
NewDirectory,
Rename,
Cut,
Copy,
Paste,
Duplicate,
Delete,
CopyFullPath,
CopyRelativePath,
}
impl FileExplorerContextMenuItem {
pub fn all() -> &'static [Self] {
&[
Self::NewFile,
Self::NewDirectory,
Self::Rename,
Self::Cut,
Self::Copy,
Self::Paste,
Self::Delete,
Self::Duplicate,
Self::CopyFullPath,
Self::CopyRelativePath,
]
}
pub fn multi_selection() -> &'static [Self] {
&[
Self::Cut,
Self::Copy,
Self::Paste,
Self::Delete,
Self::Duplicate,
Self::CopyFullPath,
Self::CopyRelativePath,
]
}
pub fn root_single_selection() -> &'static [Self] {
&[Self::NewFile, Self::NewDirectory, Self::Paste]
}
pub fn label(&self) -> String {
match self {
Self::NewFile => t!("explorer.context.new_file").to_string(),
Self::NewDirectory => t!("explorer.context.new_directory").to_string(),
Self::Rename => t!("explorer.context.rename").to_string(),
Self::Cut => t!("explorer.context.cut").to_string(),
Self::Copy => t!("explorer.context.copy").to_string(),
Self::Paste => t!("explorer.context.paste").to_string(),
Self::Duplicate => t!("explorer.context.duplicate").to_string(),
Self::Delete => t!("explorer.context.delete").to_string(),
Self::CopyFullPath => t!("explorer.context.copy_full_path").to_string(),
Self::CopyRelativePath => t!("explorer.context.copy_relative_path").to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct FileExplorerContextMenu {
pub position: (u16, u16),
pub highlighted: usize,
pub is_multi_selection: bool,
pub is_root_selected: bool,
}
impl FileExplorerContextMenu {
pub fn new(x: u16, y: u16, is_multi_selection: bool, is_root_selected: bool) -> Self {
Self {
position: (x, y),
highlighted: 0,
is_multi_selection,
is_root_selected,
}
}
pub fn items(&self) -> &'static [FileExplorerContextMenuItem] {
if self.is_multi_selection {
FileExplorerContextMenuItem::multi_selection()
} else if self.is_root_selected {
FileExplorerContextMenuItem::root_single_selection()
} else {
FileExplorerContextMenuItem::all()
}
}
pub fn height(&self) -> u16 {
self.items().len() as u16 + 2
}
pub fn clamped_position(&self, screen_width: u16, screen_height: u16) -> (u16, u16) {
let x = if self.position.0 + FILE_EXPLORER_CONTEXT_MENU_WIDTH > screen_width {
screen_width.saturating_sub(FILE_EXPLORER_CONTEXT_MENU_WIDTH)
} else {
self.position.0
};
let h = self.height();
let y = if self.position.1 + h > screen_height {
screen_height.saturating_sub(h)
} else {
self.position.1
};
(x, y)
}
pub fn next_item(&mut self) {
let len = self.items().len();
self.highlighted = (self.highlighted + 1) % len;
}
pub fn prev_item(&mut self) {
let len = self.items().len();
self.highlighted = if self.highlighted == 0 {
len - 1
} else {
self.highlighted - 1
};
}
}
#[derive(Debug, Clone, Default)]
pub struct CellThemeInfo {
pub fg_key: Option<&'static str>,
pub bg_key: Option<&'static str>,
pub region: &'static str,
pub syntax_category: Option<&'static str>,
}
#[derive(Debug, Clone)]
pub struct ThemeKeyInfo {
pub fg_key: Option<String>,
pub bg_key: Option<String>,
pub region: String,
pub fg_color: Option<ratatui::style::Color>,
pub bg_color: Option<ratatui::style::Color>,
pub syntax_category: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ThemeInfoPopup {
pub position: (u16, u16),
pub info: ThemeKeyInfo,
pub button_highlighted: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TabDropZone {
TabBar(LeafId, Option<usize>),
SplitLeft(LeafId),
SplitRight(LeafId),
SplitTop(LeafId),
SplitBottom(LeafId),
SplitCenter(LeafId),
}
impl TabDropZone {
pub fn split_id(&self) -> LeafId {
match self {
Self::TabBar(id, _)
| Self::SplitLeft(id)
| Self::SplitRight(id)
| Self::SplitTop(id)
| Self::SplitBottom(id)
| Self::SplitCenter(id) => *id,
}
}
}
#[derive(Debug, Clone)]
pub struct TabDragState {
pub buffer_id: BufferId,
pub source_split_id: LeafId,
pub start_position: (u16, u16),
pub current_position: (u16, u16),
pub drop_zone: Option<TabDropZone>,
}
impl TabDragState {
pub fn new(buffer_id: BufferId, source_split_id: LeafId, start_position: (u16, u16)) -> Self {
Self {
buffer_id,
source_split_id,
start_position,
current_position: start_position,
drop_zone: None,
}
}
pub fn is_dragging(&self) -> bool {
let dx = (self.current_position.0 as i32 - self.start_position.0 as i32).abs();
let dy = (self.current_position.1 as i32 - self.start_position.1 as i32).abs();
dx > 3 || dy > 3 }
}
#[derive(Debug, Clone, Default)]
pub(crate) struct MouseState {
pub dragging_scrollbar: Option<LeafId>,
pub dragging_horizontal_scrollbar: Option<LeafId>,
pub drag_start_hcol: Option<u16>,
pub drag_start_left_column: Option<usize>,
pub last_position: Option<(u16, u16)>,
pub lsp_hover_state: Option<(usize, std::time::Instant, u16, u16)>,
pub lsp_hover_request_sent: bool,
pub drag_start_row: Option<u16>,
pub drag_start_top_byte: Option<usize>,
pub drag_start_view_line_offset: Option<usize>,
pub dragging_separator: Option<(ContainerId, SplitDirection)>,
pub drag_start_position: Option<(u16, u16)>,
pub drag_start_ratio: Option<f32>,
pub dragging_file_explorer: bool,
pub drag_start_explorer_width: Option<crate::config::ExplorerWidth>,
pub hover_target: Option<HoverTarget>,
pub dragging_text_selection: bool,
pub drag_selection_split: Option<LeafId>,
pub drag_selection_anchor: Option<usize>,
pub drag_selection_by_words: bool,
pub drag_selection_word_end: Option<usize>,
pub dragging_tab: Option<TabDragState>,
pub dragging_popup_scrollbar: Option<usize>,
pub drag_start_popup_scroll: Option<usize>,
pub dragging_prompt_scrollbar: bool,
pub selecting_in_popup: Option<usize>,
pub drag_start_composite_scroll_row: Option<usize>,
}
#[derive(Debug, Clone, Default)]
pub struct ViewLineMapping {
pub char_source_bytes: Vec<Option<usize>>,
pub visual_to_char: Vec<usize>,
pub line_end_byte: usize,
pub is_plugin_virtual: bool,
}
impl ViewLineMapping {
#[inline]
pub fn source_byte_at_visual_col(&self, visual_col: usize) -> Option<usize> {
let char_idx = self.visual_to_char.get(visual_col).copied()?;
self.char_source_bytes.get(char_idx).copied().flatten()
}
pub fn nearest_source_byte(&self, goal_col: usize) -> Option<usize> {
let width = self.visual_to_char.len();
if width == 0 {
return None;
}
for delta in 1..width {
if goal_col + delta < width {
if let Some(byte) = self.source_byte_at_visual_col(goal_col + delta) {
return Some(byte);
}
}
if delta <= goal_col {
if let Some(byte) = self.source_byte_at_visual_col(goal_col - delta) {
return Some(byte);
}
}
}
None
}
#[inline]
pub fn contains_byte(&self, byte_pos: usize) -> bool {
if let Some(first_byte) = self.char_source_bytes.iter().find_map(|b| *b) {
byte_pos >= first_byte && byte_pos <= self.line_end_byte
} else {
byte_pos == self.line_end_byte
}
}
#[inline]
pub fn first_source_byte(&self) -> Option<usize> {
self.char_source_bytes.iter().find_map(|b| *b)
}
}
pub(crate) type PopupAreaLayout = (usize, Rect, Rect, usize, usize, Option<Rect>, usize);
#[derive(Debug, Clone, Default)]
pub(crate) struct ChromeLayout {
pub popup_areas: Vec<PopupAreaLayout>,
pub global_popup_areas: Vec<(usize, Rect, Rect, usize, usize)>,
pub suggestions_area: Option<(Rect, usize, usize, usize)>,
pub suggestions_outer_area: Option<Rect>,
pub suggestions_scrollbar_rect: Option<Rect>,
pub settings_layout: Option<crate::view::settings::SettingsLayout>,
pub status_bar_area: Option<(u16, u16, u16)>,
pub status_bar_lsp_area: Option<(u16, u16, u16)>,
pub status_bar_warning_area: Option<(u16, u16, u16)>,
pub status_bar_line_ending_area: Option<(u16, u16, u16)>,
pub status_bar_encoding_area: Option<(u16, u16, u16)>,
pub status_bar_language_area: Option<(u16, u16, u16)>,
pub status_bar_message_area: Option<(u16, u16, u16)>,
pub status_bar_remote_area: Option<(u16, u16, u16)>,
pub search_options_layout: Option<crate::view::ui::status_bar::SearchOptionsLayout>,
pub menu_layout: Option<crate::view::ui::menu::MenuLayout>,
pub last_frame_width: u16,
pub last_frame_height: u16,
pub cell_theme_map: Vec<CellThemeInfo>,
}
impl ChromeLayout {
pub fn reset_cell_theme_map(&mut self) {
let total = self.last_frame_width as usize * self.last_frame_height as usize;
self.cell_theme_map.clear();
self.cell_theme_map.resize(total, CellThemeInfo::default());
}
pub fn cell_theme_at(&self, col: u16, row: u16) -> Option<&CellThemeInfo> {
let idx = row as usize * self.last_frame_width as usize + col as usize;
self.cell_theme_map.get(idx)
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct WindowLayoutCache {
pub file_explorer_area: Option<Rect>,
pub editor_content_area: Option<Rect>,
pub split_areas: Vec<(LeafId, BufferId, Rect, Rect, usize, usize)>,
pub horizontal_scrollbar_areas: Vec<(LeafId, BufferId, Rect, usize, usize, usize)>,
pub separator_areas: Vec<(ContainerId, SplitDirection, u16, u16, u16)>,
pub tab_layouts: HashMap<LeafId, crate::view::ui::tabs::TabLayout>,
pub close_split_areas: Vec<(LeafId, u16, u16, u16)>,
pub maximize_split_areas: Vec<(LeafId, u16, u16, u16)>,
pub view_line_mappings: HashMap<LeafId, Vec<ViewLineMapping>>,
}
impl WindowLayoutCache {
pub fn find_visual_row(&self, split_id: LeafId, byte_pos: usize) -> Option<usize> {
let mappings = self.view_line_mappings.get(&split_id)?;
mappings.iter().position(|m| m.contains_byte(byte_pos))
}
pub fn byte_to_visual_column(&self, split_id: LeafId, byte_pos: usize) -> Option<usize> {
let mappings = self.view_line_mappings.get(&split_id)?;
let row_idx = self.find_visual_row(split_id, byte_pos)?;
let row = mappings.get(row_idx)?;
for (visual_col, &char_idx) in row.visual_to_char.iter().enumerate() {
if let Some(source_byte) = row.char_source_bytes.get(char_idx).and_then(|b| *b) {
if source_byte == byte_pos {
return Some(visual_col);
}
if source_byte > byte_pos {
return Some(visual_col.saturating_sub(1));
}
}
}
Some(row.visual_to_char.len())
}
pub fn move_visual_line(
&self,
split_id: LeafId,
current_pos: usize,
goal_visual_col: usize,
direction: i8, ) -> Option<(usize, usize)> {
let mappings = self.view_line_mappings.get(&split_id)?;
let current_row = self.find_visual_row(split_id, current_pos)?;
let mut target_row = current_row;
let navigable = |idx: usize| -> bool {
mappings
.get(idx)
.map(|m| m.char_source_bytes.iter().any(|b| b.is_some()))
.unwrap_or(false)
};
loop {
target_row = if direction < 0 {
target_row.checked_sub(1)?
} else {
let next = target_row + 1;
if next >= mappings.len() {
return None;
}
next
};
if navigable(target_row) {
break;
}
let mapping = mappings.get(target_row)?;
if mapping.is_plugin_virtual {
continue;
}
break;
}
let target_mapping = mappings.get(target_row)?;
let new_pos = if goal_visual_col >= target_mapping.visual_to_char.len() {
target_mapping.line_end_byte
} else {
target_mapping
.source_byte_at_visual_col(goal_visual_col)
.or_else(|| target_mapping.nearest_source_byte(goal_visual_col))
.unwrap_or(target_mapping.line_end_byte)
};
Some((new_pos, goal_visual_col))
}
pub fn visual_line_start(
&self,
split_id: LeafId,
byte_pos: usize,
allow_advance: bool,
) -> Option<usize> {
let mappings = self.view_line_mappings.get(&split_id)?;
let row_idx = self.find_visual_row(split_id, byte_pos)?;
let row = mappings.get(row_idx)?;
let row_start = row.first_source_byte()?;
if allow_advance && byte_pos == row_start && row_idx > 0 {
let prev_row = mappings.get(row_idx - 1)?;
prev_row.first_source_byte()
} else {
Some(row_start)
}
}
pub fn visual_line_end(
&self,
split_id: LeafId,
byte_pos: usize,
allow_advance: bool,
) -> Option<usize> {
let mappings = self.view_line_mappings.get(&split_id)?;
let row_idx = self.find_visual_row(split_id, byte_pos)?;
let row = mappings.get(row_idx)?;
if allow_advance && byte_pos == row.line_end_byte && row_idx + 1 < mappings.len() {
let next_row = mappings.get(row_idx + 1)?;
Some(next_row.line_end_byte)
} else {
Some(row.line_end_byte)
}
}
}
pub fn file_path_to_lsp_uri(path: &Path) -> Option<lsp_types::Uri> {
fresh_core::file_uri::path_to_lsp_uri(path)
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct LspUri(lsp_types::Uri);
impl LspUri {
pub fn from_host_path(
path: &Path,
translation: Option<&crate::services::authority::PathTranslation>,
) -> Option<Self> {
let mapped = translation
.and_then(|t| t.host_to_remote(path))
.unwrap_or_else(|| path.to_path_buf());
fresh_core::file_uri::path_to_lsp_uri(&mapped).map(Self)
}
pub fn from_wire(uri: lsp_types::Uri) -> Self {
Self(uri)
}
pub fn as_uri(&self) -> &lsp_types::Uri {
&self.0
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
pub fn to_host_path(
&self,
translation: Option<&crate::services::authority::PathTranslation>,
) -> Option<PathBuf> {
let raw = fresh_core::file_uri::lsp_uri_to_path(&self.0)?;
Some(
translation
.and_then(|t| t.remote_to_host(&raw))
.unwrap_or(raw),
)
}
}
pub fn file_path_to_lsp_uri_with_translation(
path: &Path,
translation: Option<&crate::services::authority::PathTranslation>,
) -> Option<lsp_types::Uri> {
LspUri::from_host_path(path, translation).map(|u| u.into_inner())
}
impl LspUri {
pub fn into_inner(self) -> lsp_types::Uri {
self.0
}
}
#[cfg(all(test, unix))]
mod lsp_uri_tests {
use super::*;
use crate::services::authority::PathTranslation;
fn translation() -> PathTranslation {
PathTranslation {
host_root: PathBuf::from("/tmp/.tmpA1B2"),
remote_root: PathBuf::from("/workspaces/proj"),
}
}
#[test]
fn from_host_path_under_workspace_translates_to_remote_uri() {
let host = PathBuf::from("/tmp/.tmpA1B2/src/util.py");
let lsp_uri = LspUri::from_host_path(&host, Some(&translation())).expect("absolute path");
assert_eq!(lsp_uri.as_str(), "file:///workspaces/proj/src/util.py");
}
#[test]
fn from_host_path_outside_workspace_passes_through() {
let host = PathBuf::from("/usr/include/stdio.h");
let lsp_uri = LspUri::from_host_path(&host, Some(&translation())).expect("absolute path");
assert_eq!(lsp_uri.as_str(), "file:///usr/include/stdio.h");
}
#[test]
fn to_host_path_under_remote_root_translates_back() {
let wire: lsp_types::Uri = "file:///workspaces/proj/src/util.py".parse().unwrap();
let host = LspUri::from_wire(wire)
.to_host_path(Some(&translation()))
.expect("file:// URI");
assert_eq!(host, PathBuf::from("/tmp/.tmpA1B2/src/util.py"));
}
#[test]
fn to_host_path_outside_remote_root_passes_through() {
let wire: lsp_types::Uri = "file:///usr/include/stdio.h".parse().unwrap();
let host = LspUri::from_wire(wire)
.to_host_path(Some(&translation()))
.expect("file:// URI");
assert_eq!(host, PathBuf::from("/usr/include/stdio.h"));
}
#[test]
fn round_trip_host_to_wire_to_host_under_workspace() {
let host = PathBuf::from("/tmp/.tmpA1B2/main.py");
let lsp_uri = LspUri::from_host_path(&host, Some(&translation())).unwrap();
let back = lsp_uri.to_host_path(Some(&translation())).unwrap();
assert_eq!(back, host);
}
#[test]
fn no_translation_is_identity() {
let host = PathBuf::from("/some/host/path/file.rs");
let lsp_uri = LspUri::from_host_path(&host, None).unwrap();
assert_eq!(lsp_uri.as_str(), "file:///some/host/path/file.rs");
let back = lsp_uri.to_host_path(None).unwrap();
assert_eq!(back, host);
}
}
#[derive(Debug)]
pub struct OverlayPreviewState {
pub buffer_id: BufferId,
pub view_state: crate::view::split::SplitViewState,
pub loaded_buffers: HashSet<BufferId>,
}
#[cfg(test)]
mod uri_encoding_tests {
use super::*;
fn abs_path(suffix: &str) -> PathBuf {
std::env::temp_dir().join(suffix)
}
#[test]
fn test_brackets_in_path() {
let path = abs_path("MY_PROJECTS [temp]/gogame/main.go");
let uri = file_path_to_lsp_uri(&path);
assert!(
uri.is_some(),
"URI should be computed for path with brackets"
);
let uri = uri.unwrap();
assert!(
uri.as_str().contains("%5Btemp%5D"),
"Brackets should be percent-encoded: {}",
uri.as_str()
);
}
#[test]
fn test_spaces_in_path() {
let path = abs_path("My Projects/src/main.go");
let uri = file_path_to_lsp_uri(&path);
assert!(uri.is_some(), "URI should be computed for path with spaces");
}
#[test]
fn test_normal_path() {
let path = abs_path("project/main.go");
let uri = file_path_to_lsp_uri(&path);
assert!(uri.is_some(), "URI should be computed for normal path");
let s = uri.unwrap().as_str().to_string();
assert!(s.starts_with("file:///"), "Should be a file URI: {}", s);
assert!(
s.ends_with("project/main.go"),
"Should end with the path: {}",
s
);
}
#[test]
fn test_relative_path_returns_none() {
let path = PathBuf::from("main.go");
assert!(file_path_to_lsp_uri(&path).is_none());
}
#[test]
fn test_all_special_chars() {
let path = abs_path("a[b]c{d}e^g`h/file.rs");
let uri = file_path_to_lsp_uri(&path);
assert!(uri.is_some(), "Should handle all special characters");
let s = uri.unwrap().as_str().to_string();
assert!(!s.contains('['), "[ should be encoded in {}", s);
assert!(!s.contains(']'), "] should be encoded in {}", s);
assert!(!s.contains('{'), "{{ should be encoded in {}", s);
assert!(!s.contains('}'), "}} should be encoded in {}", s);
assert!(!s.contains('^'), "^ should be encoded in {}", s);
assert!(!s.contains('`'), "` should be encoded in {}", s);
}
}
#[cfg(test)]
mod is_library_path_tests {
use super::*;
fn check(path: &str) -> bool {
BufferMetadata::is_library_path(Path::new(path), Path::new("/working_dir"))
}
#[test]
fn cargo_config_toml_is_not_a_library_file() {
assert!(!check("/home/user/.cargo/config.toml"));
assert!(!check("/home/user/project/.cargo/config.toml"));
}
#[test]
fn cargo_credentials_and_env_are_not_library_files() {
assert!(!check("/home/user/.cargo/credentials.toml"));
assert!(!check("/home/user/.cargo/env"));
}
#[test]
fn cargo_registry_sources_are_library_files() {
assert!(check(
"/home/user/.cargo/registry/src/index.crates.io-1cd66030c949c28d/serde-1.0.0/src/lib.rs"
));
}
#[test]
fn cargo_git_checkouts_are_library_files() {
assert!(check(
"/home/user/.cargo/git/checkouts/some-dep-abcdef/abcdef/src/lib.rs"
));
}
#[test]
fn cargo_config_toml_is_not_a_library_file_windows() {
assert!(!check("C:\\Users\\user\\.cargo\\config.toml"));
}
#[test]
fn cargo_registry_sources_are_library_files_windows() {
assert!(check(
"C:\\Users\\user\\.cargo\\registry\\src\\index.crates.io-1cd66030c949c28d\\serde-1.0.0\\src\\lib.rs"
));
}
}