use crate::{App, Context};
use super::state::EditorState;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum StickyPosition {
#[default]
Top,
Status,
}
impl EditorState {
pub fn sticky_stack(&self, cx: &App) -> Vec<crate::highlight::DocumentSymbol> {
let cursor = self.cursor(cx);
let mut stack: Vec<crate::highlight::DocumentSymbol> = self
.outline
.iter()
.filter(|symbol| symbol.range.start <= cursor && cursor <= symbol.range.end)
.cloned()
.collect();
stack.sort_by(|a, b| {
a.range
.start
.cmp(&b.range.start)
.then((b.range.end - b.range.start).cmp(&(a.range.end - a.range.start)))
});
stack
}
pub fn set_sticky_scroll(&mut self, enabled: bool, cx: &mut Context<Self>) {
self.sticky_scroll = enabled;
cx.notify();
}
pub fn sticky_scroll_enabled(&self) -> bool {
self.sticky_scroll
}
pub fn set_sticky_position(&mut self, position: StickyPosition, cx: &mut Context<Self>) {
self.sticky_position = position;
cx.notify();
}
pub fn sticky_position(&self) -> StickyPosition {
self.sticky_position
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::highlight::{DocumentSymbol, SymbolKind};
use crate::{AppContext as _, Entity, Render, SharedString, Window};
struct Probe {
state: Entity<EditorState>,
}
impl Render for Probe {
fn render(
&mut self,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> impl crate::IntoElement {
crate::div()
}
}
struct NestedStub;
impl crate::highlight::Highlighter for NestedStub {
fn language(&self) -> SharedString {
"stub".into()
}
fn update(
&mut self,
_edit: Option<crate::highlight::TextEdit>,
_text: &ropey::Rope,
_folding: bool,
_window: &mut Window,
_cx: &mut App,
) {
}
fn styles(
&self,
range: &std::ops::Range<usize>,
_resolver: &dyn crate::highlight::HighlightStyleResolver,
) -> Vec<(std::ops::Range<usize>, crate::HighlightStyle)> {
vec![(range.clone(), crate::HighlightStyle::default())]
}
fn fold_ranges(&self, _text: &ropey::Rope) -> Vec<crate::highlight::FoldRange> {
Vec::new()
}
fn document_symbols(&self, _text: &ropey::Rope) -> Vec<DocumentSymbol> {
vec![
DocumentSymbol {
kind: SymbolKind::Struct,
name: "Outer".into(),
range: 0..20,
start_row: 0,
},
DocumentSymbol {
kind: SymbolKind::Function,
name: "inner".into(),
range: 5..10,
start_row: 0,
},
]
}
}
fn with_nested(cx: &mut crate::TestAppContext) -> Entity<EditorState> {
let (probe, cx) = cx.add_window_view(|window, cx| {
let editor =
cx.new(|cx| EditorState::new(window, cx, "0123456789abcdefghij0123456789"));
Probe { state: editor }
});
let editor = probe.read_with(cx, |probe, _| probe.state.clone());
cx.update(|window, cx| {
editor.update(cx, |state, cx| {
state.set_highlighter(Some(Box::new(NestedStub)), window, cx);
});
});
editor
}
#[rgpui::test]
fn sticky_stack_nested(cx: &mut crate::TestAppContext) {
let editor = with_nested(cx);
assert!(editor.read_with(cx, |state, _| state.sticky_scroll_enabled()));
editor.update(cx, |state, cx| {
state.set_selected_range(7..7, cx);
});
let names: Vec<String> = editor.read_with(cx, |state, cx| {
state
.sticky_stack(cx)
.iter()
.map(|s| s.name.to_string())
.collect()
});
assert_eq!(names, vec!["Outer".to_string(), "inner".to_string()]);
editor.update(cx, |state, cx| {
state.set_selected_range(15..15, cx);
});
let names: Vec<String> = editor.read_with(cx, |state, cx| {
state
.sticky_stack(cx)
.iter()
.map(|s| s.name.to_string())
.collect()
});
assert_eq!(names, vec!["Outer".to_string()]);
}
#[rgpui::test]
fn sticky_stack_empty_outside(cx: &mut crate::TestAppContext) {
let editor = with_nested(cx);
editor.update(cx, |state, cx| {
state.set_selected_range(25..25, cx);
});
assert!(
editor
.read_with(cx, |state, cx| state.sticky_stack(cx))
.is_empty()
);
}
#[rgpui::test]
fn sticky_position_defaults_top(cx: &mut crate::TestAppContext) {
let editor = with_nested(cx);
assert_eq!(
editor.read_with(cx, |state, _| state.sticky_position()),
StickyPosition::Top
);
editor.update(cx, |state, cx| {
state.set_sticky_position(StickyPosition::Status, cx);
});
assert_eq!(
editor.read_with(cx, |state, _| state.sticky_position()),
StickyPosition::Status
);
editor.update(cx, |state, cx| {
state.set_selected_range(7..7, cx);
});
assert_eq!(
editor.read_with(cx, |state, cx| state
.sticky_stack(cx)
.iter()
.map(|s| s.name.to_string())
.collect::<Vec<_>>()),
vec!["Outer".to_string(), "inner".to_string()]
);
}
#[rgpui::test]
fn sticky_empty_without_highlighter_and_toggle(cx: &mut crate::TestAppContext) {
let (probe, cx) = cx.add_window_view(|window, cx| {
let editor = cx.new(|cx| EditorState::new(window, cx, "fn main() {}\n"));
Probe { state: editor }
});
let editor = probe.read_with(cx, |probe, _| probe.state.clone());
assert!(
editor
.read_with(cx, |state, cx| state.sticky_stack(cx))
.is_empty()
);
editor.update(cx, |state, cx| {
state.set_sticky_scroll(false, cx);
});
assert!(!editor.read_with(cx, |state, _| state.sticky_scroll_enabled()));
}
}