Merge pull request #342 from zed-industries/symbolic-nav

Introduce outline view
This commit is contained in:
Max Brunsfeld 2022-01-14 12:02:43 -08:00 committed by GitHub
commit 485554cd0c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 1457 additions and 235 deletions

17
Cargo.lock generated
View file

@ -2602,6 +2602,7 @@ dependencies = [
"ctor", "ctor",
"env_logger", "env_logger",
"futures", "futures",
"fuzzy",
"gpui", "gpui",
"lazy_static", "lazy_static",
"log", "log",
@ -3121,6 +3122,21 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85" checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85"
[[package]]
name = "outline"
version = "0.1.0"
dependencies = [
"editor",
"fuzzy",
"gpui",
"language",
"ordered-float",
"postage",
"smol",
"text",
"workspace",
]
[[package]] [[package]]
name = "p256" name = "p256"
version = "0.9.0" version = "0.9.0"
@ -5724,6 +5740,7 @@ dependencies = [
"log-panics", "log-panics",
"lsp", "lsp",
"num_cpus", "num_cpus",
"outline",
"parking_lot", "parking_lot",
"postage", "postage",
"project", "project",

View file

@ -28,8 +28,10 @@ use language::{
BracketPair, Buffer, Diagnostic, DiagnosticSeverity, Language, Point, Selection, SelectionGoal, BracketPair, Buffer, Diagnostic, DiagnosticSeverity, Language, Point, Selection, SelectionGoal,
TransactionId, TransactionId,
}; };
pub use multi_buffer::{Anchor, ExcerptId, ExcerptProperties, MultiBuffer, ToOffset, ToPoint}; pub use multi_buffer::{
use multi_buffer::{AnchorRangeExt, MultiBufferChunks, MultiBufferSnapshot}; Anchor, AnchorRangeExt, ExcerptId, ExcerptProperties, MultiBuffer, ToOffset, ToPoint,
};
use multi_buffer::{MultiBufferChunks, MultiBufferSnapshot};
use postage::watch; use postage::watch;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use smallvec::SmallVec; use smallvec::SmallVec;
@ -374,7 +376,7 @@ pub struct Editor {
blinking_paused: bool, blinking_paused: bool,
mode: EditorMode, mode: EditorMode,
placeholder_text: Option<Arc<str>>, placeholder_text: Option<Arc<str>>,
highlighted_row: Option<u32>, highlighted_rows: Option<Range<u32>>,
} }
pub struct EditorSnapshot { pub struct EditorSnapshot {
@ -503,7 +505,7 @@ impl Editor {
blinking_paused: false, blinking_paused: false,
mode: EditorMode::Full, mode: EditorMode::Full,
placeholder_text: None, placeholder_text: None,
highlighted_row: None, highlighted_rows: None,
}; };
let selection = Selection { let selection = Selection {
id: post_inc(&mut this.next_selection_id), 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>) { 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 { let selection = Selection {
id: post_inc(&mut self.next_selection_id), id: post_inc(&mut self.next_selection_id),
start: 0, start: 0,
@ -2405,6 +2412,11 @@ impl Editor {
} }
pub fn move_to_end(&mut self, _: &MoveToEnd, cx: &mut ViewContext<Self>) { 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 cursor = self.buffer.read(cx).read(cx).len();
let selection = Selection { let selection = Selection {
id: post_inc(&mut self.next_selection_id), id: post_inc(&mut self.next_selection_id),
@ -3544,12 +3556,12 @@ impl Editor {
.update(cx, |map, cx| map.set_wrap_width(width, cx)) .update(cx, |map, cx| map.set_wrap_width(width, cx))
} }
pub fn set_highlighted_row(&mut self, row: Option<u32>) { pub fn set_highlighted_rows(&mut self, rows: Option<Range<u32>>) {
self.highlighted_row = row; self.highlighted_rows = rows;
} }
pub fn highlighted_row(&mut self) -> Option<u32> { pub fn highlighted_rows(&self) -> Option<Range<u32>> {
self.highlighted_row self.highlighted_rows.clone()
} }
fn next_blink_epoch(&mut self) -> usize { fn next_blink_epoch(&mut self) -> usize {

View file

@ -7,6 +7,8 @@ use clock::ReplicaId;
use collections::{BTreeMap, HashMap}; use collections::{BTreeMap, HashMap};
use gpui::{ use gpui::{
color::Color, color::Color,
elements::layout_highlighted_chunks,
fonts::HighlightStyle,
geometry::{ geometry::{
rect::RectF, rect::RectF,
vector::{vec2f, Vector2F}, vector::{vec2f, Vector2F},
@ -19,7 +21,7 @@ use gpui::{
MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle, MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle,
}; };
use json::json; use json::json;
use language::{Bias, Chunk}; use language::Bias;
use smallvec::SmallVec; use smallvec::SmallVec;
use std::{ use std::{
cmp::{self, Ordering}, 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( let origin = vec2f(
bounds.origin_x(), 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 { cx.scene.push_quad(Quad {
bounds: RectF::new(origin, size), bounds: RectF::new(origin, size),
background: Some(style.highlighted_line_background), background: Some(style.highlighted_line_background),
@ -537,86 +543,37 @@ impl EditorElement {
) )
}) })
.collect(); .collect();
} } else {
let style = &self.settings.style;
let style = &self.settings.style; let chunks = snapshot
let mut prev_font_properties = style.text.font_properties.clone(); .chunks(rows.clone(), Some(&style.syntax))
let mut prev_font_id = style.text.font_id; .map(|chunk| {
let highlight = if let Some(severity) = chunk.diagnostic {
let mut layouts = Vec::with_capacity(rows.len()); let underline = Some(super::diagnostic_style(severity, true, style).text);
let mut line = String::new(); if let Some(mut highlight) = chunk.highlight_style {
let mut styles = Vec::new(); highlight.underline = underline;
let mut row = rows.start; Some(highlight)
let mut line_exceeded_max_len = false; } else {
let chunks = snapshot.chunks(rows.clone(), Some(&style.syntax)); Some(HighlightStyle {
underline,
let newline_chunk = Chunk { color: style.text.color,
text: "\n", font_properties: style.text.font_properties,
..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;
} }
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 { } else {
highlight_style.underline chunk.highlight_style
}; };
(chunk.text, highlight)
line.push_str(line_chunk); });
styles.push(( layout_highlighted_chunks(
line_chunk.len(), chunks,
RunStyle { &style.text,
font_id, &cx.text_layout_cache,
color: highlight_style.color, &cx.font_cache,
underline, MAX_LINE_LEN,
}, rows.len() as usize,
)); )
prev_font_id = font_id;
prev_font_properties = highlight_style.font_properties;
}
}
} }
layouts
} }
fn layout_blocks( fn layout_blocks(
@ -640,15 +597,20 @@ impl EditorElement {
.to_display_point(snapshot) .to_display_point(snapshot)
.row(); .row();
let anchor_x = text_x + if rows.contains(&anchor_row) { let anchor_x = text_x
line_layouts[(anchor_row - rows.start) as usize] + if rows.contains(&anchor_row) {
.x_for_index(block.column() as usize) line_layouts[(anchor_row - rows.start) as usize]
} else { .x_for_index(block.column() as usize)
layout_line(anchor_row, snapshot, style, cx.text_layout_cache) } else {
.x_for_index(block.column() as usize) 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( element.layout(
SizeConstraint { SizeConstraint {
min: Vector2F::zero(), min: Vector2F::zero(),
@ -750,9 +712,9 @@ impl Element for EditorElement {
let mut selections = HashMap::default(); let mut selections = HashMap::default();
let mut active_rows = BTreeMap::new(); let mut active_rows = BTreeMap::new();
let mut highlighted_row = None; let mut highlighted_rows = None;
self.update_view(cx.app, |view, cx| { 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 display_map = view.display_map.update(cx, |map, cx| map.snapshot(cx));
let local_selections = view let local_selections = view
@ -831,7 +793,7 @@ impl Element for EditorElement {
snapshot, snapshot,
style: self.settings.style.clone(), style: self.settings.style.clone(),
active_rows, active_rows,
highlighted_row, highlighted_rows,
line_layouts, line_layouts,
line_number_layouts, line_number_layouts,
blocks, blocks,
@ -962,7 +924,7 @@ pub struct LayoutState {
style: EditorStyle, style: EditorStyle,
snapshot: EditorSnapshot, snapshot: EditorSnapshot,
active_rows: BTreeMap<u32, bool>, active_rows: BTreeMap<u32, bool>,
highlighted_row: Option<u32>, highlighted_rows: Option<Range<u32>>,
line_layouts: Vec<text_layout::Line>, line_layouts: Vec<text_layout::Line>,
line_number_layouts: Vec<Option<text_layout::Line>>, line_number_layouts: Vec<Option<text_layout::Line>>,
blocks: Vec<(u32, ElementBox)>, blocks: Vec<(u32, ElementBox)>,

View file

@ -6,8 +6,8 @@ use clock::ReplicaId;
use collections::{HashMap, HashSet}; use collections::{HashMap, HashSet};
use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
use language::{ use language::{
Buffer, BufferChunks, BufferSnapshot, Chunk, DiagnosticEntry, Event, File, Language, Selection, Buffer, BufferChunks, BufferSnapshot, Chunk, DiagnosticEntry, Event, File, Language, Outline,
ToOffset as _, ToPoint as _, TransactionId, OutlineItem, Selection, ToOffset as _, ToPoint as _, TransactionId,
}; };
use std::{ use std::{
cell::{Ref, RefCell}, 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>( fn buffer_snapshot_for_excerpt<'a>(
&'a self, &'a self,
excerpt_id: &'a ExcerptId, excerpt_id: &'a ExcerptId,

View file

@ -3,11 +3,7 @@ use fuzzy::PathMatch;
use gpui::{ use gpui::{
action, action,
elements::*, elements::*,
keymap::{ keymap::{self, Binding},
self,
menu::{SelectNext, SelectPrev},
Binding,
},
AppContext, Axis, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View, AppContext, Axis, Entity, ModelHandle, MutableAppContext, RenderContext, Task, View,
ViewContext, ViewHandle, WeakViewHandle, ViewContext, ViewHandle, WeakViewHandle,
}; };
@ -22,7 +18,10 @@ use std::{
}, },
}; };
use util::post_inc; use util::post_inc;
use workspace::{Settings, Workspace}; use workspace::{
menu::{Confirm, SelectNext, SelectPrev},
Settings, Workspace,
};
pub struct FileFinder { pub struct FileFinder {
handle: WeakViewHandle<Self>, handle: WeakViewHandle<Self>,
@ -40,7 +39,6 @@ pub struct FileFinder {
} }
action!(Toggle); action!(Toggle);
action!(Confirm);
action!(Select, ProjectPath); action!(Select, ProjectPath);
pub fn init(cx: &mut MutableAppContext) { pub fn init(cx: &mut MutableAppContext) {
@ -53,7 +51,6 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_bindings(vec![ cx.add_bindings(vec![
Binding::new("cmd-p", Toggle, None), Binding::new("cmd-p", Toggle, None),
Binding::new("escape", Toggle, Some("FileFinder")), Binding::new("escape", Toggle, Some("FileFinder")),
Binding::new("enter", Confirm, Some("FileFinder")),
]); ]);
} }
@ -353,7 +350,8 @@ impl FileFinder {
let mat = &self.matches[selected_index]; let mat = &self.matches[selected_index];
self.selected = Some((mat.worktree_id, mat.path.clone())); 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(); cx.notify();
} }
@ -364,7 +362,8 @@ impl FileFinder {
let mat = &self.matches[selected_index]; let mat = &self.matches[selected_index];
self.selected = Some((mat.worktree_id, mat.path.clone())); 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(); cx.notify();
} }
@ -415,7 +414,8 @@ impl FileFinder {
} }
self.latest_search_query = query; self.latest_search_query = query;
self.latest_search_did_cancel = did_cancel; 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(); cx.notify();
} }
} }

View file

@ -9,6 +9,7 @@ impl CharBag {
} }
fn insert(&mut self, c: char) { fn insert(&mut self, c: char) {
let c = c.to_ascii_lowercase();
if c >= 'a' && c <= 'z' { if c >= 'a' && c <= 'z' {
let mut count = self.0; let mut count = self.0;
let idx = c as u8 - 'a' as u8; let idx = c as u8 - 'a' as u8;

View file

@ -55,6 +55,7 @@ pub struct PathMatch {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct StringMatchCandidate { pub struct StringMatchCandidate {
pub id: usize,
pub string: String, pub string: String,
pub char_bag: CharBag, pub char_bag: CharBag,
} }
@ -109,6 +110,7 @@ impl<'a> MatchCandidate for &'a StringMatchCandidate {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct StringMatch { pub struct StringMatch {
pub candidate_id: usize,
pub score: f64, pub score: f64,
pub positions: Vec<usize>, pub positions: Vec<usize>,
pub string: String, pub string: String,
@ -116,7 +118,7 @@ pub struct StringMatch {
impl PartialEq for StringMatch { impl PartialEq for StringMatch {
fn eq(&self, other: &Self) -> bool { 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 self.score
.partial_cmp(&other.score) .partial_cmp(&other.score)
.unwrap_or(Ordering::Equal) .unwrap_or(Ordering::Equal)
.then_with(|| self.string.cmp(&other.string)) .then_with(|| self.candidate_id.cmp(&other.candidate_id))
} }
} }
impl PartialEq for PathMatch { impl PartialEq for PathMatch {
fn eq(&self, other: &Self) -> bool { 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() { for (segment_idx, results) in segment_results.iter_mut().enumerate() {
let cancel_flag = &cancel_flag; let cancel_flag = &cancel_flag;
scope.spawn(async move { scope.spawn(async move {
let segment_start = segment_idx * segment_size; let segment_start = cmp::min(segment_idx * segment_size, candidates.len());
let segment_end = segment_start + segment_size; let segment_end = cmp::min(segment_start + segment_size, candidates.len());
let mut matcher = Matcher::new( let mut matcher = Matcher::new(
query, query,
lowercase_query, lowercase_query,
@ -330,6 +332,7 @@ impl<'a> Matcher<'a> {
results, results,
cancel_flag, cancel_flag,
|candidate, score| StringMatch { |candidate, score| StringMatch {
candidate_id: candidate.id,
score, score,
positions: Vec::new(), positions: Vec::new(),
string: candidate.string.to_string(), string: candidate.string.to_string(),
@ -433,13 +436,17 @@ impl<'a> Matcher<'a> {
} }
} }
fn find_last_positions(&mut self, prefix: &[char], path: &[char]) -> bool { fn find_last_positions(
let mut path = path.iter(); &mut self,
let mut prefix_iter = prefix.iter(); lowercase_prefix: &[char],
for (i, char) in self.query.iter().enumerate().rev() { lowercase_candidate: &[char],
if let Some(j) = path.rposition(|c| c == char) { ) -> bool {
self.last_positions[i] = j + prefix.len(); let mut lowercase_prefix = lowercase_prefix.iter();
} else if let Some(j) = prefix_iter.rposition(|c| c == char) { 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; self.last_positions[i] = j;
} else { } else {
return false; return false;

View file

@ -26,7 +26,7 @@ pub struct GoToLine {
line_editor: ViewHandle<Editor>, line_editor: ViewHandle<Editor>,
active_editor: ViewHandle<Editor>, active_editor: ViewHandle<Editor>,
restore_state: Option<RestoreState>, restore_state: Option<RestoreState>,
line_selection: Option<Selection<usize>>, line_selection_id: Option<usize>,
cursor_point: Point, cursor_point: Point,
max_point: Point, max_point: Point,
} }
@ -84,7 +84,7 @@ impl GoToLine {
line_editor, line_editor,
active_editor, active_editor,
restore_state, restore_state,
line_selection: None, line_selection_id: None,
cursor_point, cursor_point,
max_point, max_point,
} }
@ -139,13 +139,18 @@ impl GoToLine {
column.map(|column| column.saturating_sub(1)).unwrap_or(0), 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 snapshot = active_editor.snapshot(cx).display_snapshot;
let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left); let point = snapshot.buffer_snapshot.clip_point(point, Bias::Left);
let display_point = point.to_display_point(&snapshot); 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.select_ranges([point..point], Some(Autoscroll::Center), cx);
active_editor.set_highlighted_row(Some(display_point.row())); active_editor.set_highlighted_rows(Some(row..row + 1));
Some(active_editor.newest_selection(&snapshot.buffer_snapshot)) Some(
active_editor
.newest_selection::<usize>(&snapshot.buffer_snapshot)
.id,
)
}); });
cx.notify(); cx.notify();
} }
@ -159,14 +164,14 @@ impl Entity for GoToLine {
type Event = Event; type Event = Event;
fn release(&mut self, cx: &mut MutableAppContext) { 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(); let restore_state = self.restore_state.take();
self.active_editor.update(cx, |editor, cx| { self.active_editor.update(cx, |editor, cx| {
editor.set_highlighted_row(None); editor.set_highlighted_rows(None);
if let Some((line_selection, restore_state)) = line_selection.zip(restore_state) { if let Some((line_selection_id, restore_state)) = line_selection_id.zip(restore_state) {
let newest_selection = let newest_selection =
editor.newest_selection::<usize>(&editor.buffer().read(cx).read(cx)); 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.set_scroll_position(restore_state.scroll_position, cx);
editor.update_selections(restore_state.selections, None, 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>) { fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
cx.focus(&self.line_editor); cx.focus(&self.line_editor);
} }
fn on_blur(&mut self, _: &mut ViewContext<Self>) {}
} }

View file

@ -52,6 +52,11 @@ impl Container {
self 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 { pub fn with_margin_left(mut self, margin: f32) -> Self {
self.style.margin.left = margin; self.style.margin.left = margin;
self self

View file

@ -1,13 +1,16 @@
use std::{ops::Range, sync::Arc};
use crate::{ use crate::{
color::Color, color::Color,
fonts::TextStyle, fonts::{HighlightStyle, TextStyle},
geometry::{ geometry::{
rect::RectF, rect::RectF,
vector::{vec2f, Vector2F}, vector::{vec2f, Vector2F},
}, },
json::{ToJson, Value}, json::{ToJson, Value},
text_layout::{Line, ShapedBoundary}, text_layout::{Line, RunStyle, ShapedBoundary},
DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, DebugContext, Element, Event, EventContext, FontCache, LayoutContext, PaintContext,
SizeConstraint, TextLayoutCache,
}; };
use serde_json::json; use serde_json::json;
@ -15,10 +18,12 @@ pub struct Text {
text: String, text: String,
style: TextStyle, style: TextStyle,
soft_wrap: bool, soft_wrap: bool,
highlights: Vec<(Range<usize>, HighlightStyle)>,
} }
pub struct LayoutState { pub struct LayoutState {
lines: Vec<(Line, Vec<ShapedBoundary>)>, shaped_lines: Vec<Line>,
wrap_boundaries: Vec<Vec<ShapedBoundary>>,
line_height: f32, line_height: f32,
} }
@ -28,6 +33,7 @@ impl Text {
text, text,
style, style,
soft_wrap: true, soft_wrap: true,
highlights: Vec::new(),
} }
} }
@ -36,6 +42,11 @@ impl Text {
self 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 { pub fn with_soft_wrap(mut self, soft_wrap: bool) -> Self {
self.soft_wrap = soft_wrap; self.soft_wrap = soft_wrap;
self self
@ -51,32 +62,59 @@ impl Element for Text {
constraint: SizeConstraint, constraint: SizeConstraint,
cx: &mut LayoutContext, cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) { ) -> (Vector2F, Self::LayoutState) {
let font_id = self.style.font_id; // Convert the string and highlight ranges into an iterator of highlighted chunks.
let line_height = cx.font_cache.line_height(font_id, self.style.font_size); 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); // Perform shaping on these highlighted chunks
let mut lines = Vec::new(); 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 line_count = 0;
let mut max_line_width = 0_f32; let mut max_line_width = 0_f32;
for line in self.text.lines() { let mut wrap_boundaries = Vec::new();
let shaped_line = cx.text_layout_cache.layout_str( let mut wrapper = cx.font_cache.line_wrapper(font_id, self.style.font_size);
line, for (line, shaped_line) in self.text.lines().zip(&shaped_lines) {
self.style.font_size, if self.soft_wrap {
&[(line.len(), self.style.to_run())], let boundaries = wrapper
); .wrap_shaped_line(line, shaped_line, constraint.max.x())
let wrap_boundaries = if self.soft_wrap { .collect::<Vec<_>>();
wrapper line_count += boundaries.len() + 1;
.wrap_shaped_line(line, &shaped_line, constraint.max.x()) wrap_boundaries.push(boundaries);
.collect::<Vec<_>>()
} else { } else {
Vec::new() line_count += 1;
}; }
max_line_width = max_line_width.max(shaped_line.width()); 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( let size = vec2f(
max_line_width max_line_width
.ceil() .ceil()
@ -84,7 +122,14 @@ impl Element for Text {
.min(constraint.max.x()), .min(constraint.max.x()),
(line_height * line_count as f32).ceil(), (line_height * line_count as f32).ceil(),
); );
(size, LayoutState { lines, line_height }) (
size,
LayoutState {
shaped_lines,
wrap_boundaries,
line_height,
},
)
} }
fn paint( fn paint(
@ -95,8 +140,10 @@ impl Element for Text {
cx: &mut PaintContext, cx: &mut PaintContext,
) -> Self::PaintState { ) -> Self::PaintState {
let mut origin = bounds.origin(); let mut origin = bounds.origin();
for (line, wrap_boundaries) in &layout.lines { let empty = Vec::new();
let wrapped_line_boundaries = RectF::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, origin,
vec2f( vec2f(
bounds.width(), bounds.width(),
@ -104,16 +151,20 @@ impl Element for Text {
), ),
); );
if wrapped_line_boundaries.intersects(visible_bounds) { if boundaries.intersects(visible_bounds) {
line.paint_wrapped( if self.soft_wrap {
origin, line.paint_wrapped(
visible_bounds, origin,
layout.line_height, visible_bounds,
wrap_boundaries.iter().copied(), layout.line_height,
cx, 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
}

View file

@ -14,9 +14,15 @@ use std::{cmp, ops::Range, sync::Arc};
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct UniformListState(Arc<Mutex<StateInner>>); pub struct UniformListState(Arc<Mutex<StateInner>>);
#[derive(Debug)]
pub enum ScrollTarget {
Show(usize),
Center(usize),
}
impl UniformListState { impl UniformListState {
pub fn scroll_to(&self, item_ix: usize) { pub fn scroll_to(&self, scroll_to: ScrollTarget) {
self.0.lock().scroll_to = Some(item_ix); self.0.lock().scroll_to = Some(scroll_to);
} }
pub fn scroll_top(&self) -> f32 { pub fn scroll_top(&self) -> f32 {
@ -27,7 +33,7 @@ impl UniformListState {
#[derive(Default)] #[derive(Default)]
struct StateInner { struct StateInner {
scroll_top: f32, scroll_top: f32,
scroll_to: Option<usize>, scroll_to: Option<ScrollTarget>,
} }
pub struct LayoutState { pub struct LayoutState {
@ -93,20 +99,38 @@ where
fn autoscroll(&mut self, scroll_max: f32, list_height: f32, item_height: f32) { fn autoscroll(&mut self, scroll_max: f32, list_height: f32, item_height: f32) {
let mut state = self.state.0.lock(); let mut state = self.state.0.lock();
if state.scroll_top > scroll_max { if let Some(scroll_to) = state.scroll_to.take() {
state.scroll_top = scroll_max; 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_top = self.padding_top + item_ix as f32 * item_height;
let item_bottom = item_top + item_height; let item_bottom = item_top + item_height;
if center {
if item_top < state.scroll_top { let item_center = item_top + item_height / 2.;
state.scroll_top = item_top; state.scroll_top = (item_center - list_height / 2.).max(0.);
} else if item_bottom > (state.scroll_top + list_height) { } else {
state.scroll_top = item_bottom - list_height; 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 { fn scroll_top(&self) -> f32 {

View file

@ -30,7 +30,7 @@ pub struct TextStyle {
pub underline: Option<Color>, pub underline: Option<Color>,
} }
#[derive(Copy, Clone, Debug, Default)] #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct HighlightStyle { pub struct HighlightStyle {
pub color: Color, pub color: Color,
pub font_properties: Properties, pub font_properties: Properties,

View file

@ -23,6 +23,7 @@ struct Pending {
context: Option<Context>, context: Option<Context>,
} }
#[derive(Default)]
pub struct Keymap(Vec<Binding>); pub struct Keymap(Vec<Binding>);
pub struct 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 { impl Binding {
pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self { pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
let context = if let Some(context) = context { let context = if let Some(context) = context {

View file

@ -19,6 +19,7 @@ test-support = [
[dependencies] [dependencies]
clock = { path = "../clock" } clock = { path = "../clock" }
collections = { path = "../collections" } collections = { path = "../collections" }
fuzzy = { path = "../fuzzy" }
gpui = { path = "../gpui" } gpui = { path = "../gpui" }
lsp = { path = "../lsp" } lsp = { path = "../lsp" }
rpc = { path = "../rpc" } rpc = { path = "../rpc" }

View file

@ -6,7 +6,8 @@ pub use crate::{
}; };
use crate::{ use crate::{
diagnostic_set::{DiagnosticEntry, DiagnosticGroup}, diagnostic_set::{DiagnosticEntry, DiagnosticGroup},
range_from_lsp, outline::OutlineItem,
range_from_lsp, Outline,
}; };
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use clock::ReplicaId; use clock::ReplicaId;
@ -193,7 +194,7 @@ pub trait File {
fn as_any(&self) -> &dyn Any; fn as_any(&self) -> &dyn Any;
} }
struct QueryCursorHandle(Option<QueryCursor>); pub(crate) struct QueryCursorHandle(Option<QueryCursor>);
#[derive(Clone)] #[derive(Clone)]
struct SyntaxTree { struct SyntaxTree {
@ -1264,6 +1265,13 @@ impl Buffer {
self.edit_internal(ranges_iter, new_text, true, cx) 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>( pub fn edit_internal<I, S, T>(
&mut self, &mut self,
ranges_iter: I, 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>( pub fn enclosing_bracket_ranges<T: ToOffset>(
&self, &self,
range: Range<T>, range: Range<T>,
@ -1854,6 +1966,12 @@ impl BufferSnapshot {
.min_by_key(|(open_range, close_range)| close_range.end - open_range.start) .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>( pub fn remote_selections_in_range<'a>(
&'a self, &'a self,
range: Range<Anchor>, range: Range<Anchor>,
@ -2108,7 +2226,7 @@ impl<'a> Iterator for BufferChunks<'a> {
} }
impl QueryCursorHandle { impl QueryCursorHandle {
fn new() -> Self { pub(crate) fn new() -> Self {
QueryCursorHandle(Some( QueryCursorHandle(Some(
QUERY_CURSORS QUERY_CURSORS
.lock() .lock()

View file

@ -1,6 +1,7 @@
mod buffer; mod buffer;
mod diagnostic_set; mod diagnostic_set;
mod highlight_map; mod highlight_map;
mod outline;
pub mod proto; pub mod proto;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
@ -13,6 +14,7 @@ pub use diagnostic_set::DiagnosticEntry;
use gpui::AppContext; use gpui::AppContext;
use highlight_map::HighlightMap; use highlight_map::HighlightMap;
use lazy_static::lazy_static; use lazy_static::lazy_static;
pub use outline::{Outline, OutlineItem};
use parking_lot::Mutex; use parking_lot::Mutex;
use serde::Deserialize; use serde::Deserialize;
use std::{ops::Range, path::Path, str, sync::Arc}; use std::{ops::Range, path::Path, str, sync::Arc};
@ -74,6 +76,7 @@ pub struct Grammar {
pub(crate) highlights_query: Query, pub(crate) highlights_query: Query,
pub(crate) brackets_query: Query, pub(crate) brackets_query: Query,
pub(crate) indents_query: Query, pub(crate) indents_query: Query,
pub(crate) outline_query: Query,
pub(crate) highlight_map: Mutex<HighlightMap>, pub(crate) highlight_map: Mutex<HighlightMap>,
} }
@ -127,6 +130,7 @@ impl Language {
brackets_query: Query::new(ts_language, "").unwrap(), brackets_query: Query::new(ts_language, "").unwrap(),
highlights_query: Query::new(ts_language, "").unwrap(), highlights_query: Query::new(ts_language, "").unwrap(),
indents_query: Query::new(ts_language, "").unwrap(), indents_query: Query::new(ts_language, "").unwrap(),
outline_query: Query::new(ts_language, "").unwrap(),
ts_language, ts_language,
highlight_map: Default::default(), highlight_map: Default::default(),
}) })
@ -164,6 +168,16 @@ impl Language {
Ok(self) 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 { pub fn name(&self) -> &str {
self.config.name.as_str() self.config.name.as_str()
} }

View 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
}
}

View file

@ -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] #[gpui::test]
fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) { fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
let buffer = cx.add_model(|cx| { let buffer = cx.add_model(|cx| {
@ -1017,14 +1150,18 @@ fn rust_lang() -> Language {
) )
.with_indents_query( .with_indents_query(
r#" r#"
(call_expression) @indent (call_expression) @indent
(field_expression) @indent (field_expression) @indent
(_ "(" ")" @end) @indent (_ "(" ")" @end) @indent
(_ "{" "}" @end) @indent (_ "{" "}" @end) @indent
"#, "#,
) )
.unwrap() .unwrap()
.with_brackets_query(r#" ("{" @open "}" @close) "#) .with_brackets_query(
r#"
("{" @open "}" @close)
"#,
)
.unwrap() .unwrap()
} }

View file

@ -16,7 +16,7 @@ use std::{
io::Write, io::Write,
str::FromStr, str::FromStr,
sync::{ sync::{
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst}, atomic::{AtomicUsize, Ordering::SeqCst},
Arc, Arc,
}, },
}; };
@ -431,7 +431,7 @@ pub struct FakeLanguageServer {
buffer: Vec<u8>, buffer: Vec<u8>,
stdin: smol::io::BufReader<async_pipe::PipeReader>, stdin: smol::io::BufReader<async_pipe::PipeReader>,
stdout: smol::io::BufWriter<async_pipe::PipeWriter>, stdout: smol::io::BufWriter<async_pipe::PipeWriter>,
pub started: Arc<AtomicBool>, pub started: Arc<std::sync::atomic::AtomicBool>,
} }
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
@ -449,7 +449,7 @@ impl LanguageServer {
stdin: smol::io::BufReader::new(stdin.1), stdin: smol::io::BufReader::new(stdin.1),
stdout: smol::io::BufWriter::new(stdout.0), stdout: smol::io::BufWriter::new(stdout.0),
buffer: Vec::new(), 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(); let server = Self::new_internal(stdin.0, stdout.1, Path::new("/"), executor).unwrap();

18
crates/outline/Cargo.toml Normal file
View 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"

View 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
},
),
]
);
}
}

View file

@ -1,14 +1,10 @@
use gpui::{ use gpui::{
action, action,
elements::{ elements::{
Align, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement, Svg, Align, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement, ScrollTarget,
UniformList, UniformListState, Svg, UniformList, UniformListState,
},
keymap::{
self,
menu::{SelectNext, SelectPrev},
Binding,
}, },
keymap::{self, Binding},
platform::CursorStyle, platform::CursorStyle,
AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, ReadModel, View, AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, ReadModel, View,
ViewContext, ViewHandle, WeakViewHandle, ViewContext, ViewHandle, WeakViewHandle,
@ -20,7 +16,10 @@ use std::{
ffi::OsStr, ffi::OsStr,
ops::Range, ops::Range,
}; };
use workspace::{Settings, Workspace}; use workspace::{
menu::{SelectNext, SelectPrev},
Settings, Workspace,
};
pub struct ProjectPanel { pub struct ProjectPanel {
project: ModelHandle<Project>, project: ModelHandle<Project>,
@ -278,7 +277,7 @@ impl ProjectPanel {
fn autoscroll(&mut self) { fn autoscroll(&mut self) {
if let Some(selection) = self.selection { if let Some(selection) = self.selection {
self.list.scroll_to(selection.index); self.list.scroll_to(ScrollTarget::Show(selection.index));
} }
} }

View file

@ -3,7 +3,7 @@ use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
use gpui::{ use gpui::{
action, action,
elements::*, elements::*,
keymap::{self, menu, Binding}, keymap::{self, Binding},
AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, View, AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, View,
ViewContext, ViewHandle, ViewContext, ViewHandle,
}; };
@ -11,7 +11,10 @@ use parking_lot::Mutex;
use postage::watch; use postage::watch;
use std::{cmp, sync::Arc}; use std::{cmp, sync::Arc};
use theme::ThemeRegistry; use theme::ThemeRegistry;
use workspace::{AppState, Settings, Workspace}; use workspace::{
menu::{Confirm, SelectNext, SelectPrev},
AppState, Settings, Workspace,
};
#[derive(Clone)] #[derive(Clone)]
pub struct ThemeSelectorParams { pub struct ThemeSelectorParams {
@ -30,7 +33,6 @@ pub struct ThemeSelector {
selected_index: usize, selected_index: usize,
} }
action!(Confirm);
action!(Toggle, ThemeSelectorParams); action!(Toggle, ThemeSelectorParams);
action!(Reload, 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 cmd-t", Toggle(params.clone()), None),
Binding::new("cmd-k t", Reload(params.clone()), None), Binding::new("cmd-k t", Reload(params.clone()), None),
Binding::new("escape", Toggle(params.clone()), Some("ThemeSelector")), 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 { if self.selected_index > 0 {
self.selected_index -= 1; self.selected_index -= 1;
} }
self.list_state.scroll_to(self.selected_index); self.list_state
.scroll_to(ScrollTarget::Show(self.selected_index));
cx.notify(); 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() { if self.selected_index + 1 < self.matches.len() {
self.selected_index += 1; self.selected_index += 1;
} }
self.list_state.scroll_to(self.selected_index); self.list_state
.scroll_to(ScrollTarget::Show(self.selected_index));
cx.notify(); cx.notify();
} }
@ -157,7 +160,9 @@ impl ThemeSelector {
let candidates = self let candidates = self
.themes .themes
.list() .list()
.map(|name| StringMatchCandidate { .enumerate()
.map(|(id, name)| StringMatchCandidate {
id,
char_bag: name.as_str().into(), char_bag: name.as_str().into(),
string: name, string: name,
}) })
@ -167,7 +172,9 @@ impl ThemeSelector {
self.matches = if query.is_empty() { self.matches = if query.is_empty() {
candidates candidates
.into_iter() .into_iter()
.map(|candidate| StringMatch { .enumerate()
.map(|(index, candidate)| StringMatch {
candidate_id: index,
string: candidate.string, string: candidate.string,
positions: Vec::new(), positions: Vec::new(),
score: 0.0, score: 0.0,

View 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")),
]);
}

View file

@ -1,3 +1,4 @@
pub mod menu;
pub mod pane; pub mod pane;
pub mod pane_group; pub mod pane_group;
pub mod settings; pub mod settings;
@ -48,6 +49,9 @@ action!(Save);
action!(DebugElements); action!(DebugElements);
pub fn init(cx: &mut MutableAppContext) { pub fn init(cx: &mut MutableAppContext) {
pane::init(cx);
menu::init(cx);
cx.add_global_action(open); cx.add_global_action(open);
cx.add_global_action(move |action: &OpenPaths, cx: &mut MutableAppContext| { cx.add_global_action(move |action: &OpenPaths, cx: &mut MutableAppContext| {
open_paths(&action.0.paths, &action.0.app_state, cx).detach(); open_paths(&action.0.paths, &action.0.app_state, cx).detach();
@ -84,7 +88,6 @@ pub fn init(cx: &mut MutableAppContext) {
None, None,
), ),
]); ]);
pane::init(cx);
} }
pub struct AppState { pub struct AppState {

View file

@ -43,6 +43,7 @@ gpui = { path = "../gpui" }
journal = { path = "../journal" } journal = { path = "../journal" }
language = { path = "../language" } language = { path = "../language" }
lsp = { path = "../lsp" } lsp = { path = "../lsp" }
outline = { path = "../outline" }
project = { path = "../project" } project = { path = "../project" }
project_panel = { path = "../project_panel" } project_panel = { path = "../project_panel" }
rpc = { path = "../rpc" } rpc = { path = "../rpc" }

View file

@ -211,7 +211,7 @@ text = { extends = "$text.0" }
[selector] [selector]
background = "$surface.0" background = "$surface.0"
padding = 8 padding = 8
margin.top = 52 margin = { top = 52, bottom = 52 }
corner_radius = 6 corner_radius = 6
shadow = { offset = [0, 2], blur = 16, color = "$shadow.0" } shadow = { offset = [0, 2], blur = 16, color = "$shadow.0" }
border = { width = 1, color = "$border.0" } border = { width = 1, color = "$border.0" }

View 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

View file

@ -24,6 +24,8 @@ fn rust() -> Language {
.unwrap() .unwrap()
.with_indents_query(load_query("rust/indents.scm").as_ref()) .with_indents_query(load_query("rust/indents.scm").as_ref())
.unwrap() .unwrap()
.with_outline_query(load_query("rust/outline.scm").as_ref())
.unwrap()
} }
fn markdown() -> Language { fn markdown() -> Language {

View file

@ -59,6 +59,7 @@ fn main() {
go_to_line::init(cx); go_to_line::init(cx);
file_finder::init(cx); file_finder::init(cx);
chat_panel::init(cx); chat_panel::init(cx);
outline::init(cx);
project_panel::init(cx); project_panel::init(cx);
diagnostics::init(cx); diagnostics::init(cx);
cx.spawn({ cx.spawn({