Merge pull request #342 from zed-industries/symbolic-nav
Introduce outline view
This commit is contained in:
commit
485554cd0c
30 changed files with 1457 additions and 235 deletions
17
Cargo.lock
generated
17
Cargo.lock
generated
|
@ -2602,6 +2602,7 @@ dependencies = [
|
|||
"ctor",
|
||||
"env_logger",
|
||||
"futures",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"lazy_static",
|
||||
"log",
|
||||
|
@ -3121,6 +3122,21 @@ version = "2.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85"
|
||||
|
||||
[[package]]
|
||||
name = "outline"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"editor",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"language",
|
||||
"ordered-float",
|
||||
"postage",
|
||||
"smol",
|
||||
"text",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "p256"
|
||||
version = "0.9.0"
|
||||
|
@ -5724,6 +5740,7 @@ dependencies = [
|
|||
"log-panics",
|
||||
"lsp",
|
||||
"num_cpus",
|
||||
"outline",
|
||||
"parking_lot",
|
||||
"postage",
|
||||
"project",
|
||||
|
|
|
@ -28,8 +28,10 @@ use language::{
|
|||
BracketPair, Buffer, Diagnostic, DiagnosticSeverity, Language, Point, Selection, SelectionGoal,
|
||||
TransactionId,
|
||||
};
|
||||
pub use multi_buffer::{Anchor, ExcerptId, ExcerptProperties, MultiBuffer, ToOffset, ToPoint};
|
||||
use multi_buffer::{AnchorRangeExt, MultiBufferChunks, MultiBufferSnapshot};
|
||||
pub use multi_buffer::{
|
||||
Anchor, AnchorRangeExt, ExcerptId, ExcerptProperties, MultiBuffer, ToOffset, ToPoint,
|
||||
};
|
||||
use multi_buffer::{MultiBufferChunks, MultiBufferSnapshot};
|
||||
use postage::watch;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smallvec::SmallVec;
|
||||
|
@ -374,7 +376,7 @@ pub struct Editor {
|
|||
blinking_paused: bool,
|
||||
mode: EditorMode,
|
||||
placeholder_text: Option<Arc<str>>,
|
||||
highlighted_row: Option<u32>,
|
||||
highlighted_rows: Option<Range<u32>>,
|
||||
}
|
||||
|
||||
pub struct EditorSnapshot {
|
||||
|
@ -503,7 +505,7 @@ impl Editor {
|
|||
blinking_paused: false,
|
||||
mode: EditorMode::Full,
|
||||
placeholder_text: None,
|
||||
highlighted_row: None,
|
||||
highlighted_rows: None,
|
||||
};
|
||||
let selection = Selection {
|
||||
id: post_inc(&mut this.next_selection_id),
|
||||
|
@ -2388,6 +2390,11 @@ impl Editor {
|
|||
}
|
||||
|
||||
pub fn move_to_beginning(&mut self, _: &MoveToBeginning, cx: &mut ViewContext<Self>) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate_action();
|
||||
return;
|
||||
}
|
||||
|
||||
let selection = Selection {
|
||||
id: post_inc(&mut self.next_selection_id),
|
||||
start: 0,
|
||||
|
@ -2405,6 +2412,11 @@ impl Editor {
|
|||
}
|
||||
|
||||
pub fn move_to_end(&mut self, _: &MoveToEnd, cx: &mut ViewContext<Self>) {
|
||||
if matches!(self.mode, EditorMode::SingleLine) {
|
||||
cx.propagate_action();
|
||||
return;
|
||||
}
|
||||
|
||||
let cursor = self.buffer.read(cx).read(cx).len();
|
||||
let selection = Selection {
|
||||
id: post_inc(&mut self.next_selection_id),
|
||||
|
@ -3544,12 +3556,12 @@ impl Editor {
|
|||
.update(cx, |map, cx| map.set_wrap_width(width, cx))
|
||||
}
|
||||
|
||||
pub fn set_highlighted_row(&mut self, row: Option<u32>) {
|
||||
self.highlighted_row = row;
|
||||
pub fn set_highlighted_rows(&mut self, rows: Option<Range<u32>>) {
|
||||
self.highlighted_rows = rows;
|
||||
}
|
||||
|
||||
pub fn highlighted_row(&mut self) -> Option<u32> {
|
||||
self.highlighted_row
|
||||
pub fn highlighted_rows(&self) -> Option<Range<u32>> {
|
||||
self.highlighted_rows.clone()
|
||||
}
|
||||
|
||||
fn next_blink_epoch(&mut self) -> usize {
|
||||
|
|
|
@ -7,6 +7,8 @@ use clock::ReplicaId;
|
|||
use collections::{BTreeMap, HashMap};
|
||||
use gpui::{
|
||||
color::Color,
|
||||
elements::layout_highlighted_chunks,
|
||||
fonts::HighlightStyle,
|
||||
geometry::{
|
||||
rect::RectF,
|
||||
vector::{vec2f, Vector2F},
|
||||
|
@ -19,7 +21,7 @@ use gpui::{
|
|||
MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle,
|
||||
};
|
||||
use json::json;
|
||||
use language::{Bias, Chunk};
|
||||
use language::Bias;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
cmp::{self, Ordering},
|
||||
|
@ -263,12 +265,16 @@ impl EditorElement {
|
|||
}
|
||||
}
|
||||
|
||||
if let Some(highlighted_row) = layout.highlighted_row {
|
||||
if let Some(highlighted_rows) = &layout.highlighted_rows {
|
||||
let origin = vec2f(
|
||||
bounds.origin_x(),
|
||||
bounds.origin_y() + (layout.line_height * highlighted_row as f32) - scroll_top,
|
||||
bounds.origin_y() + (layout.line_height * highlighted_rows.start as f32)
|
||||
- scroll_top,
|
||||
);
|
||||
let size = vec2f(
|
||||
bounds.width(),
|
||||
layout.line_height * highlighted_rows.len() as f32,
|
||||
);
|
||||
let size = vec2f(bounds.width(), layout.line_height);
|
||||
cx.scene.push_quad(Quad {
|
||||
bounds: RectF::new(origin, size),
|
||||
background: Some(style.highlighted_line_background),
|
||||
|
@ -537,86 +543,37 @@ impl EditorElement {
|
|||
)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
let style = &self.settings.style;
|
||||
let mut prev_font_properties = style.text.font_properties.clone();
|
||||
let mut prev_font_id = style.text.font_id;
|
||||
|
||||
let mut layouts = Vec::with_capacity(rows.len());
|
||||
let mut line = String::new();
|
||||
let mut styles = Vec::new();
|
||||
let mut row = rows.start;
|
||||
let mut line_exceeded_max_len = false;
|
||||
let chunks = snapshot.chunks(rows.clone(), Some(&style.syntax));
|
||||
|
||||
let newline_chunk = Chunk {
|
||||
text: "\n",
|
||||
..Default::default()
|
||||
};
|
||||
'outer: for chunk in chunks.chain([newline_chunk]) {
|
||||
for (ix, mut line_chunk) in chunk.text.split('\n').enumerate() {
|
||||
if ix > 0 {
|
||||
layouts.push(cx.text_layout_cache.layout_str(
|
||||
&line,
|
||||
style.text.font_size,
|
||||
&styles,
|
||||
));
|
||||
line.clear();
|
||||
styles.clear();
|
||||
row += 1;
|
||||
line_exceeded_max_len = false;
|
||||
if row == rows.end {
|
||||
break 'outer;
|
||||
}
|
||||
}
|
||||
|
||||
if !line_chunk.is_empty() && !line_exceeded_max_len {
|
||||
let highlight_style =
|
||||
chunk.highlight_style.unwrap_or(style.text.clone().into());
|
||||
// Avoid a lookup if the font properties match the previous ones.
|
||||
let font_id = if highlight_style.font_properties == prev_font_properties {
|
||||
prev_font_id
|
||||
} else {
|
||||
cx.font_cache
|
||||
.select_font(
|
||||
style.text.font_family_id,
|
||||
&highlight_style.font_properties,
|
||||
)
|
||||
.unwrap_or(style.text.font_id)
|
||||
};
|
||||
|
||||
if line.len() + line_chunk.len() > MAX_LINE_LEN {
|
||||
let mut chunk_len = MAX_LINE_LEN - line.len();
|
||||
while !line_chunk.is_char_boundary(chunk_len) {
|
||||
chunk_len -= 1;
|
||||
} else {
|
||||
let style = &self.settings.style;
|
||||
let chunks = snapshot
|
||||
.chunks(rows.clone(), Some(&style.syntax))
|
||||
.map(|chunk| {
|
||||
let highlight = if let Some(severity) = chunk.diagnostic {
|
||||
let underline = Some(super::diagnostic_style(severity, true, style).text);
|
||||
if let Some(mut highlight) = chunk.highlight_style {
|
||||
highlight.underline = underline;
|
||||
Some(highlight)
|
||||
} else {
|
||||
Some(HighlightStyle {
|
||||
underline,
|
||||
color: style.text.color,
|
||||
font_properties: style.text.font_properties,
|
||||
})
|
||||
}
|
||||
line_chunk = &line_chunk[..chunk_len];
|
||||
line_exceeded_max_len = true;
|
||||
}
|
||||
|
||||
let underline = if let Some(severity) = chunk.diagnostic {
|
||||
Some(super::diagnostic_style(severity, true, style).text)
|
||||
} else {
|
||||
highlight_style.underline
|
||||
chunk.highlight_style
|
||||
};
|
||||
|
||||
line.push_str(line_chunk);
|
||||
styles.push((
|
||||
line_chunk.len(),
|
||||
RunStyle {
|
||||
font_id,
|
||||
color: highlight_style.color,
|
||||
underline,
|
||||
},
|
||||
));
|
||||
prev_font_id = font_id;
|
||||
prev_font_properties = highlight_style.font_properties;
|
||||
}
|
||||
}
|
||||
(chunk.text, highlight)
|
||||
});
|
||||
layout_highlighted_chunks(
|
||||
chunks,
|
||||
&style.text,
|
||||
&cx.text_layout_cache,
|
||||
&cx.font_cache,
|
||||
MAX_LINE_LEN,
|
||||
rows.len() as usize,
|
||||
)
|
||||
}
|
||||
|
||||
layouts
|
||||
}
|
||||
|
||||
fn layout_blocks(
|
||||
|
@ -640,15 +597,20 @@ impl EditorElement {
|
|||
.to_display_point(snapshot)
|
||||
.row();
|
||||
|
||||
let anchor_x = text_x + if rows.contains(&anchor_row) {
|
||||
line_layouts[(anchor_row - rows.start) as usize]
|
||||
.x_for_index(block.column() as usize)
|
||||
} else {
|
||||
layout_line(anchor_row, snapshot, style, cx.text_layout_cache)
|
||||
.x_for_index(block.column() as usize)
|
||||
};
|
||||
let anchor_x = text_x
|
||||
+ if rows.contains(&anchor_row) {
|
||||
line_layouts[(anchor_row - rows.start) as usize]
|
||||
.x_for_index(block.column() as usize)
|
||||
} else {
|
||||
layout_line(anchor_row, snapshot, style, cx.text_layout_cache)
|
||||
.x_for_index(block.column() as usize)
|
||||
};
|
||||
|
||||
let mut element = block.render(&BlockContext { cx, anchor_x, line_number_x, });
|
||||
let mut element = block.render(&BlockContext {
|
||||
cx,
|
||||
anchor_x,
|
||||
line_number_x,
|
||||
});
|
||||
element.layout(
|
||||
SizeConstraint {
|
||||
min: Vector2F::zero(),
|
||||
|
@ -750,9 +712,9 @@ impl Element for EditorElement {
|
|||
|
||||
let mut selections = HashMap::default();
|
||||
let mut active_rows = BTreeMap::new();
|
||||
let mut highlighted_row = None;
|
||||
let mut highlighted_rows = None;
|
||||
self.update_view(cx.app, |view, cx| {
|
||||
highlighted_row = view.highlighted_row();
|
||||
highlighted_rows = view.highlighted_rows();
|
||||
let display_map = view.display_map.update(cx, |map, cx| map.snapshot(cx));
|
||||
|
||||
let local_selections = view
|
||||
|
@ -831,7 +793,7 @@ impl Element for EditorElement {
|
|||
snapshot,
|
||||
style: self.settings.style.clone(),
|
||||
active_rows,
|
||||
highlighted_row,
|
||||
highlighted_rows,
|
||||
line_layouts,
|
||||
line_number_layouts,
|
||||
blocks,
|
||||
|
@ -962,7 +924,7 @@ pub struct LayoutState {
|
|||
style: EditorStyle,
|
||||
snapshot: EditorSnapshot,
|
||||
active_rows: BTreeMap<u32, bool>,
|
||||
highlighted_row: Option<u32>,
|
||||
highlighted_rows: Option<Range<u32>>,
|
||||
line_layouts: Vec<text_layout::Line>,
|
||||
line_number_layouts: Vec<Option<text_layout::Line>>,
|
||||
blocks: Vec<(u32, ElementBox)>,
|
||||
|
|
|
@ -6,8 +6,8 @@ use clock::ReplicaId;
|
|||
use collections::{HashMap, HashSet};
|
||||
use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
|
||||
use language::{
|
||||
Buffer, BufferChunks, BufferSnapshot, Chunk, DiagnosticEntry, Event, File, Language, Selection,
|
||||
ToOffset as _, ToPoint as _, TransactionId,
|
||||
Buffer, BufferChunks, BufferSnapshot, Chunk, DiagnosticEntry, Event, File, Language, Outline,
|
||||
OutlineItem, Selection, ToOffset as _, ToPoint as _, TransactionId,
|
||||
};
|
||||
use std::{
|
||||
cell::{Ref, RefCell},
|
||||
|
@ -1698,6 +1698,26 @@ impl MultiBufferSnapshot {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option<Outline<Anchor>> {
|
||||
let buffer = self.as_singleton()?;
|
||||
let outline = buffer.outline(theme)?;
|
||||
let excerpt_id = &self.excerpts.iter().next().unwrap().id;
|
||||
Some(Outline::new(
|
||||
outline
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|item| OutlineItem {
|
||||
depth: item.depth,
|
||||
range: self.anchor_in_excerpt(excerpt_id.clone(), item.range.start)
|
||||
..self.anchor_in_excerpt(excerpt_id.clone(), item.range.end),
|
||||
text: item.text,
|
||||
highlight_ranges: item.highlight_ranges,
|
||||
name_ranges: item.name_ranges,
|
||||
})
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
|
||||
fn buffer_snapshot_for_excerpt<'a>(
|
||||
&'a self,
|
||||
excerpt_id: &'a ExcerptId,
|
||||
|
|
|
@ -3,11 +3,7 @@ use fuzzy::PathMatch;
|
|||
use gpui::{
|
||||
action,
|
||||
elements::*,
|
||||
keymap::{
|
||||
self,
|
||||
menu::{SelectNext, SelectPrev},
|
||||
Binding,
|
||||
},
|
||||
keymap::{self, Binding},
|
||||
AppContext, Axis, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
|
@ -22,7 +18,10 @@ use std::{
|
|||
},
|
||||
};
|
||||
use util::post_inc;
|
||||
use workspace::{Settings, Workspace};
|
||||
use workspace::{
|
||||
menu::{Confirm, SelectNext, SelectPrev},
|
||||
Settings, Workspace,
|
||||
};
|
||||
|
||||
pub struct FileFinder {
|
||||
handle: WeakViewHandle<Self>,
|
||||
|
@ -40,7 +39,6 @@ pub struct FileFinder {
|
|||
}
|
||||
|
||||
action!(Toggle);
|
||||
action!(Confirm);
|
||||
action!(Select, ProjectPath);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
|
@ -53,7 +51,6 @@ pub fn init(cx: &mut MutableAppContext) {
|
|||
cx.add_bindings(vec![
|
||||
Binding::new("cmd-p", Toggle, None),
|
||||
Binding::new("escape", Toggle, Some("FileFinder")),
|
||||
Binding::new("enter", Confirm, Some("FileFinder")),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -353,7 +350,8 @@ impl FileFinder {
|
|||
let mat = &self.matches[selected_index];
|
||||
self.selected = Some((mat.worktree_id, mat.path.clone()));
|
||||
}
|
||||
self.list_state.scroll_to(selected_index);
|
||||
self.list_state
|
||||
.scroll_to(ScrollTarget::Show(selected_index));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
@ -364,7 +362,8 @@ impl FileFinder {
|
|||
let mat = &self.matches[selected_index];
|
||||
self.selected = Some((mat.worktree_id, mat.path.clone()));
|
||||
}
|
||||
self.list_state.scroll_to(selected_index);
|
||||
self.list_state
|
||||
.scroll_to(ScrollTarget::Show(selected_index));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
@ -415,7 +414,8 @@ impl FileFinder {
|
|||
}
|
||||
self.latest_search_query = query;
|
||||
self.latest_search_did_cancel = did_cancel;
|
||||
self.list_state.scroll_to(self.selected_index());
|
||||
self.list_state
|
||||
.scroll_to(ScrollTarget::Show(self.selected_index()));
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ impl CharBag {
|
|||
}
|
||||
|
||||
fn insert(&mut self, c: char) {
|
||||
let c = c.to_ascii_lowercase();
|
||||
if c >= 'a' && c <= 'z' {
|
||||
let mut count = self.0;
|
||||
let idx = c as u8 - 'a' as u8;
|
||||
|
|
|
@ -55,6 +55,7 @@ pub struct PathMatch {
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct StringMatchCandidate {
|
||||
pub id: usize,
|
||||
pub string: String,
|
||||
pub char_bag: CharBag,
|
||||
}
|
||||
|
@ -109,6 +110,7 @@ impl<'a> MatchCandidate for &'a StringMatchCandidate {
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct StringMatch {
|
||||
pub candidate_id: usize,
|
||||
pub score: f64,
|
||||
pub positions: Vec<usize>,
|
||||
pub string: String,
|
||||
|
@ -116,7 +118,7 @@ pub struct StringMatch {
|
|||
|
||||
impl PartialEq for StringMatch {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.score.eq(&other.score)
|
||||
self.cmp(other).is_eq()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,13 +135,13 @@ impl Ord for StringMatch {
|
|||
self.score
|
||||
.partial_cmp(&other.score)
|
||||
.unwrap_or(Ordering::Equal)
|
||||
.then_with(|| self.string.cmp(&other.string))
|
||||
.then_with(|| self.candidate_id.cmp(&other.candidate_id))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for PathMatch {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.score.eq(&other.score)
|
||||
self.cmp(other).is_eq()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -187,8 +189,8 @@ pub async fn match_strings(
|
|||
for (segment_idx, results) in segment_results.iter_mut().enumerate() {
|
||||
let cancel_flag = &cancel_flag;
|
||||
scope.spawn(async move {
|
||||
let segment_start = segment_idx * segment_size;
|
||||
let segment_end = segment_start + segment_size;
|
||||
let segment_start = cmp::min(segment_idx * segment_size, candidates.len());
|
||||
let segment_end = cmp::min(segment_start + segment_size, candidates.len());
|
||||
let mut matcher = Matcher::new(
|
||||
query,
|
||||
lowercase_query,
|
||||
|
@ -330,6 +332,7 @@ impl<'a> Matcher<'a> {
|
|||
results,
|
||||
cancel_flag,
|
||||
|candidate, score| StringMatch {
|
||||
candidate_id: candidate.id,
|
||||
score,
|
||||
positions: Vec::new(),
|
||||
string: candidate.string.to_string(),
|
||||
|
@ -433,13 +436,17 @@ impl<'a> Matcher<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
fn find_last_positions(&mut self, prefix: &[char], path: &[char]) -> bool {
|
||||
let mut path = path.iter();
|
||||
let mut prefix_iter = prefix.iter();
|
||||
for (i, char) in self.query.iter().enumerate().rev() {
|
||||
if let Some(j) = path.rposition(|c| c == char) {
|
||||
self.last_positions[i] = j + prefix.len();
|
||||
} else if let Some(j) = prefix_iter.rposition(|c| c == char) {
|
||||
fn find_last_positions(
|
||||
&mut self,
|
||||
lowercase_prefix: &[char],
|
||||
lowercase_candidate: &[char],
|
||||
) -> bool {
|
||||
let mut lowercase_prefix = lowercase_prefix.iter();
|
||||
let mut lowercase_candidate = lowercase_candidate.iter();
|
||||
for (i, char) in self.lowercase_query.iter().enumerate().rev() {
|
||||
if let Some(j) = lowercase_candidate.rposition(|c| c == char) {
|
||||
self.last_positions[i] = j + lowercase_prefix.len();
|
||||
} else if let Some(j) = lowercase_prefix.rposition(|c| c == char) {
|
||||
self.last_positions[i] = j;
|
||||
} else {
|
||||
return false;
|
||||
|
|
|
@ -26,7 +26,7 @@ pub struct GoToLine {
|
|||
line_editor: ViewHandle<Editor>,
|
||||
active_editor: ViewHandle<Editor>,
|
||||
restore_state: Option<RestoreState>,
|
||||
line_selection: Option<Selection<usize>>,
|
||||
line_selection_id: Option<usize>,
|
||||
cursor_point: Point,
|
||||
max_point: Point,
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ impl GoToLine {
|
|||
line_editor,
|
||||
active_editor,
|
||||
restore_state,
|
||||
line_selection: None,
|
||||
line_selection_id: None,
|
||||
cursor_point,
|
||||
max_point,
|
||||
}
|
||||
|
@ -139,13 +139,18 @@ impl GoToLine {
|
|||
column.map(|column| column.saturating_sub(1)).unwrap_or(0),
|
||||
)
|
||||
}) {
|
||||
self.line_selection = self.active_editor.update(cx, |active_editor, cx| {
|
||||
self.line_selection_id = self.active_editor.update(cx, |active_editor, cx| {
|
||||
let snapshot = active_editor.snapshot(cx).display_snapshot;
|
||||
let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
|
||||
let display_point = point.to_display_point(&snapshot);
|
||||
let row = display_point.row();
|
||||
active_editor.select_ranges([point..point], Some(Autoscroll::Center), cx);
|
||||
active_editor.set_highlighted_row(Some(display_point.row()));
|
||||
Some(active_editor.newest_selection(&snapshot.buffer_snapshot))
|
||||
active_editor.set_highlighted_rows(Some(row..row + 1));
|
||||
Some(
|
||||
active_editor
|
||||
.newest_selection::<usize>(&snapshot.buffer_snapshot)
|
||||
.id,
|
||||
)
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
@ -159,14 +164,14 @@ impl Entity for GoToLine {
|
|||
type Event = Event;
|
||||
|
||||
fn release(&mut self, cx: &mut MutableAppContext) {
|
||||
let line_selection = self.line_selection.take();
|
||||
let line_selection_id = self.line_selection_id.take();
|
||||
let restore_state = self.restore_state.take();
|
||||
self.active_editor.update(cx, |editor, cx| {
|
||||
editor.set_highlighted_row(None);
|
||||
if let Some((line_selection, restore_state)) = line_selection.zip(restore_state) {
|
||||
editor.set_highlighted_rows(None);
|
||||
if let Some((line_selection_id, restore_state)) = line_selection_id.zip(restore_state) {
|
||||
let newest_selection =
|
||||
editor.newest_selection::<usize>(&editor.buffer().read(cx).read(cx));
|
||||
if line_selection.id == newest_selection.id {
|
||||
if line_selection_id == newest_selection.id {
|
||||
editor.set_scroll_position(restore_state.scroll_position, cx);
|
||||
editor.update_selections(restore_state.selections, None, cx);
|
||||
}
|
||||
|
@ -219,6 +224,4 @@ impl View for GoToLine {
|
|||
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
|
||||
cx.focus(&self.line_editor);
|
||||
}
|
||||
|
||||
fn on_blur(&mut self, _: &mut ViewContext<Self>) {}
|
||||
}
|
||||
|
|
|
@ -52,6 +52,11 @@ impl Container {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn with_margin_bottom(mut self, margin: f32) -> Self {
|
||||
self.style.margin.bottom = margin;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_margin_left(mut self, margin: f32) -> Self {
|
||||
self.style.margin.left = margin;
|
||||
self
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
color::Color,
|
||||
fonts::TextStyle,
|
||||
fonts::{HighlightStyle, TextStyle},
|
||||
geometry::{
|
||||
rect::RectF,
|
||||
vector::{vec2f, Vector2F},
|
||||
},
|
||||
json::{ToJson, Value},
|
||||
text_layout::{Line, ShapedBoundary},
|
||||
DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
|
||||
text_layout::{Line, RunStyle, ShapedBoundary},
|
||||
DebugContext, Element, Event, EventContext, FontCache, LayoutContext, PaintContext,
|
||||
SizeConstraint, TextLayoutCache,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
|
@ -15,10 +18,12 @@ pub struct Text {
|
|||
text: String,
|
||||
style: TextStyle,
|
||||
soft_wrap: bool,
|
||||
highlights: Vec<(Range<usize>, HighlightStyle)>,
|
||||
}
|
||||
|
||||
pub struct LayoutState {
|
||||
lines: Vec<(Line, Vec<ShapedBoundary>)>,
|
||||
shaped_lines: Vec<Line>,
|
||||
wrap_boundaries: Vec<Vec<ShapedBoundary>>,
|
||||
line_height: f32,
|
||||
}
|
||||
|
||||
|
@ -28,6 +33,7 @@ impl Text {
|
|||
text,
|
||||
style,
|
||||
soft_wrap: true,
|
||||
highlights: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -36,6 +42,11 @@ impl Text {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn with_highlights(mut self, runs: Vec<(Range<usize>, HighlightStyle)>) -> Self {
|
||||
self.highlights = runs;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_soft_wrap(mut self, soft_wrap: bool) -> Self {
|
||||
self.soft_wrap = soft_wrap;
|
||||
self
|
||||
|
@ -51,32 +62,59 @@ impl Element for Text {
|
|||
constraint: SizeConstraint,
|
||||
cx: &mut LayoutContext,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
let font_id = self.style.font_id;
|
||||
let line_height = cx.font_cache.line_height(font_id, self.style.font_size);
|
||||
// Convert the string and highlight ranges into an iterator of highlighted chunks.
|
||||
let mut offset = 0;
|
||||
let mut highlight_ranges = self.highlights.iter().peekable();
|
||||
let chunks = std::iter::from_fn(|| {
|
||||
let result;
|
||||
if let Some((range, highlight)) = highlight_ranges.peek() {
|
||||
if offset < range.start {
|
||||
result = Some((&self.text[offset..range.start], None));
|
||||
offset = range.start;
|
||||
} else {
|
||||
result = Some((&self.text[range.clone()], Some(*highlight)));
|
||||
highlight_ranges.next();
|
||||
offset = range.end;
|
||||
}
|
||||
} else if offset < self.text.len() {
|
||||
result = Some((&self.text[offset..], None));
|
||||
offset = self.text.len();
|
||||
} else {
|
||||
result = None;
|
||||
}
|
||||
result
|
||||
});
|
||||
|
||||
let mut wrapper = cx.font_cache.line_wrapper(font_id, self.style.font_size);
|
||||
let mut lines = Vec::new();
|
||||
// Perform shaping on these highlighted chunks
|
||||
let shaped_lines = layout_highlighted_chunks(
|
||||
chunks,
|
||||
&self.style,
|
||||
cx.text_layout_cache,
|
||||
&cx.font_cache,
|
||||
usize::MAX,
|
||||
self.text.matches('\n').count() + 1,
|
||||
);
|
||||
|
||||
// If line wrapping is enabled, wrap each of the shaped lines.
|
||||
let font_id = self.style.font_id;
|
||||
let mut line_count = 0;
|
||||
let mut max_line_width = 0_f32;
|
||||
for line in self.text.lines() {
|
||||
let shaped_line = cx.text_layout_cache.layout_str(
|
||||
line,
|
||||
self.style.font_size,
|
||||
&[(line.len(), self.style.to_run())],
|
||||
);
|
||||
let wrap_boundaries = if self.soft_wrap {
|
||||
wrapper
|
||||
.wrap_shaped_line(line, &shaped_line, constraint.max.x())
|
||||
.collect::<Vec<_>>()
|
||||
let mut wrap_boundaries = Vec::new();
|
||||
let mut wrapper = cx.font_cache.line_wrapper(font_id, self.style.font_size);
|
||||
for (line, shaped_line) in self.text.lines().zip(&shaped_lines) {
|
||||
if self.soft_wrap {
|
||||
let boundaries = wrapper
|
||||
.wrap_shaped_line(line, shaped_line, constraint.max.x())
|
||||
.collect::<Vec<_>>();
|
||||
line_count += boundaries.len() + 1;
|
||||
wrap_boundaries.push(boundaries);
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
line_count += 1;
|
||||
}
|
||||
max_line_width = max_line_width.max(shaped_line.width());
|
||||
line_count += wrap_boundaries.len() + 1;
|
||||
lines.push((shaped_line, wrap_boundaries));
|
||||
}
|
||||
|
||||
let line_height = cx.font_cache.line_height(font_id, self.style.font_size);
|
||||
let size = vec2f(
|
||||
max_line_width
|
||||
.ceil()
|
||||
|
@ -84,7 +122,14 @@ impl Element for Text {
|
|||
.min(constraint.max.x()),
|
||||
(line_height * line_count as f32).ceil(),
|
||||
);
|
||||
(size, LayoutState { lines, line_height })
|
||||
(
|
||||
size,
|
||||
LayoutState {
|
||||
shaped_lines,
|
||||
wrap_boundaries,
|
||||
line_height,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
|
@ -95,8 +140,10 @@ impl Element for Text {
|
|||
cx: &mut PaintContext,
|
||||
) -> Self::PaintState {
|
||||
let mut origin = bounds.origin();
|
||||
for (line, wrap_boundaries) in &layout.lines {
|
||||
let wrapped_line_boundaries = RectF::new(
|
||||
let empty = Vec::new();
|
||||
for (ix, line) in layout.shaped_lines.iter().enumerate() {
|
||||
let wrap_boundaries = layout.wrap_boundaries.get(ix).unwrap_or(&empty);
|
||||
let boundaries = RectF::new(
|
||||
origin,
|
||||
vec2f(
|
||||
bounds.width(),
|
||||
|
@ -104,16 +151,20 @@ impl Element for Text {
|
|||
),
|
||||
);
|
||||
|
||||
if wrapped_line_boundaries.intersects(visible_bounds) {
|
||||
line.paint_wrapped(
|
||||
origin,
|
||||
visible_bounds,
|
||||
layout.line_height,
|
||||
wrap_boundaries.iter().copied(),
|
||||
cx,
|
||||
);
|
||||
if boundaries.intersects(visible_bounds) {
|
||||
if self.soft_wrap {
|
||||
line.paint_wrapped(
|
||||
origin,
|
||||
visible_bounds,
|
||||
layout.line_height,
|
||||
wrap_boundaries.iter().copied(),
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
line.paint(origin, visible_bounds, layout.line_height, cx);
|
||||
}
|
||||
}
|
||||
origin.set_y(wrapped_line_boundaries.max_y());
|
||||
origin.set_y(boundaries.max_y());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -143,3 +194,71 @@ impl Element for Text {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform text layout on a series of highlighted chunks of text.
|
||||
pub fn layout_highlighted_chunks<'a>(
|
||||
chunks: impl Iterator<Item = (&'a str, Option<HighlightStyle>)>,
|
||||
style: &'a TextStyle,
|
||||
text_layout_cache: &'a TextLayoutCache,
|
||||
font_cache: &'a Arc<FontCache>,
|
||||
max_line_len: usize,
|
||||
max_line_count: usize,
|
||||
) -> Vec<Line> {
|
||||
let mut layouts = Vec::with_capacity(max_line_count);
|
||||
let mut prev_font_properties = style.font_properties.clone();
|
||||
let mut prev_font_id = style.font_id;
|
||||
let mut line = String::new();
|
||||
let mut styles = Vec::new();
|
||||
let mut row = 0;
|
||||
let mut line_exceeded_max_len = false;
|
||||
for (chunk, highlight_style) in chunks.chain([("\n", None)]) {
|
||||
for (ix, mut line_chunk) in chunk.split('\n').enumerate() {
|
||||
if ix > 0 {
|
||||
layouts.push(text_layout_cache.layout_str(&line, style.font_size, &styles));
|
||||
line.clear();
|
||||
styles.clear();
|
||||
row += 1;
|
||||
line_exceeded_max_len = false;
|
||||
if row == max_line_count {
|
||||
return layouts;
|
||||
}
|
||||
}
|
||||
|
||||
if !line_chunk.is_empty() && !line_exceeded_max_len {
|
||||
let highlight_style = highlight_style.unwrap_or(style.clone().into());
|
||||
|
||||
// Avoid a lookup if the font properties match the previous ones.
|
||||
let font_id = if highlight_style.font_properties == prev_font_properties {
|
||||
prev_font_id
|
||||
} else {
|
||||
font_cache
|
||||
.select_font(style.font_family_id, &highlight_style.font_properties)
|
||||
.unwrap_or(style.font_id)
|
||||
};
|
||||
|
||||
if line.len() + line_chunk.len() > max_line_len {
|
||||
let mut chunk_len = max_line_len - line.len();
|
||||
while !line_chunk.is_char_boundary(chunk_len) {
|
||||
chunk_len -= 1;
|
||||
}
|
||||
line_chunk = &line_chunk[..chunk_len];
|
||||
line_exceeded_max_len = true;
|
||||
}
|
||||
|
||||
line.push_str(line_chunk);
|
||||
styles.push((
|
||||
line_chunk.len(),
|
||||
RunStyle {
|
||||
font_id,
|
||||
color: highlight_style.color,
|
||||
underline: highlight_style.underline,
|
||||
},
|
||||
));
|
||||
prev_font_id = font_id;
|
||||
prev_font_properties = highlight_style.font_properties;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
layouts
|
||||
}
|
||||
|
|
|
@ -14,9 +14,15 @@ use std::{cmp, ops::Range, sync::Arc};
|
|||
#[derive(Clone, Default)]
|
||||
pub struct UniformListState(Arc<Mutex<StateInner>>);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ScrollTarget {
|
||||
Show(usize),
|
||||
Center(usize),
|
||||
}
|
||||
|
||||
impl UniformListState {
|
||||
pub fn scroll_to(&self, item_ix: usize) {
|
||||
self.0.lock().scroll_to = Some(item_ix);
|
||||
pub fn scroll_to(&self, scroll_to: ScrollTarget) {
|
||||
self.0.lock().scroll_to = Some(scroll_to);
|
||||
}
|
||||
|
||||
pub fn scroll_top(&self) -> f32 {
|
||||
|
@ -27,7 +33,7 @@ impl UniformListState {
|
|||
#[derive(Default)]
|
||||
struct StateInner {
|
||||
scroll_top: f32,
|
||||
scroll_to: Option<usize>,
|
||||
scroll_to: Option<ScrollTarget>,
|
||||
}
|
||||
|
||||
pub struct LayoutState {
|
||||
|
@ -93,20 +99,38 @@ where
|
|||
fn autoscroll(&mut self, scroll_max: f32, list_height: f32, item_height: f32) {
|
||||
let mut state = self.state.0.lock();
|
||||
|
||||
if state.scroll_top > scroll_max {
|
||||
state.scroll_top = scroll_max;
|
||||
}
|
||||
if let Some(scroll_to) = state.scroll_to.take() {
|
||||
let item_ix;
|
||||
let center;
|
||||
match scroll_to {
|
||||
ScrollTarget::Show(ix) => {
|
||||
item_ix = ix;
|
||||
center = false;
|
||||
}
|
||||
ScrollTarget::Center(ix) => {
|
||||
item_ix = ix;
|
||||
center = true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(item_ix) = state.scroll_to.take() {
|
||||
let item_top = self.padding_top + item_ix as f32 * item_height;
|
||||
let item_bottom = item_top + item_height;
|
||||
|
||||
if item_top < state.scroll_top {
|
||||
state.scroll_top = item_top;
|
||||
} else if item_bottom > (state.scroll_top + list_height) {
|
||||
state.scroll_top = item_bottom - list_height;
|
||||
if center {
|
||||
let item_center = item_top + item_height / 2.;
|
||||
state.scroll_top = (item_center - list_height / 2.).max(0.);
|
||||
} else {
|
||||
let scroll_bottom = state.scroll_top + list_height;
|
||||
if item_top < state.scroll_top {
|
||||
state.scroll_top = item_top;
|
||||
} else if item_bottom > scroll_bottom {
|
||||
state.scroll_top = item_bottom - list_height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if state.scroll_top > scroll_max {
|
||||
state.scroll_top = scroll_max;
|
||||
}
|
||||
}
|
||||
|
||||
fn scroll_top(&self) -> f32 {
|
||||
|
|
|
@ -30,7 +30,7 @@ pub struct TextStyle {
|
|||
pub underline: Option<Color>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default)]
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct HighlightStyle {
|
||||
pub color: Color,
|
||||
pub font_properties: Properties,
|
||||
|
|
|
@ -23,6 +23,7 @@ struct Pending {
|
|||
context: Option<Context>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Keymap(Vec<Binding>);
|
||||
|
||||
pub struct Binding {
|
||||
|
@ -153,24 +154,6 @@ impl Keymap {
|
|||
}
|
||||
}
|
||||
|
||||
pub mod menu {
|
||||
use crate::action;
|
||||
|
||||
action!(SelectPrev);
|
||||
action!(SelectNext);
|
||||
}
|
||||
|
||||
impl Default for Keymap {
|
||||
fn default() -> Self {
|
||||
Self(vec![
|
||||
Binding::new("up", menu::SelectPrev, Some("menu")),
|
||||
Binding::new("ctrl-p", menu::SelectPrev, Some("menu")),
|
||||
Binding::new("down", menu::SelectNext, Some("menu")),
|
||||
Binding::new("ctrl-n", menu::SelectNext, Some("menu")),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl Binding {
|
||||
pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
|
||||
let context = if let Some(context) = context {
|
||||
|
|
|
@ -19,6 +19,7 @@ test-support = [
|
|||
[dependencies]
|
||||
clock = { path = "../clock" }
|
||||
collections = { path = "../collections" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
gpui = { path = "../gpui" }
|
||||
lsp = { path = "../lsp" }
|
||||
rpc = { path = "../rpc" }
|
||||
|
|
|
@ -6,7 +6,8 @@ pub use crate::{
|
|||
};
|
||||
use crate::{
|
||||
diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
|
||||
range_from_lsp,
|
||||
outline::OutlineItem,
|
||||
range_from_lsp, Outline,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use clock::ReplicaId;
|
||||
|
@ -193,7 +194,7 @@ pub trait File {
|
|||
fn as_any(&self) -> &dyn Any;
|
||||
}
|
||||
|
||||
struct QueryCursorHandle(Option<QueryCursor>);
|
||||
pub(crate) struct QueryCursorHandle(Option<QueryCursor>);
|
||||
|
||||
#[derive(Clone)]
|
||||
struct SyntaxTree {
|
||||
|
@ -1264,6 +1265,13 @@ impl Buffer {
|
|||
self.edit_internal(ranges_iter, new_text, true, cx)
|
||||
}
|
||||
|
||||
/*
|
||||
impl Buffer
|
||||
pub fn edit
|
||||
pub fn edit_internal
|
||||
pub fn edit_with_autoindent
|
||||
*/
|
||||
|
||||
pub fn edit_internal<I, S, T>(
|
||||
&mut self,
|
||||
ranges_iter: I,
|
||||
|
@ -1827,6 +1835,110 @@ impl BufferSnapshot {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option<Outline<Anchor>> {
|
||||
let tree = self.tree.as_ref()?;
|
||||
let grammar = self
|
||||
.language
|
||||
.as_ref()
|
||||
.and_then(|language| language.grammar.as_ref())?;
|
||||
|
||||
let mut cursor = QueryCursorHandle::new();
|
||||
let matches = cursor.matches(
|
||||
&grammar.outline_query,
|
||||
tree.root_node(),
|
||||
TextProvider(self.as_rope()),
|
||||
);
|
||||
|
||||
let mut chunks = self.chunks(0..self.len(), theme);
|
||||
|
||||
let item_capture_ix = grammar.outline_query.capture_index_for_name("item")?;
|
||||
let name_capture_ix = grammar.outline_query.capture_index_for_name("name")?;
|
||||
let context_capture_ix = grammar
|
||||
.outline_query
|
||||
.capture_index_for_name("context")
|
||||
.unwrap_or(u32::MAX);
|
||||
|
||||
let mut stack = Vec::<Range<usize>>::new();
|
||||
let items = matches
|
||||
.filter_map(|mat| {
|
||||
let item_node = mat.nodes_for_capture_index(item_capture_ix).next()?;
|
||||
let range = item_node.start_byte()..item_node.end_byte();
|
||||
let mut text = String::new();
|
||||
let mut name_ranges = Vec::new();
|
||||
let mut highlight_ranges = Vec::new();
|
||||
|
||||
for capture in mat.captures {
|
||||
let node_is_name;
|
||||
if capture.index == name_capture_ix {
|
||||
node_is_name = true;
|
||||
} else if capture.index == context_capture_ix {
|
||||
node_is_name = false;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
let range = capture.node.start_byte()..capture.node.end_byte();
|
||||
if !text.is_empty() {
|
||||
text.push(' ');
|
||||
}
|
||||
if node_is_name {
|
||||
let mut start = text.len();
|
||||
let end = start + range.len();
|
||||
|
||||
// When multiple names are captured, then the matcheable text
|
||||
// includes the whitespace in between the names.
|
||||
if !name_ranges.is_empty() {
|
||||
start -= 1;
|
||||
}
|
||||
|
||||
name_ranges.push(start..end);
|
||||
}
|
||||
|
||||
let mut offset = range.start;
|
||||
chunks.seek(offset);
|
||||
while let Some(mut chunk) = chunks.next() {
|
||||
if chunk.text.len() > range.end - offset {
|
||||
chunk.text = &chunk.text[0..(range.end - offset)];
|
||||
offset = range.end;
|
||||
} else {
|
||||
offset += chunk.text.len();
|
||||
}
|
||||
if let Some(style) = chunk.highlight_style {
|
||||
let start = text.len();
|
||||
let end = start + chunk.text.len();
|
||||
highlight_ranges.push((start..end, style));
|
||||
}
|
||||
text.push_str(chunk.text);
|
||||
if offset >= range.end {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while stack.last().map_or(false, |prev_range| {
|
||||
!prev_range.contains(&range.start) || !prev_range.contains(&range.end)
|
||||
}) {
|
||||
stack.pop();
|
||||
}
|
||||
stack.push(range.clone());
|
||||
|
||||
Some(OutlineItem {
|
||||
depth: stack.len() - 1,
|
||||
range: self.anchor_after(range.start)..self.anchor_before(range.end),
|
||||
text,
|
||||
highlight_ranges,
|
||||
name_ranges,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if items.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(Outline::new(items))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enclosing_bracket_ranges<T: ToOffset>(
|
||||
&self,
|
||||
range: Range<T>,
|
||||
|
@ -1854,6 +1966,12 @@ impl BufferSnapshot {
|
|||
.min_by_key(|(open_range, close_range)| close_range.end - open_range.start)
|
||||
}
|
||||
|
||||
/*
|
||||
impl BufferSnapshot
|
||||
pub fn remote_selections_in_range(&self, Range<Anchor>) -> impl Iterator<Item = (ReplicaId, impl Iterator<Item = &Selection<Anchor>>)>
|
||||
pub fn remote_selections_in_range(&self, Range<Anchor>) -> impl Iterator<Item = (ReplicaId, i
|
||||
*/
|
||||
|
||||
pub fn remote_selections_in_range<'a>(
|
||||
&'a self,
|
||||
range: Range<Anchor>,
|
||||
|
@ -2108,7 +2226,7 @@ impl<'a> Iterator for BufferChunks<'a> {
|
|||
}
|
||||
|
||||
impl QueryCursorHandle {
|
||||
fn new() -> Self {
|
||||
pub(crate) fn new() -> Self {
|
||||
QueryCursorHandle(Some(
|
||||
QUERY_CURSORS
|
||||
.lock()
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
mod buffer;
|
||||
mod diagnostic_set;
|
||||
mod highlight_map;
|
||||
mod outline;
|
||||
pub mod proto;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
@ -13,6 +14,7 @@ pub use diagnostic_set::DiagnosticEntry;
|
|||
use gpui::AppContext;
|
||||
use highlight_map::HighlightMap;
|
||||
use lazy_static::lazy_static;
|
||||
pub use outline::{Outline, OutlineItem};
|
||||
use parking_lot::Mutex;
|
||||
use serde::Deserialize;
|
||||
use std::{ops::Range, path::Path, str, sync::Arc};
|
||||
|
@ -74,6 +76,7 @@ pub struct Grammar {
|
|||
pub(crate) highlights_query: Query,
|
||||
pub(crate) brackets_query: Query,
|
||||
pub(crate) indents_query: Query,
|
||||
pub(crate) outline_query: Query,
|
||||
pub(crate) highlight_map: Mutex<HighlightMap>,
|
||||
}
|
||||
|
||||
|
@ -127,6 +130,7 @@ impl Language {
|
|||
brackets_query: Query::new(ts_language, "").unwrap(),
|
||||
highlights_query: Query::new(ts_language, "").unwrap(),
|
||||
indents_query: Query::new(ts_language, "").unwrap(),
|
||||
outline_query: Query::new(ts_language, "").unwrap(),
|
||||
ts_language,
|
||||
highlight_map: Default::default(),
|
||||
})
|
||||
|
@ -164,6 +168,16 @@ impl Language {
|
|||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn with_outline_query(mut self, source: &str) -> Result<Self> {
|
||||
let grammar = self
|
||||
.grammar
|
||||
.as_mut()
|
||||
.and_then(Arc::get_mut)
|
||||
.ok_or_else(|| anyhow!("grammar does not exist or is already being used"))?;
|
||||
grammar.outline_query = Query::new(grammar.ts_language, source)?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
self.config.name.as_str()
|
||||
}
|
||||
|
|
146
crates/language/src/outline.rs
Normal file
146
crates/language/src/outline.rs
Normal file
|
@ -0,0 +1,146 @@
|
|||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{executor::Background, fonts::HighlightStyle};
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Outline<T> {
|
||||
pub items: Vec<OutlineItem<T>>,
|
||||
candidates: Vec<StringMatchCandidate>,
|
||||
path_candidates: Vec<StringMatchCandidate>,
|
||||
path_candidate_prefixes: Vec<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct OutlineItem<T> {
|
||||
pub depth: usize,
|
||||
pub range: Range<T>,
|
||||
pub text: String,
|
||||
pub highlight_ranges: Vec<(Range<usize>, HighlightStyle)>,
|
||||
pub name_ranges: Vec<Range<usize>>,
|
||||
}
|
||||
|
||||
impl<T> Outline<T> {
|
||||
pub fn new(items: Vec<OutlineItem<T>>) -> Self {
|
||||
let mut candidates = Vec::new();
|
||||
let mut path_candidates = Vec::new();
|
||||
let mut path_candidate_prefixes = Vec::new();
|
||||
let mut path_text = String::new();
|
||||
let mut path_stack = Vec::new();
|
||||
|
||||
for (id, item) in items.iter().enumerate() {
|
||||
if item.depth < path_stack.len() {
|
||||
path_stack.truncate(item.depth);
|
||||
path_text.truncate(path_stack.last().copied().unwrap_or(0));
|
||||
}
|
||||
if !path_text.is_empty() {
|
||||
path_text.push(' ');
|
||||
}
|
||||
path_candidate_prefixes.push(path_text.len());
|
||||
path_text.push_str(&item.text);
|
||||
path_stack.push(path_text.len());
|
||||
|
||||
let candidate_text = item
|
||||
.name_ranges
|
||||
.iter()
|
||||
.map(|range| &item.text[range.start as usize..range.end as usize])
|
||||
.collect::<String>();
|
||||
|
||||
path_candidates.push(StringMatchCandidate {
|
||||
id,
|
||||
char_bag: path_text.as_str().into(),
|
||||
string: path_text.clone(),
|
||||
});
|
||||
candidates.push(StringMatchCandidate {
|
||||
id,
|
||||
char_bag: candidate_text.as_str().into(),
|
||||
string: candidate_text,
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
candidates,
|
||||
path_candidates,
|
||||
path_candidate_prefixes,
|
||||
items,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn search(&self, query: &str, executor: Arc<Background>) -> Vec<StringMatch> {
|
||||
let query = query.trim_start();
|
||||
let is_path_query = query.contains(' ');
|
||||
let smart_case = query.chars().any(|c| c.is_uppercase());
|
||||
let mut matches = fuzzy::match_strings(
|
||||
if is_path_query {
|
||||
&self.path_candidates
|
||||
} else {
|
||||
&self.candidates
|
||||
},
|
||||
query,
|
||||
smart_case,
|
||||
100,
|
||||
&Default::default(),
|
||||
executor.clone(),
|
||||
)
|
||||
.await;
|
||||
matches.sort_unstable_by_key(|m| m.candidate_id);
|
||||
|
||||
let mut tree_matches = Vec::new();
|
||||
|
||||
let mut prev_item_ix = 0;
|
||||
for mut string_match in matches {
|
||||
let outline_match = &self.items[string_match.candidate_id];
|
||||
|
||||
if is_path_query {
|
||||
let prefix_len = self.path_candidate_prefixes[string_match.candidate_id];
|
||||
string_match
|
||||
.positions
|
||||
.retain(|position| *position >= prefix_len);
|
||||
for position in &mut string_match.positions {
|
||||
*position -= prefix_len;
|
||||
}
|
||||
} else {
|
||||
let mut name_ranges = outline_match.name_ranges.iter();
|
||||
let mut name_range = name_ranges.next().unwrap();
|
||||
let mut preceding_ranges_len = 0;
|
||||
for position in &mut string_match.positions {
|
||||
while *position >= preceding_ranges_len + name_range.len() as usize {
|
||||
preceding_ranges_len += name_range.len();
|
||||
name_range = name_ranges.next().unwrap();
|
||||
}
|
||||
*position = name_range.start as usize + (*position - preceding_ranges_len);
|
||||
}
|
||||
}
|
||||
|
||||
let insertion_ix = tree_matches.len();
|
||||
let mut cur_depth = outline_match.depth;
|
||||
for (ix, item) in self.items[prev_item_ix..string_match.candidate_id]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
{
|
||||
if cur_depth == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let candidate_index = ix + prev_item_ix;
|
||||
if item.depth == cur_depth - 1 {
|
||||
tree_matches.insert(
|
||||
insertion_ix,
|
||||
StringMatch {
|
||||
candidate_id: candidate_index,
|
||||
score: Default::default(),
|
||||
positions: Default::default(),
|
||||
string: Default::default(),
|
||||
},
|
||||
);
|
||||
cur_depth -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
prev_item_ix = string_match.candidate_id + 1;
|
||||
tree_matches.push(string_match);
|
||||
}
|
||||
|
||||
tree_matches
|
||||
}
|
||||
}
|
|
@ -278,6 +278,139 @@ async fn test_reparse(mut cx: gpui::TestAppContext) {
|
|||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_outline(mut cx: gpui::TestAppContext) {
|
||||
let language = Some(Arc::new(
|
||||
rust_lang()
|
||||
.with_outline_query(
|
||||
r#"
|
||||
(struct_item
|
||||
"struct" @context
|
||||
name: (_) @name) @item
|
||||
(enum_item
|
||||
"enum" @context
|
||||
name: (_) @name) @item
|
||||
(enum_variant
|
||||
name: (_) @name) @item
|
||||
(field_declaration
|
||||
name: (_) @name) @item
|
||||
(impl_item
|
||||
"impl" @context
|
||||
trait: (_) @name
|
||||
"for" @context
|
||||
type: (_) @name) @item
|
||||
(function_item
|
||||
"fn" @context
|
||||
name: (_) @name) @item
|
||||
(mod_item
|
||||
"mod" @context
|
||||
name: (_) @name) @item
|
||||
"#,
|
||||
)
|
||||
.unwrap(),
|
||||
));
|
||||
|
||||
let text = r#"
|
||||
struct Person {
|
||||
name: String,
|
||||
age: usize,
|
||||
}
|
||||
|
||||
mod module {
|
||||
enum LoginState {
|
||||
LoggedOut,
|
||||
LoggingOn,
|
||||
LoggedIn {
|
||||
person: Person,
|
||||
time: Instant,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Person {}
|
||||
|
||||
impl Drop for Person {
|
||||
fn drop(&mut self) {
|
||||
println!("bye");
|
||||
}
|
||||
}
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, None, cx));
|
||||
let outline = buffer
|
||||
.read_with(&cx, |buffer, _| buffer.snapshot().outline(None))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
outline
|
||||
.items
|
||||
.iter()
|
||||
.map(|item| (item.text.as_str(), item.depth))
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
("struct Person", 0),
|
||||
("name", 1),
|
||||
("age", 1),
|
||||
("mod module", 0),
|
||||
("enum LoginState", 1),
|
||||
("LoggedOut", 2),
|
||||
("LoggingOn", 2),
|
||||
("LoggedIn", 2),
|
||||
("person", 3),
|
||||
("time", 3),
|
||||
("impl Eq for Person", 0),
|
||||
("impl Drop for Person", 0),
|
||||
("fn drop", 1),
|
||||
]
|
||||
);
|
||||
|
||||
// Without space, we only match on names
|
||||
assert_eq!(
|
||||
search(&outline, "oon", &cx).await,
|
||||
&[
|
||||
("mod module", vec![]), // included as the parent of a match
|
||||
("enum LoginState", vec![]), // included as the parent of a match
|
||||
("LoggingOn", vec![1, 7, 8]), // matches
|
||||
("impl Drop for Person", vec![7, 18, 19]), // matches in two disjoint names
|
||||
]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
search(&outline, "dp p", &cx).await,
|
||||
&[
|
||||
("impl Drop for Person", vec![5, 8, 9, 14]),
|
||||
("fn drop", vec![]),
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
search(&outline, "dpn", &cx).await,
|
||||
&[("impl Drop for Person", vec![5, 14, 19])]
|
||||
);
|
||||
assert_eq!(
|
||||
search(&outline, "impl ", &cx).await,
|
||||
&[
|
||||
("impl Eq for Person", vec![0, 1, 2, 3, 4]),
|
||||
("impl Drop for Person", vec![0, 1, 2, 3, 4]),
|
||||
("fn drop", vec![]),
|
||||
]
|
||||
);
|
||||
|
||||
async fn search<'a>(
|
||||
outline: &'a Outline<Anchor>,
|
||||
query: &str,
|
||||
cx: &gpui::TestAppContext,
|
||||
) -> Vec<(&'a str, Vec<usize>)> {
|
||||
let matches = cx
|
||||
.read(|cx| outline.search(query, cx.background().clone()))
|
||||
.await;
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|mat| (outline.items[mat.candidate_id].text.as_str(), mat.positions))
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
|
||||
let buffer = cx.add_model(|cx| {
|
||||
|
@ -1017,14 +1150,18 @@ fn rust_lang() -> Language {
|
|||
)
|
||||
.with_indents_query(
|
||||
r#"
|
||||
(call_expression) @indent
|
||||
(field_expression) @indent
|
||||
(_ "(" ")" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
"#,
|
||||
(call_expression) @indent
|
||||
(field_expression) @indent
|
||||
(_ "(" ")" @end) @indent
|
||||
(_ "{" "}" @end) @indent
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
.with_brackets_query(r#" ("{" @open "}" @close) "#)
|
||||
.with_brackets_query(
|
||||
r#"
|
||||
("{" @open "}" @close)
|
||||
"#,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ use std::{
|
|||
io::Write,
|
||||
str::FromStr,
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
|
||||
atomic::{AtomicUsize, Ordering::SeqCst},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
@ -431,7 +431,7 @@ pub struct FakeLanguageServer {
|
|||
buffer: Vec<u8>,
|
||||
stdin: smol::io::BufReader<async_pipe::PipeReader>,
|
||||
stdout: smol::io::BufWriter<async_pipe::PipeWriter>,
|
||||
pub started: Arc<AtomicBool>,
|
||||
pub started: Arc<std::sync::atomic::AtomicBool>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
|
@ -449,7 +449,7 @@ impl LanguageServer {
|
|||
stdin: smol::io::BufReader::new(stdin.1),
|
||||
stdout: smol::io::BufWriter::new(stdout.0),
|
||||
buffer: Vec::new(),
|
||||
started: Arc::new(AtomicBool::new(true)),
|
||||
started: Arc::new(std::sync::atomic::AtomicBool::new(true)),
|
||||
};
|
||||
|
||||
let server = Self::new_internal(stdin.0, stdout.1, Path::new("/"), executor).unwrap();
|
||||
|
|
18
crates/outline/Cargo.toml
Normal file
18
crates/outline/Cargo.toml
Normal file
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "outline"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
path = "src/outline.rs"
|
||||
|
||||
[dependencies]
|
||||
editor = { path = "../editor" }
|
||||
fuzzy = { path = "../fuzzy" }
|
||||
gpui = { path = "../gpui" }
|
||||
language = { path = "../language" }
|
||||
text = { path = "../text" }
|
||||
workspace = { path = "../workspace" }
|
||||
ordered-float = "2.1.1"
|
||||
postage = { version = "0.4", features = ["futures-traits"] }
|
||||
smol = "1.2"
|
540
crates/outline/src/outline.rs
Normal file
540
crates/outline/src/outline.rs
Normal file
|
@ -0,0 +1,540 @@
|
|||
use editor::{
|
||||
display_map::ToDisplayPoint, Anchor, AnchorRangeExt, Autoscroll, Editor, EditorSettings,
|
||||
ToPoint,
|
||||
};
|
||||
use fuzzy::StringMatch;
|
||||
use gpui::{
|
||||
action,
|
||||
elements::*,
|
||||
fonts::{self, HighlightStyle},
|
||||
geometry::vector::Vector2F,
|
||||
keymap::{self, Binding},
|
||||
AppContext, Axis, Entity, MutableAppContext, RenderContext, View, ViewContext, ViewHandle,
|
||||
WeakViewHandle,
|
||||
};
|
||||
use language::{Outline, Selection};
|
||||
use ordered_float::OrderedFloat;
|
||||
use postage::watch;
|
||||
use std::{
|
||||
cmp::{self, Reverse},
|
||||
ops::Range,
|
||||
sync::Arc,
|
||||
};
|
||||
use workspace::{
|
||||
menu::{Confirm, SelectFirst, SelectLast, SelectNext, SelectPrev},
|
||||
Settings, Workspace,
|
||||
};
|
||||
|
||||
action!(Toggle);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_bindings([
|
||||
Binding::new("cmd-shift-O", Toggle, Some("Editor")),
|
||||
Binding::new("escape", Toggle, Some("OutlineView")),
|
||||
]);
|
||||
cx.add_action(OutlineView::toggle);
|
||||
cx.add_action(OutlineView::confirm);
|
||||
cx.add_action(OutlineView::select_prev);
|
||||
cx.add_action(OutlineView::select_next);
|
||||
cx.add_action(OutlineView::select_first);
|
||||
cx.add_action(OutlineView::select_last);
|
||||
}
|
||||
|
||||
struct OutlineView {
|
||||
handle: WeakViewHandle<Self>,
|
||||
active_editor: ViewHandle<Editor>,
|
||||
outline: Outline<Anchor>,
|
||||
selected_match_index: usize,
|
||||
restore_state: Option<RestoreState>,
|
||||
symbol_selection_id: Option<usize>,
|
||||
matches: Vec<StringMatch>,
|
||||
query_editor: ViewHandle<Editor>,
|
||||
list_state: UniformListState,
|
||||
settings: watch::Receiver<Settings>,
|
||||
}
|
||||
|
||||
struct RestoreState {
|
||||
scroll_position: Vector2F,
|
||||
selections: Vec<Selection<usize>>,
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
Dismissed,
|
||||
}
|
||||
|
||||
impl Entity for OutlineView {
|
||||
type Event = Event;
|
||||
|
||||
fn release(&mut self, cx: &mut MutableAppContext) {
|
||||
self.restore_active_editor(cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl View for OutlineView {
|
||||
fn ui_name() -> &'static str {
|
||||
"OutlineView"
|
||||
}
|
||||
|
||||
fn keymap_context(&self, _: &AppContext) -> keymap::Context {
|
||||
let mut cx = Self::default_keymap_context();
|
||||
cx.set.insert("menu".into());
|
||||
cx
|
||||
}
|
||||
|
||||
fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
|
||||
let settings = self.settings.borrow();
|
||||
|
||||
Flex::new(Axis::Vertical)
|
||||
.with_child(
|
||||
Container::new(ChildView::new(self.query_editor.id()).boxed())
|
||||
.with_style(settings.theme.selector.input_editor.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(Flexible::new(1.0, false, self.render_matches()).boxed())
|
||||
.contained()
|
||||
.with_style(settings.theme.selector.container)
|
||||
.constrained()
|
||||
.with_max_width(800.0)
|
||||
.with_max_height(1200.0)
|
||||
.aligned()
|
||||
.top()
|
||||
.named("outline view")
|
||||
}
|
||||
|
||||
fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
|
||||
cx.focus(&self.query_editor);
|
||||
}
|
||||
}
|
||||
|
||||
impl OutlineView {
|
||||
fn new(
|
||||
outline: Outline<Anchor>,
|
||||
editor: ViewHandle<Editor>,
|
||||
settings: watch::Receiver<Settings>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let query_editor = cx.add_view(|cx| {
|
||||
Editor::single_line(
|
||||
{
|
||||
let settings = settings.clone();
|
||||
Arc::new(move |_| {
|
||||
let settings = settings.borrow();
|
||||
EditorSettings {
|
||||
style: settings.theme.selector.input_editor.as_editor(),
|
||||
tab_size: settings.tab_size,
|
||||
soft_wrap: editor::SoftWrap::None,
|
||||
}
|
||||
})
|
||||
},
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.subscribe(&query_editor, Self::on_query_editor_event)
|
||||
.detach();
|
||||
|
||||
let restore_state = editor.update(cx, |editor, cx| {
|
||||
Some(RestoreState {
|
||||
scroll_position: editor.scroll_position(cx),
|
||||
selections: editor.local_selections::<usize>(cx),
|
||||
})
|
||||
});
|
||||
|
||||
let mut this = Self {
|
||||
handle: cx.weak_handle(),
|
||||
active_editor: editor,
|
||||
matches: Default::default(),
|
||||
selected_match_index: 0,
|
||||
restore_state,
|
||||
symbol_selection_id: None,
|
||||
outline,
|
||||
query_editor,
|
||||
list_state: Default::default(),
|
||||
settings,
|
||||
};
|
||||
this.update_matches(cx);
|
||||
this
|
||||
}
|
||||
|
||||
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
|
||||
if let Some(editor) = workspace
|
||||
.active_item(cx)
|
||||
.and_then(|item| item.to_any().downcast::<Editor>())
|
||||
{
|
||||
let settings = workspace.settings();
|
||||
let buffer = editor
|
||||
.read(cx)
|
||||
.buffer()
|
||||
.read(cx)
|
||||
.read(cx)
|
||||
.outline(Some(settings.borrow().theme.editor.syntax.as_ref()));
|
||||
if let Some(outline) = buffer {
|
||||
workspace.toggle_modal(cx, |cx, _| {
|
||||
let view = cx.add_view(|cx| OutlineView::new(outline, editor, settings, cx));
|
||||
cx.subscribe(&view, Self::on_event).detach();
|
||||
view
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
|
||||
if self.selected_match_index > 0 {
|
||||
self.select(self.selected_match_index - 1, true, false, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
|
||||
if self.selected_match_index + 1 < self.matches.len() {
|
||||
self.select(self.selected_match_index + 1, true, false, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
|
||||
self.select(0, true, false, cx);
|
||||
}
|
||||
|
||||
fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
|
||||
self.select(self.matches.len().saturating_sub(1), true, false, cx);
|
||||
}
|
||||
|
||||
fn select(&mut self, index: usize, navigate: bool, center: bool, cx: &mut ViewContext<Self>) {
|
||||
self.selected_match_index = index;
|
||||
self.list_state.scroll_to(if center {
|
||||
ScrollTarget::Center(index)
|
||||
} else {
|
||||
ScrollTarget::Show(index)
|
||||
});
|
||||
if navigate {
|
||||
let selected_match = &self.matches[self.selected_match_index];
|
||||
let outline_item = &self.outline.items[selected_match.candidate_id];
|
||||
self.symbol_selection_id = self.active_editor.update(cx, |active_editor, cx| {
|
||||
let snapshot = active_editor.snapshot(cx).display_snapshot;
|
||||
let buffer_snapshot = &snapshot.buffer_snapshot;
|
||||
let start = outline_item.range.start.to_point(&buffer_snapshot);
|
||||
let end = outline_item.range.end.to_point(&buffer_snapshot);
|
||||
let display_rows = start.to_display_point(&snapshot).row()
|
||||
..end.to_display_point(&snapshot).row() + 1;
|
||||
active_editor.select_ranges([start..start], Some(Autoscroll::Center), cx);
|
||||
active_editor.set_highlighted_rows(Some(display_rows));
|
||||
Some(active_editor.newest_selection::<usize>(&buffer_snapshot).id)
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
||||
self.restore_state.take();
|
||||
cx.emit(Event::Dismissed);
|
||||
}
|
||||
|
||||
fn restore_active_editor(&mut self, cx: &mut MutableAppContext) {
|
||||
let symbol_selection_id = self.symbol_selection_id.take();
|
||||
self.active_editor.update(cx, |editor, cx| {
|
||||
editor.set_highlighted_rows(None);
|
||||
if let Some((symbol_selection_id, restore_state)) =
|
||||
symbol_selection_id.zip(self.restore_state.as_ref())
|
||||
{
|
||||
let newest_selection =
|
||||
editor.newest_selection::<usize>(&editor.buffer().read(cx).read(cx));
|
||||
if symbol_selection_id == newest_selection.id {
|
||||
editor.set_scroll_position(restore_state.scroll_position, cx);
|
||||
editor.update_selections(restore_state.selections.clone(), None, cx);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
workspace: &mut Workspace,
|
||||
_: ViewHandle<Self>,
|
||||
event: &Event,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
match event {
|
||||
Event::Dismissed => workspace.dismiss_modal(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_query_editor_event(
|
||||
&mut self,
|
||||
_: ViewHandle<Editor>,
|
||||
event: &editor::Event,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
editor::Event::Blurred => cx.emit(Event::Dismissed),
|
||||
editor::Event::Edited => self.update_matches(cx),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let selected_index;
|
||||
let navigate_to_selected_index;
|
||||
let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx));
|
||||
if query.is_empty() {
|
||||
self.restore_active_editor(cx);
|
||||
self.matches = self
|
||||
.outline
|
||||
.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, _)| StringMatch {
|
||||
candidate_id: index,
|
||||
score: Default::default(),
|
||||
positions: Default::default(),
|
||||
string: Default::default(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let editor = self.active_editor.read(cx);
|
||||
let buffer = editor.buffer().read(cx).read(cx);
|
||||
let cursor_offset = editor.newest_selection::<usize>(&buffer).head();
|
||||
selected_index = self
|
||||
.outline
|
||||
.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, item)| {
|
||||
let range = item.range.to_offset(&buffer);
|
||||
let distance_to_closest_endpoint = cmp::min(
|
||||
(range.start as isize - cursor_offset as isize).abs() as usize,
|
||||
(range.end as isize - cursor_offset as isize).abs() as usize,
|
||||
);
|
||||
let depth = if range.contains(&cursor_offset) {
|
||||
Some(item.depth)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(ix, depth, distance_to_closest_endpoint)
|
||||
})
|
||||
.max_by_key(|(_, depth, distance)| (*depth, Reverse(*distance)))
|
||||
.unwrap()
|
||||
.0;
|
||||
navigate_to_selected_index = false;
|
||||
} else {
|
||||
self.matches = smol::block_on(self.outline.search(&query, cx.background().clone()));
|
||||
selected_index = self
|
||||
.matches
|
||||
.iter()
|
||||
.enumerate()
|
||||
.max_by_key(|(_, m)| OrderedFloat(m.score))
|
||||
.map(|(ix, _)| ix)
|
||||
.unwrap_or(0);
|
||||
navigate_to_selected_index = !self.matches.is_empty();
|
||||
}
|
||||
self.select(selected_index, navigate_to_selected_index, true, cx);
|
||||
}
|
||||
|
||||
fn render_matches(&self) -> ElementBox {
|
||||
if self.matches.is_empty() {
|
||||
let settings = self.settings.borrow();
|
||||
return Container::new(
|
||||
Label::new(
|
||||
"No matches".into(),
|
||||
settings.theme.selector.empty.label.clone(),
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_style(settings.theme.selector.empty.container)
|
||||
.named("empty matches");
|
||||
}
|
||||
|
||||
let handle = self.handle.clone();
|
||||
let list = UniformList::new(
|
||||
self.list_state.clone(),
|
||||
self.matches.len(),
|
||||
move |mut range, items, cx| {
|
||||
let cx = cx.as_ref();
|
||||
let view = handle.upgrade(cx).unwrap();
|
||||
let view = view.read(cx);
|
||||
let start = range.start;
|
||||
range.end = cmp::min(range.end, view.matches.len());
|
||||
items.extend(
|
||||
view.matches[range]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(move |(ix, m)| view.render_match(m, start + ix)),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Container::new(list.boxed())
|
||||
.with_margin_top(6.0)
|
||||
.named("matches")
|
||||
}
|
||||
|
||||
fn render_match(&self, string_match: &StringMatch, index: usize) -> ElementBox {
|
||||
let settings = self.settings.borrow();
|
||||
let style = if index == self.selected_match_index {
|
||||
&settings.theme.selector.active_item
|
||||
} else {
|
||||
&settings.theme.selector.item
|
||||
};
|
||||
let outline_item = &self.outline.items[string_match.candidate_id];
|
||||
|
||||
Text::new(outline_item.text.clone(), style.label.text.clone())
|
||||
.with_soft_wrap(false)
|
||||
.with_highlights(combine_syntax_and_fuzzy_match_highlights(
|
||||
&outline_item.text,
|
||||
style.label.text.clone().into(),
|
||||
&outline_item.highlight_ranges,
|
||||
&string_match.positions,
|
||||
))
|
||||
.contained()
|
||||
.with_padding_left(20. * outline_item.depth as f32)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
fn combine_syntax_and_fuzzy_match_highlights(
|
||||
text: &str,
|
||||
default_style: HighlightStyle,
|
||||
syntax_ranges: &[(Range<usize>, HighlightStyle)],
|
||||
match_indices: &[usize],
|
||||
) -> Vec<(Range<usize>, HighlightStyle)> {
|
||||
let mut result = Vec::new();
|
||||
let mut match_indices = match_indices.iter().copied().peekable();
|
||||
|
||||
for (range, mut syntax_highlight) in syntax_ranges
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain([(usize::MAX..0, Default::default())])
|
||||
{
|
||||
syntax_highlight.font_properties.weight(Default::default());
|
||||
|
||||
// Add highlights for any fuzzy match characters before the next
|
||||
// syntax highlight range.
|
||||
while let Some(&match_index) = match_indices.peek() {
|
||||
if match_index >= range.start {
|
||||
break;
|
||||
}
|
||||
match_indices.next();
|
||||
let end_index = char_ix_after(match_index, text);
|
||||
let mut match_style = default_style;
|
||||
match_style.font_properties.weight(fonts::Weight::BOLD);
|
||||
result.push((match_index..end_index, match_style));
|
||||
}
|
||||
|
||||
if range.start == usize::MAX {
|
||||
break;
|
||||
}
|
||||
|
||||
// Add highlights for any fuzzy match characters within the
|
||||
// syntax highlight range.
|
||||
let mut offset = range.start;
|
||||
while let Some(&match_index) = match_indices.peek() {
|
||||
if match_index >= range.end {
|
||||
break;
|
||||
}
|
||||
|
||||
match_indices.next();
|
||||
if match_index > offset {
|
||||
result.push((offset..match_index, syntax_highlight));
|
||||
}
|
||||
|
||||
let mut end_index = char_ix_after(match_index, text);
|
||||
while let Some(&next_match_index) = match_indices.peek() {
|
||||
if next_match_index == end_index && next_match_index < range.end {
|
||||
end_index = char_ix_after(next_match_index, text);
|
||||
match_indices.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let mut match_style = syntax_highlight;
|
||||
match_style.font_properties.weight(fonts::Weight::BOLD);
|
||||
result.push((match_index..end_index, match_style));
|
||||
offset = end_index;
|
||||
}
|
||||
|
||||
if offset < range.end {
|
||||
result.push((offset..range.end, syntax_highlight));
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn char_ix_after(ix: usize, text: &str) -> usize {
|
||||
ix + text[ix..].chars().next().unwrap().len_utf8()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::{color::Color, fonts::HighlightStyle};
|
||||
|
||||
#[test]
|
||||
fn test_combine_syntax_and_fuzzy_match_highlights() {
|
||||
let string = "abcdefghijklmnop";
|
||||
let default = HighlightStyle::default();
|
||||
let syntax_ranges = [
|
||||
(
|
||||
0..3,
|
||||
HighlightStyle {
|
||||
color: Color::red(),
|
||||
..default
|
||||
},
|
||||
),
|
||||
(
|
||||
4..8,
|
||||
HighlightStyle {
|
||||
color: Color::green(),
|
||||
..default
|
||||
},
|
||||
),
|
||||
];
|
||||
let match_indices = [4, 6, 7, 8];
|
||||
assert_eq!(
|
||||
combine_syntax_and_fuzzy_match_highlights(
|
||||
&string,
|
||||
default,
|
||||
&syntax_ranges,
|
||||
&match_indices,
|
||||
),
|
||||
&[
|
||||
(
|
||||
0..3,
|
||||
HighlightStyle {
|
||||
color: Color::red(),
|
||||
..default
|
||||
},
|
||||
),
|
||||
(
|
||||
4..5,
|
||||
HighlightStyle {
|
||||
color: Color::green(),
|
||||
font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD),
|
||||
..default
|
||||
},
|
||||
),
|
||||
(
|
||||
5..6,
|
||||
HighlightStyle {
|
||||
color: Color::green(),
|
||||
..default
|
||||
},
|
||||
),
|
||||
(
|
||||
6..8,
|
||||
HighlightStyle {
|
||||
color: Color::green(),
|
||||
font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD),
|
||||
..default
|
||||
},
|
||||
),
|
||||
(
|
||||
8..9,
|
||||
HighlightStyle {
|
||||
font_properties: *fonts::Properties::default().weight(fonts::Weight::BOLD),
|
||||
..default
|
||||
},
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,14 +1,10 @@
|
|||
use gpui::{
|
||||
action,
|
||||
elements::{
|
||||
Align, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement, Svg,
|
||||
UniformList, UniformListState,
|
||||
},
|
||||
keymap::{
|
||||
self,
|
||||
menu::{SelectNext, SelectPrev},
|
||||
Binding,
|
||||
Align, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement, ScrollTarget,
|
||||
Svg, UniformList, UniformListState,
|
||||
},
|
||||
keymap::{self, Binding},
|
||||
platform::CursorStyle,
|
||||
AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, ReadModel, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle,
|
||||
|
@ -20,7 +16,10 @@ use std::{
|
|||
ffi::OsStr,
|
||||
ops::Range,
|
||||
};
|
||||
use workspace::{Settings, Workspace};
|
||||
use workspace::{
|
||||
menu::{SelectNext, SelectPrev},
|
||||
Settings, Workspace,
|
||||
};
|
||||
|
||||
pub struct ProjectPanel {
|
||||
project: ModelHandle<Project>,
|
||||
|
@ -278,7 +277,7 @@ impl ProjectPanel {
|
|||
|
||||
fn autoscroll(&mut self) {
|
||||
if let Some(selection) = self.selection {
|
||||
self.list.scroll_to(selection.index);
|
||||
self.list.scroll_to(ScrollTarget::Show(selection.index));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
|
|||
use gpui::{
|
||||
action,
|
||||
elements::*,
|
||||
keymap::{self, menu, Binding},
|
||||
keymap::{self, Binding},
|
||||
AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, View,
|
||||
ViewContext, ViewHandle,
|
||||
};
|
||||
|
@ -11,7 +11,10 @@ use parking_lot::Mutex;
|
|||
use postage::watch;
|
||||
use std::{cmp, sync::Arc};
|
||||
use theme::ThemeRegistry;
|
||||
use workspace::{AppState, Settings, Workspace};
|
||||
use workspace::{
|
||||
menu::{Confirm, SelectNext, SelectPrev},
|
||||
AppState, Settings, Workspace,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ThemeSelectorParams {
|
||||
|
@ -30,7 +33,6 @@ pub struct ThemeSelector {
|
|||
selected_index: usize,
|
||||
}
|
||||
|
||||
action!(Confirm);
|
||||
action!(Toggle, ThemeSelectorParams);
|
||||
action!(Reload, ThemeSelectorParams);
|
||||
|
||||
|
@ -45,7 +47,6 @@ pub fn init(params: ThemeSelectorParams, cx: &mut MutableAppContext) {
|
|||
Binding::new("cmd-k cmd-t", Toggle(params.clone()), None),
|
||||
Binding::new("cmd-k t", Reload(params.clone()), None),
|
||||
Binding::new("escape", Toggle(params.clone()), Some("ThemeSelector")),
|
||||
Binding::new("enter", Confirm, Some("ThemeSelector")),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -136,19 +137,21 @@ impl ThemeSelector {
|
|||
}
|
||||
}
|
||||
|
||||
fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
|
||||
fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
|
||||
if self.selected_index > 0 {
|
||||
self.selected_index -= 1;
|
||||
}
|
||||
self.list_state.scroll_to(self.selected_index);
|
||||
self.list_state
|
||||
.scroll_to(ScrollTarget::Show(self.selected_index));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
|
||||
fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
|
||||
if self.selected_index + 1 < self.matches.len() {
|
||||
self.selected_index += 1;
|
||||
}
|
||||
self.list_state.scroll_to(self.selected_index);
|
||||
self.list_state
|
||||
.scroll_to(ScrollTarget::Show(self.selected_index));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
@ -157,7 +160,9 @@ impl ThemeSelector {
|
|||
let candidates = self
|
||||
.themes
|
||||
.list()
|
||||
.map(|name| StringMatchCandidate {
|
||||
.enumerate()
|
||||
.map(|(id, name)| StringMatchCandidate {
|
||||
id,
|
||||
char_bag: name.as_str().into(),
|
||||
string: name,
|
||||
})
|
||||
|
@ -167,7 +172,9 @@ impl ThemeSelector {
|
|||
self.matches = if query.is_empty() {
|
||||
candidates
|
||||
.into_iter()
|
||||
.map(|candidate| StringMatch {
|
||||
.enumerate()
|
||||
.map(|(index, candidate)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: candidate.string,
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
|
|
19
crates/workspace/src/menu.rs
Normal file
19
crates/workspace/src/menu.rs
Normal file
|
@ -0,0 +1,19 @@
|
|||
use gpui::{action, keymap::Binding, MutableAppContext};
|
||||
|
||||
action!(Confirm);
|
||||
action!(SelectPrev);
|
||||
action!(SelectNext);
|
||||
action!(SelectFirst);
|
||||
action!(SelectLast);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_bindings([
|
||||
Binding::new("up", SelectPrev, Some("menu")),
|
||||
Binding::new("ctrl-p", SelectPrev, Some("menu")),
|
||||
Binding::new("down", SelectNext, Some("menu")),
|
||||
Binding::new("ctrl-n", SelectNext, Some("menu")),
|
||||
Binding::new("cmd-up", SelectFirst, Some("menu")),
|
||||
Binding::new("cmd-down", SelectLast, Some("menu")),
|
||||
Binding::new("enter", Confirm, Some("menu")),
|
||||
]);
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
pub mod menu;
|
||||
pub mod pane;
|
||||
pub mod pane_group;
|
||||
pub mod settings;
|
||||
|
@ -48,6 +49,9 @@ action!(Save);
|
|||
action!(DebugElements);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
pane::init(cx);
|
||||
menu::init(cx);
|
||||
|
||||
cx.add_global_action(open);
|
||||
cx.add_global_action(move |action: &OpenPaths, cx: &mut MutableAppContext| {
|
||||
open_paths(&action.0.paths, &action.0.app_state, cx).detach();
|
||||
|
@ -84,7 +88,6 @@ pub fn init(cx: &mut MutableAppContext) {
|
|||
None,
|
||||
),
|
||||
]);
|
||||
pane::init(cx);
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
|
|
|
@ -43,6 +43,7 @@ gpui = { path = "../gpui" }
|
|||
journal = { path = "../journal" }
|
||||
language = { path = "../language" }
|
||||
lsp = { path = "../lsp" }
|
||||
outline = { path = "../outline" }
|
||||
project = { path = "../project" }
|
||||
project_panel = { path = "../project_panel" }
|
||||
rpc = { path = "../rpc" }
|
||||
|
|
|
@ -211,7 +211,7 @@ text = { extends = "$text.0" }
|
|||
[selector]
|
||||
background = "$surface.0"
|
||||
padding = 8
|
||||
margin.top = 52
|
||||
margin = { top = 52, bottom = 52 }
|
||||
corner_radius = 6
|
||||
shadow = { offset = [0, 2], blur = 16, color = "$shadow.0" }
|
||||
border = { width = 1, color = "$border.0" }
|
||||
|
|
63
crates/zed/languages/rust/outline.scm
Normal file
63
crates/zed/languages/rust/outline.scm
Normal file
|
@ -0,0 +1,63 @@
|
|||
(struct_item
|
||||
(visibility_modifier)? @context
|
||||
"struct" @context
|
||||
name: (_) @name) @item
|
||||
|
||||
(enum_item
|
||||
(visibility_modifier)? @context
|
||||
"enum" @context
|
||||
name: (_) @name) @item
|
||||
|
||||
(enum_variant
|
||||
(visibility_modifier)? @context
|
||||
name: (_) @name) @item
|
||||
|
||||
(impl_item
|
||||
"impl" @context
|
||||
trait: (_)? @name
|
||||
"for"? @context
|
||||
type: (_) @name) @item
|
||||
|
||||
(trait_item
|
||||
(visibility_modifier)? @context
|
||||
"trait" @context
|
||||
name: (_) @name) @item
|
||||
|
||||
(function_item
|
||||
(visibility_modifier)? @context
|
||||
(function_modifiers)? @context
|
||||
"fn" @context
|
||||
name: (_) @name) @item
|
||||
|
||||
(function_signature_item
|
||||
(visibility_modifier)? @context
|
||||
(function_modifiers)? @context
|
||||
"fn" @context
|
||||
name: (_) @name) @item
|
||||
|
||||
(macro_definition
|
||||
. "macro_rules!" @context
|
||||
name: (_) @name) @item
|
||||
|
||||
(mod_item
|
||||
(visibility_modifier)? @context
|
||||
"mod" @context
|
||||
name: (_) @name) @item
|
||||
|
||||
(type_item
|
||||
(visibility_modifier)? @context
|
||||
"type" @context
|
||||
name: (_) @name) @item
|
||||
|
||||
(associated_type
|
||||
"type" @context
|
||||
name: (_) @name) @item
|
||||
|
||||
(const_item
|
||||
(visibility_modifier)? @context
|
||||
"const" @context
|
||||
name: (_) @name) @item
|
||||
|
||||
(field_declaration
|
||||
(visibility_modifier)? @context
|
||||
name: (_) @name) @item
|
|
@ -24,6 +24,8 @@ fn rust() -> Language {
|
|||
.unwrap()
|
||||
.with_indents_query(load_query("rust/indents.scm").as_ref())
|
||||
.unwrap()
|
||||
.with_outline_query(load_query("rust/outline.scm").as_ref())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn markdown() -> Language {
|
||||
|
|
|
@ -59,6 +59,7 @@ fn main() {
|
|||
go_to_line::init(cx);
|
||||
file_finder::init(cx);
|
||||
chat_panel::init(cx);
|
||||
outline::init(cx);
|
||||
project_panel::init(cx);
|
||||
diagnostics::init(cx);
|
||||
cx.spawn({
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue