Nathan SoboWIPd104df91 year agoHistory
578 lines · 19.9 KB · Rust
RawDownload
use super::{    fold_map::{FoldPlaceholder, FoldSnapshot},    inlay_map::{InlayEdit, InlaySnapshot},};use collections::{HashMap, HashSet};use gpui::{AnyElement, App, Context, Entity};use language::{Anchor, Buffer, BufferSnapshot, Edit, Point};use multi_buffer::{ExcerptId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};use parking_lot::Mutex;use serde::{Deserialize, Serialize};use std::{any::TypeId, ops::Range, sync::Arc};use sum_tree::TreeMap;use tree_sitter::{Query, QueryCapture, QueryCursor};use ui::{Color, IntoElement, Label};use util::ResultExt; /// Defines how to find foldable regions using Tree-sitter queries#[derive(Clone, Debug)]pub struct FoldingQuery {    pub query: Query,    pub auto_fold: bool,                 // Should regions auto-fold on load?    pub display_capture_ix: Option<u32>, // Which capture to show when folded    pub action_capture_ix: Option<u32>,  // Which capture contains action data    pub proximity_expand: bool,          // Expand when cursor is near?} /// Represents a foldable region found by queries#[derive(Clone, Debug)]pub struct SyntaxFold {    pub id: SyntaxFoldId,    pub range: Range<Anchor>,    pub display_text: Option<String>,    // Text to show when folded    pub action_data: Option<FoldAction>, // What happens on cmd+click    pub proximity_expand: bool,    pub auto_fold: bool,    pub query_index: usize, // Which query created this fold} /// Unique identifier for a syntax fold#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]pub struct SyntaxFoldId(usize); /// Possible actions when clicking folded content#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]pub enum FoldAction {    OpenUrl(String),                   // Open URL in browser    GoToFile(String),                  // Open file in editor    RunCommand(String),                // Run system command    Custom(String, serde_json::Value), // Extensibility point} /// Configuration for syntax folding#[derive(Clone, Debug, Default)]pub struct SyntaxFoldConfig {    pub enabled: bool,    pub auto_fold_on_open: bool,    pub proximity_expand_distance: u32, // Characters away from fold to trigger expansion} /// Snapshot of the syntax fold state at a point in timepub struct SyntaxFoldSnapshot {    buffer_snapshot: MultiBufferSnapshot,    folds: TreeMap<Range<Anchor>, SyntaxFold>,    queries: Arc<Vec<FoldingQuery>>,    config: SyntaxFoldConfig,} /// Map that tracks syntax-driven folds in the bufferpub struct SyntaxFoldMap {    buffer: Entity<MultiBuffer>,    queries: Arc<Vec<FoldingQuery>>,    config: SyntaxFoldConfig,    // All detected syntax folds    folds: TreeMap<Range<Anchor>, SyntaxFold>,    next_fold_id: usize,    // Track which folds are currently applied to the fold map    applied_folds: HashMap<SyntaxFoldId, Range<Anchor>>,    // Track which folds are temporarily expanded due to cursor proximity    proximity_expanded: Arc<Mutex<HashSet<SyntaxFoldId>>>,    // Track which folds have been manually toggled by the user    user_toggled: Arc<Mutex<HashSet<SyntaxFoldId>>>,} impl SyntaxFoldMap {    pub fn new(buffer: Entity<MultiBuffer>, cx: &mut App) -> Self {        let mut this = Self {            buffer: buffer.clone(),            queries: Arc::new(Vec::new()),            config: SyntaxFoldConfig::default(),            folds: TreeMap::default(),            next_fold_id: 0,            applied_folds: HashMap::default(),            proximity_expanded: Arc::new(Mutex::new(HashSet::default())),            user_toggled: Arc::new(Mutex::new(HashSet::default())),        };         // Initialize queries from languages        this.update_queries(cx);         // Perform initial fold detection if auto-fold is enabled        if this.config.enabled && this.config.auto_fold_on_open {            this.detect_folds(cx);        }         this    }     pub fn snapshot(&self, buffer_snapshot: MultiBufferSnapshot) -> SyntaxFoldSnapshot {        SyntaxFoldSnapshot {            buffer_snapshot,            folds: self.folds.clone(),            queries: self.queries.clone(),            config: self.config.clone(),        }    }     /// Returns ranges that should be folded/unfolded based on syntax analysis    pub fn sync(        &mut self,        buffer_snapshot: MultiBufferSnapshot,        buffer_edits: Vec<Edit<Point>>,        cx: &mut App,    ) -> (Vec<Range<Anchor>>, Vec<Range<Anchor>>) {        // Re-detect folds in edited regions        for edit in &buffer_edits {            self.detect_folds_in_range(edit.new.clone(), &buffer_snapshot, cx);        }         // Determine which folds to apply/remove        let mut to_fold = Vec::new();        let mut to_unfold = Vec::new();         for (range, fold) in self.folds.iter() {            let should_be_folded = fold.auto_fold                && !self.user_toggled.lock().contains(&fold.id)                && !self.proximity_expanded.lock().contains(&fold.id);             let is_folded = self.applied_folds.contains_key(&fold.id);             if should_be_folded && !is_folded {                to_fold.push(range.clone());                self.applied_folds.insert(fold.id, range.clone());            } else if !should_be_folded && is_folded {                to_unfold.push(range.clone());                self.applied_folds.remove(&fold.id);            }        }         (to_fold, to_unfold)    }     /// Update queries from the current languages in the buffer    fn update_queries(&mut self, cx: &App) {        let mut queries = Vec::new();        let buffer = self.buffer.read(cx);         for excerpt in buffer.excerpts() {            if let Some(language) = excerpt.buffer.read(cx).language() {                if let Some(folding_query_str) = language.folding_query() {                    if let Ok(query) =                        Query::new(&language.grammar().unwrap().ts_language, folding_query_str)                    {                        // Parse query metadata from captures                        let mut auto_fold = false;                        let mut display_capture_ix = None;                        let mut action_capture_ix = None;                        let mut proximity_expand = false;                         for (ix, name) in query.capture_names().iter().enumerate() {                            match name.as_str() {                                "fold.auto" => auto_fold = true,                                "fold.text" | "fold.display" => {                                    display_capture_ix = Some(ix as u32)                                }                                "fold.action" | "fold.url" => action_capture_ix = Some(ix as u32),                                "fold.proximity" => proximity_expand = true,                                _ => {}                            }                        }                         queries.push(FoldingQuery {                            query,                            auto_fold,                            display_capture_ix,                            action_capture_ix,                            proximity_expand,                        });                    }                }            }        }         self.queries = Arc::new(queries);        self.snapshot.queries = self.queries.clone();    }     /// Detect all folds in the buffer    fn detect_folds(&mut self, cx: &mut App) {        let buffer = self.buffer.read(cx);        let snapshot = buffer.snapshot(cx);         for (excerpt_id, excerpt) in snapshot.excerpts() {            if let Some(tree) = excerpt.tree() {                let excerpt_range = excerpt.range.to_point(&excerpt.buffer);                self.detect_folds_in_excerpt(                    excerpt_id,                    &excerpt.buffer,                    tree.root_node(),                    excerpt_range,                    cx,                );            }        }    }     /// Detect folds in a specific range    fn detect_folds_in_range(        &mut self,        range: Range<Point>,        buffer_snapshot: &MultiBufferSnapshot,        cx: &mut App,    ) {        // Remove existing folds in this range        let range_anchors =            buffer_snapshot.anchor_before(range.start)..buffer_snapshot.anchor_after(range.end);        let mut folds_to_remove = Vec::new();         for (fold_range, fold) in self.folds.iter() {            if fold_range                .start                .cmp(&range_anchors.end, buffer_snapshot)                .is_lt()                && fold_range                    .end                    .cmp(&range_anchors.start, buffer_snapshot)                    .is_gt()            {                folds_to_remove.push(fold.id);            }        }         for fold_id in folds_to_remove {            self.remove_fold(fold_id);        }         // Re-detect folds in affected excerpts        for (excerpt_id, excerpt) in buffer_snapshot.excerpts() {            let excerpt_range = excerpt.range.to_point(&excerpt.buffer);            if excerpt_range.start < range.end && excerpt_range.end > range.start {                if let Some(tree) = excerpt.tree() {                    self.detect_folds_in_excerpt(                        excerpt_id,                        &excerpt.buffer,                        tree.root_node(),                        excerpt_range,                        buffer_snapshot,                        cx,                    );                }            }        }    }     /// Detect folds in a specific excerpt using tree-sitter queries    fn detect_folds_in_excerpt(        &mut self,        excerpt_id: ExcerptId,        buffer: &BufferSnapshot,        root: Node,        range: Range<Point>,        cx: &mut App,    ) {        let mut cursor = QueryCursor::new();        cursor.set_point_range(range.clone());         for (query_index, folding_query) in self.queries.iter().enumerate() {            let matches = cursor.matches(                &folding_query.query,                root,                buffer                    .as_rope()                    .chunks_in_range(range.clone())                    .collect::<String>()                    .as_bytes(),            );             for match_ in matches {                if let Some(fold) = self.create_fold_from_match(                    match_.captures,                    query_index,                    folding_query,                    excerpt_id,                    buffer,                    cx,                ) {                    // Store the fold                    self.folds.insert(fold.range.clone(), fold);                }            }        }    }     /// Create a SyntaxFold from a tree-sitter match    fn create_fold_from_match(        &mut self,        captures: &[QueryCapture],        query_index: usize,        query: &FoldingQuery,        excerpt_id: ExcerptId,        buffer: &BufferSnapshot,        cx: &App,    ) -> Option<SyntaxFold> {        // Find the main fold capture (should be the one without a suffix)        let fold_capture = captures.iter().find(|c| {            let name = query.query.capture_names()[c.index as usize].as_str();            name == "fold" || name == "fold.auto"        })?;         let fold_node = fold_capture.node;        let start = Point::new(            fold_node.start_position().row as u32,            fold_node.start_position().column as u32,        );        let end = Point::new(            fold_node.end_position().row as u32,            fold_node.end_position().column as u32,        );         // Convert to anchors in the multi-buffer        let buffer_snapshot = self.buffer.read(cx).snapshot(cx);        let start_anchor = buffer_snapshot.anchor_in_excerpt(excerpt_id, start)?;        let end_anchor = buffer_snapshot.anchor_in_excerpt(excerpt_id, end)?;         // Extract display text if specified        let display_text = if let Some(display_ix) = query.display_capture_ix {            captures                .iter()                .find(|c| c.index == display_ix)                .and_then(|c| {                    let node = c.node;                    let start = node.start_byte();                    let end = node.end_byte();                    buffer.text_for_range(start..end).collect::<String>().into()                })        } else {            None        };         // Extract action data if specified        let action_data = if let Some(action_ix) = query.action_capture_ix {            captures                .iter()                .find(|c| c.index == action_ix)                .and_then(|c| {                    let node = c.node;                    let start = node.start_byte();                    let end = node.end_byte();                    let text = buffer.text_for_range(start..end).collect::<String>();                     // Parse action based on capture name                    let capture_name = &query.query.capture_names()[c.index as usize];                    match capture_name.as_str() {                        "fold.url" => Some(FoldAction::OpenUrl(text)),                        "fold.file" => Some(FoldAction::GoToFile(text)),                        "fold.command" => Some(FoldAction::RunCommand(text)),                        _ => None,                    }                })        } else {            None        };         let fold_id = SyntaxFoldId(self.next_fold_id);        self.next_fold_id += 1;         Some(SyntaxFold {            id: fold_id,            range: start_anchor..end_anchor,            display_text,            action_data,            proximity_expand: query.proximity_expand,            auto_fold: query.auto_fold,            query_index,        })    }     /// Create a fold placeholder for syntax folds    pub fn create_fold_placeholder(fold: &SyntaxFold) -> FoldPlaceholder {        let display_text = fold.display_text.clone();        FoldPlaceholder {            render: Arc::new(move |_id, _range, _cx| {                if let Some(text) = &display_text {                    // Render the display text as a clickable element                    Label::new(text.clone())                        .color(Color::Accent)                        .into_any_element()                } else {                    // Default ellipsis                    Label::new("…").color(Color::Muted).into_any_element()                }            }),            constrain_width: true,            merge_adjacent: false,            type_tag: Some(TypeId::of::<SyntaxFold>()),        }    }     /// Remove a syntax fold    fn remove_fold(&mut self, fold_id: SyntaxFoldId) {        let mut range_to_remove = None;        for (range, fold) in self.folds.iter() {            if fold.id == fold_id {                range_to_remove = Some(range.clone());                break;            }        }         if let Some(range) = range_to_remove {            self.folds.remove(&range);            self.applied_folds.remove(&fold_id);        }    }     /// Handle cursor movement for proximity-based expansion    pub fn handle_cursor_moved(        &mut self,        cursor_offset: usize,        buffer_snapshot: &MultiBufferSnapshot,    ) -> (Vec<Range<Anchor>>, Vec<Range<Anchor>>) {        let cursor_point = cursor_offset.to_point(buffer_snapshot);        let mut to_expand = Vec::new();        let mut to_collapse = Vec::new();         // Check each fold for proximity        for (range, fold) in self.folds.iter() {            if !fold.proximity_expand {                continue;            }             let fold_start = range.start.to_point(buffer_snapshot);            let fold_end = range.end.to_point(buffer_snapshot);             // Check if cursor is near the fold            let near_fold = Self::is_point_near_range(                cursor_point,                fold_start..fold_end,                self.config.proximity_expand_distance,            );             let is_expanded = self.proximity_expanded.lock().contains(&fold.id);             if near_fold && !is_expanded {                // Mark for expansion                self.proximity_expanded.lock().insert(fold.id);                to_expand.push(range.clone());            } else if !near_fold && is_expanded {                // Mark for collapse                self.proximity_expanded.lock().remove(&fold.id);                to_collapse.push(range.clone());            }        }         (to_expand, to_collapse)    }     /// Check if a point is near a range    fn is_point_near_range(point: Point, range: Range<Point>, distance: u32) -> bool {        // Check if point is within the range        if point >= range.start && point <= range.end {            return true;        }         // Check distance before range        if point.row == range.start.row            && range.start.column.saturating_sub(point.column) <= distance        {            return true;        }         // Check distance after range        if point.row == range.end.row && point.column.saturating_sub(range.end.column) <= distance {            return true;        }         false    }     /// Get fold at a specific position    pub fn fold_at_position(        &self,        offset: usize,        buffer_snapshot: &MultiBufferSnapshot,    ) -> Option<&SyntaxFold> {        let point = offset.to_point(buffer_snapshot);         for (range, fold) in self.folds.iter() {            let start = range.start.to_point(buffer_snapshot);            let end = range.end.to_point(buffer_snapshot);             if point >= start && point <= end {

Lines 1–500 of 578.