Merge pull request #1246 from MinusGix/list

Unify Palette/Completion with List widget
This commit is contained in:
Dongdong Zhou 2022-09-18 11:43:43 +01:00 committed by GitHub
commit def69d09cb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1280 additions and 936 deletions

View File

@ -50,13 +50,13 @@
pub const LAPCE_UI_COMMAND: Selector<LapceUICommand> =
Selector::new("lapce.ui_command");
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LapceCommand {
pub kind: CommandKind,
pub data: Option<Value>,
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CommandKind {
Workbench(LapceWorkbenchCommand),
Edit(EditCommand),
@ -544,8 +544,8 @@ pub enum LapceUICommand {
RunPaletteReferences(Vec<EditorLocation<Position>>),
InitPaletteInput(String),
UpdatePaletteInput(String),
UpdatePaletteItems(String, Vec<PaletteItem>),
FilterPaletteItems(String, String, Vec<PaletteItem>),
UpdatePaletteItems(String, im::Vector<PaletteItem>),
FilterPaletteItems(String, String, im::Vector<PaletteItem>),
UpdateKeymapsFilter(String),
ResetSettingsFile(String, String),
UpdateSettingsFile(String, String, Value),
@ -712,6 +712,10 @@ pub enum LapceUICommand {
},
FileExplorerRefresh,
SetLanguage(String),
/// An item in a list was chosen
/// This is typically targeted at the widget which contains the list
ListItemSelected,
}
/// This can't be an `FnOnce` because we only ever get a reference to

View File

@ -1,16 +1,16 @@
use std::{fmt::Display, path::PathBuf, sync::Arc};
use anyhow::Error;
use druid::{Size, WidgetId};
use druid::{EventCtx, Size, WidgetId};
use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher};
use itertools::Itertools;
use lapce_core::movement::Movement;
use lapce_core::command::FocusCommand;
use lapce_rpc::{buffer::BufferId, plugin::PluginId};
use lsp_types::{CompletionItem, CompletionResponse, Position};
use regex::Regex;
use std::str::FromStr;
use crate::proxy::LapceProxy;
use crate::{config::Config, list::ListData, proxy::LapceProxy};
#[derive(Debug)]
pub struct Snippet {
@ -240,94 +240,70 @@ pub struct CompletionData {
pub offset: usize,
pub buffer_id: BufferId,
pub input: String,
pub index: usize,
pub input_items: im::HashMap<String, Arc<Vec<ScoredCompletionItem>>>,
empty: Arc<Vec<ScoredCompletionItem>>,
pub filtered_items: Arc<Vec<ScoredCompletionItem>>,
pub input_items: im::HashMap<String, im::Vector<ScoredCompletionItem>>,
empty: im::Vector<ScoredCompletionItem>,
pub completion_list: ListData<ScoredCompletionItem, ()>,
pub matcher: Arc<SkimMatcherV2>,
/// The size of the completion list
pub size: Size,
/// The size of the documentation view
pub documentation_size: Size,
}
impl CompletionData {
pub fn new() -> Self {
pub fn new(config: Arc<Config>) -> Self {
let id = WidgetId::next();
let mut completion_list = ListData::new(config, id, ());
// TODO: Make this configurable
completion_list.max_displayed_items = 15;
Self {
id: WidgetId::next(),
id,
scroll_id: WidgetId::next(),
documentation_scroll_id: WidgetId::next(),
request_id: 0,
index: 0,
offset: 0,
status: CompletionStatus::Inactive,
buffer_id: BufferId(0),
input: "".to_string(),
input_items: im::HashMap::new(),
filtered_items: Arc::new(Vec::new()),
completion_list,
matcher: Arc::new(SkimMatcherV2::default().ignore_case()),
size: Size::new(400.0, 300.0),
// TODO: Make this configurable
documentation_size: Size::new(400.0, 300.0),
empty: Arc::new(Vec::new()),
empty: im::Vector::new(),
}
}
/// Return the number of entries that are displayable
pub fn len(&self) -> usize {
self.current_items().len()
self.completion_list.items.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// We need the line height so that we can get the number displayed as a number, since
/// we just render as many fit inside the `size` defined for completion.
fn entry_count(&self, editor_line_height: usize) -> usize {
((self.size.height / editor_line_height as f64).ceil() as usize)
.saturating_sub(1)
}
pub fn next(&mut self) {
self.index = Movement::Down.update_index(self.index, self.len(), 1, true);
}
pub fn next_page(&mut self, editor_line_height: usize) {
let count = self.entry_count(editor_line_height);
self.index =
Movement::Down.update_index(self.index, self.len(), count, false);
}
pub fn previous(&mut self) {
self.index = Movement::Up.update_index(self.index, self.len(), 1, true);
}
pub fn previous_page(&mut self, editor_line_height: usize) {
let count = self.entry_count(editor_line_height);
self.index = Movement::Up.update_index(self.index, self.len(), count, false);
}
pub fn current_items(&self) -> &Arc<Vec<ScoredCompletionItem>> {
pub fn current_items(&self) -> &im::Vector<ScoredCompletionItem> {
if self.input.is_empty() {
self.all_items()
} else {
&self.filtered_items
&self.completion_list.items
}
}
pub fn all_items(&self) -> &Arc<Vec<ScoredCompletionItem>> {
pub fn all_items(&self) -> &im::Vector<ScoredCompletionItem> {
self.input_items
.get(&self.input)
.filter(|items| !items.is_empty())
.unwrap_or_else(move || self.input_items.get("").unwrap_or(&self.empty))
}
pub fn current_item(&self) -> &ScoredCompletionItem {
&self.current_items()[self.index]
pub fn current_item(&self) -> Option<&ScoredCompletionItem> {
self.completion_list
.items
.get(self.completion_list.selected_index)
}
pub fn current(&self) -> &str {
self.current_items()[self.index].item.label.as_str()
pub fn current(&self) -> Option<&str> {
self.current_item().map(|item| item.item.label.as_str())
}
#[allow(clippy::too_many_arguments)]
@ -351,12 +327,12 @@ pub fn cancel(&mut self) {
self.status = CompletionStatus::Inactive;
self.input = "".to_string();
self.input_items.clear();
self.index = 0;
self.completion_list.clear_items();
}
pub fn update_input(&mut self, input: String) {
self.input = input;
self.index = 0;
self.completion_list.selected_index = 0;
if self.status == CompletionStatus::Inactive {
return;
}
@ -379,7 +355,7 @@ pub fn receive(
CompletionResponse::Array(items) => items,
CompletionResponse::List(list) => list.items,
};
let items: Vec<ScoredCompletionItem> = items
let items: im::Vector<ScoredCompletionItem> = items
.iter()
.map(|i| ScoredCompletionItem {
item: i.to_owned(),
@ -390,20 +366,21 @@ pub fn receive(
})
.collect();
self.input_items.insert(input, Arc::new(items));
self.input_items.insert(input, items);
self.filter_items();
if self.index >= self.len() {
self.index = 0;
if self.completion_list.selected_index >= self.len() {
self.completion_list.selected_index = 0;
}
}
pub fn filter_items(&mut self) {
if self.input.is_empty() {
self.completion_list.items = self.all_items().clone();
return;
}
let mut items: Vec<ScoredCompletionItem> = self
let mut items: im::Vector<ScoredCompletionItem> = self
.all_items()
.iter()
.filter_map(|i| {
@ -439,31 +416,15 @@ pub fn filter_items(&mut self) {
.then_with(|| b.label_score.cmp(&a.label_score))
.then_with(|| a.item.label.len().cmp(&b.item.label.len()))
});
self.filtered_items = Arc::new(items);
self.completion_list.items = items;
}
pub fn run_focus_command(&mut self, ctx: &mut EventCtx, command: &FocusCommand) {
self.completion_list.run_focus_command(ctx, command);
}
}
impl Default for CompletionData {
fn default() -> Self {
Self::new()
}
}
pub struct Completion {}
impl Completion {
pub fn new() -> Self {
Self {}
}
}
impl Default for Completion {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone)]
#[derive(Clone, PartialEq)]
pub struct ScoredCompletionItem {
pub item: CompletionItem,
pub plugin_id: PluginId,

View File

@ -664,8 +664,8 @@ pub fn new(
term_sender.clone(),
event_sink.clone(),
));
let palette = Arc::new(PaletteData::new(proxy.clone()));
let completion = Arc::new(CompletionData::new());
let palette = Arc::new(PaletteData::new(config.clone(), proxy.clone()));
let completion = Arc::new(CompletionData::new(config.clone()));
let hover = Arc::new(HoverData::new());
let rename = Arc::new(RenameData::new());
let source_control = Arc::new(SourceControlData::new());
@ -958,6 +958,7 @@ pub fn completion_origin(
&self,
text: &mut PietText,
tab_size: Size,
completion_size: Size,
config: &Config,
) -> Point {
let line_height = self.config.editor.line_height() as f64;
@ -986,10 +987,8 @@ pub fn completion_origin(
let mut origin = *editor.window_origin.borrow()
- self.window_origin.borrow().to_vec2()
+ Vec2::new(point_below.x - line_height - 5.0, point_below.y);
if origin.y + self.completion.size.height + 1.0 > tab_size.height {
let height = self
.completion
.size
if origin.y + completion_size.height + 1.0 > tab_size.height {
let height = completion_size
.height
.min(self.completion.len() as f64 * line_height);
origin.y = editor.window_origin.borrow().y
@ -997,8 +996,8 @@ pub fn completion_origin(
+ point_above.y
- height;
}
if origin.x + self.completion.size.width + 1.0 > tab_size.width {
origin.x = tab_size.width - self.completion.size.width - 1.0;
if origin.x + completion_size.width + 1.0 > tab_size.width {
origin.x = tab_size.width - completion_size.width - 1.0;
}
if origin.x <= 0.0 {
origin.x = 0.0;

View File

@ -196,7 +196,7 @@ fn init_buffer_content_cmd(
}
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq)]
pub struct EditorLocation<P: EditorPosition = usize> {
pub path: PathBuf,
pub position: Option<P>,
@ -632,6 +632,52 @@ pub fn cancel_rename(&mut self, ctx: &mut EventCtx) {
}
}
pub fn completion_item_select(&mut self, ctx: &mut EventCtx) {
let item = if let Some(item) = self.completion.current_item() {
item.to_owned()
} else {
// There was no selected item, this may be due to a bug in failing to ensure that the index was valid.
return;
};
self.cancel_completion();
if item.item.data.is_some() {
let view_id = self.editor.view_id;
let buffer_id = self.doc.id();
let rev = self.doc.rev();
let offset = self.editor.cursor.offset();
let event_sink = ctx.get_external_handle();
self.proxy.proxy_rpc.completion_resolve(
item.plugin_id,
item.item.clone(),
move |result| {
// let item = result.unwrap_or_else(|_| item.clone());
let item =
if let Ok(ProxyResponse::CompletionResolveResponse {
item,
}) = result
{
*item
} else {
item.item.clone()
};
let _ = event_sink.submit_command(
LAPCE_UI_COMMAND,
LapceUICommand::ResolveCompletion(
buffer_id,
rev,
offset,
Box::new(item),
),
Target::Widget(view_id),
);
},
);
} else {
let _ = self.apply_completion_item(&item.item);
}
}
/// Update the displayed autocompletion box
/// Sends a request to the LSP for completion information
fn update_completion(
@ -1679,44 +1725,8 @@ fn run_focus_command(
Target::Widget(self.palette.widget_id),
));
} else {
let item = self.completion.current_item().to_owned();
self.cancel_completion();
if item.item.data.is_some() {
let view_id = self.editor.view_id;
let buffer_id = self.doc.id();
let rev = self.doc.rev();
let offset = self.editor.cursor.offset();
let event_sink = ctx.get_external_handle();
self.proxy.proxy_rpc.completion_resolve(
item.plugin_id,
item.item.clone(),
move |result| {
// let item = result.unwrap_or_else(|_| item.clone());
let item = if let Ok(
ProxyResponse::CompletionResolveResponse {
item,
},
) = result
{
*item
} else {
item.item.clone()
};
let _ = event_sink.submit_command(
LAPCE_UI_COMMAND,
LapceUICommand::ResolveCompletion(
buffer_id,
rev,
offset,
Box::new(item),
),
Target::Widget(view_id),
);
},
);
} else {
let _ = self.apply_completion_item(&item.item);
}
let completion = Arc::make_mut(&mut self.completion);
completion.run_focus_command(ctx, cmd);
}
}
ListNext => {
@ -1731,7 +1741,7 @@ fn run_focus_command(
));
} else {
let completion = Arc::make_mut(&mut self.completion);
completion.next();
completion.run_focus_command(ctx, cmd);
}
}
ListNextPage => {
@ -1746,7 +1756,7 @@ fn run_focus_command(
));
} else {
let completion = Arc::make_mut(&mut self.completion);
completion.next_page(self.config.editor.line_height());
completion.run_focus_command(ctx, cmd);
}
}
ListPrevious => {
@ -1761,7 +1771,7 @@ fn run_focus_command(
));
} else {
let completion = Arc::make_mut(&mut self.completion);
completion.previous();
completion.run_focus_command(ctx, cmd);
}
}
ListPreviousPage => {
@ -1776,7 +1786,7 @@ fn run_focus_command(
));
} else {
let completion = Arc::make_mut(&mut self.completion);
completion.previous_page(self.config.editor.line_height());
completion.run_focus_command(ctx, cmd);
}
}
JumpToNextSnippetPlaceholder => {

View File

@ -13,6 +13,7 @@
pub mod history;
pub mod hover;
pub mod keypress;
pub mod list;
pub mod markdown;
pub mod menu;
pub mod palette;

203
lapce-data/src/list.rs Normal file
View File

@ -0,0 +1,203 @@
use std::sync::Arc;
use druid::{Command, Data, EventCtx, Target, WidgetId};
use lapce_core::{command::FocusCommand, movement::Movement};
use crate::{
command::{
CommandExecuted, CommandKind, LapceCommand, LapceUICommand, LAPCE_UI_COMMAND,
},
config::{Config, GetConfig},
};
// Note: when adding fields to this, make sure to think whether they need to be added to the `same`
// implementation
/// Note: all `T` are going to be required by the UI code to implement `ListPaint<D>`
#[derive(Clone)]
pub struct ListData<T: Clone, D: Data> {
/// The id of the widget that contains the [`ListData`] which wishes to receive
/// events (such as when an entry is selected)
pub parent: WidgetId,
/// The items that can be selected from the list
/// Note that since this is an `im::Vector`, cloning is cheap.
pub items: im::Vector<T>,
/// Extra data attached to the list for the item rendering to use.
pub data: D,
/// The index of the item which is selected
pub selected_index: usize,
/// The maximum number of items to render in the list.
pub max_displayed_items: usize,
/// The line height of each list element
/// Defaults to the editor line height if not set
pub line_height: Option<usize>,
// These should be filled whenever you call into the `List` widget
pub config: Arc<Config>,
}
impl<T: Clone, D: Data> ListData<T, D> {
pub fn new(
config: Arc<Config>,
parent: WidgetId,
held_data: D,
) -> ListData<T, D> {
ListData {
parent,
items: im::Vector::new(),
data: held_data,
selected_index: 0,
max_displayed_items: 15,
line_height: None,
config,
}
}
/// Clone the list data, giving it data needed to update it
/// This is typically what you need to use to ensure that it has the
/// appropriately updated data when passing the data to the list's widget functions
/// Note that due to the usage of `Arc` and `im::Vector`, cloning is relatively cheap.
pub fn clone_with(&self, config: Arc<Config>) -> ListData<T, D> {
let mut data = self.clone();
data.update_data(config);
data
}
pub fn update_data(&mut self, config: Arc<Config>) {
self.config = config;
}
pub fn line_height(&self) -> usize {
self.line_height
.unwrap_or_else(|| self.config.editor.line_height())
}
/// The maximum number of items in the list that can be displayed
/// This is limited by `max_displayed_items` *or* by the number of items
pub fn max_display_count(&self) -> usize {
let mut count = 0;
for _ in self.items.iter() {
count += 1;
if count >= self.max_displayed_items {
return self.max_displayed_items;
}
}
count
}
pub fn clear_items(&mut self) {
self.items.clear();
self.selected_index = 0;
}
/// Run a command, like those received from KeyPressFocus
pub fn run_command(
&mut self,
ctx: &mut EventCtx,
command: &LapceCommand,
) -> CommandExecuted {
match &command.kind {
CommandKind::Focus(cmd) => self.run_focus_command(ctx, cmd),
_ => CommandExecuted::No,
}
}
pub fn run_focus_command(
&mut self,
ctx: &mut EventCtx,
command: &FocusCommand,
) -> CommandExecuted {
match command {
// ModalClose should be handled (if desired) by the containing widget
FocusCommand::ListNext => {
self.next();
}
FocusCommand::ListNextPage => {
self.next_page();
}
FocusCommand::ListPrevious => {
self.previous();
}
FocusCommand::ListPreviousPage => {
self.previous_page();
}
FocusCommand::ListSelect => {
self.select(ctx);
}
_ => return CommandExecuted::No,
}
CommandExecuted::Yes
}
// TODO: Option for whether moving should be wrapping
pub fn next(&mut self) {
self.selected_index = Movement::Down.update_index(
self.selected_index,
self.items.len(),
1,
true,
);
}
pub fn next_page(&mut self) {
self.selected_index = Movement::Down.update_index(
self.selected_index,
self.items.len(),
self.max_displayed_items - 1,
false,
);
}
pub fn previous(&mut self) {
self.selected_index = Movement::Up.update_index(
self.selected_index,
self.items.len(),
1,
true,
);
}
pub fn previous_page(&mut self) {
self.selected_index = Movement::Up.update_index(
self.selected_index,
self.items.len(),
self.max_displayed_items - 1,
false,
);
}
pub fn select(&self, ctx: &mut EventCtx) {
ctx.submit_command(Command::new(
LAPCE_UI_COMMAND,
LapceUICommand::ListItemSelected,
Target::Widget(self.parent),
));
}
pub fn current_selected_item(&self) -> Option<&T> {
self.items.get(self.selected_index)
}
}
impl<T: Clone + PartialEq + 'static, D: Data> Data for ListData<T, D> {
fn same(&self, other: &Self) -> bool {
// We don't compare the held Config, because that should be updated whenever
// the widget is used
self.parent == other.parent
&& self.items == other.items
&& self.data.same(&other.data)
&& self.selected_index.same(&other.selected_index)
&& self.max_displayed_items.same(&other.max_displayed_items)
&& self.line_height.same(&other.line_height)
}
}
impl<T: Clone + PartialEq + 'static, D: Data> GetConfig for ListData<T, D> {
fn get_config(&self) -> &Config {
&self.config
}
}

View File

@ -9,7 +9,6 @@
use lapce_core::command::{EditCommand, FocusCommand};
use lapce_core::language::LapceLanguage;
use lapce_core::mode::Mode;
use lapce_core::movement::Movement;
use lapce_rpc::proxy::ProxyResponse;
use lsp_types::{DocumentSymbolResponse, Position, Range, SymbolKind};
use std::cmp::Ordering;
@ -22,6 +21,7 @@
use crate::data::{LapceWorkspace, LapceWorkspaceType};
use crate::document::BufferContent;
use crate::editor::EditorLocation;
use crate::list::ListData;
use crate::panel::PanelKind;
use crate::proxy::path_from_url;
use crate::{
@ -119,7 +119,7 @@ pub enum PaletteStatus {
Done,
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq)]
pub enum PaletteItemContent {
File(PathBuf, PathBuf),
Line(usize, String),
@ -284,7 +284,7 @@ fn select(
}
}
#[derive(Clone, Debug)]
#[derive(Clone, Debug, PartialEq)]
pub struct PaletteItem {
pub content: PaletteItemContent,
pub filter_text: String,
@ -332,22 +332,31 @@ fn with_mut<V, F: FnOnce(&mut PaletteViewData) -> V>(
}
}
/// Data to be held by the palette list
#[derive(Data, Clone)]
pub struct PaletteListData {
/// Should only be `None` when it hasn't been updated initially
/// We need this just for some rendering, and not editing it.
pub workspace: Option<Arc<LapceWorkspace>>,
}
#[derive(Clone)]
pub struct PaletteData {
pub widget_id: WidgetId,
pub scroll_id: WidgetId,
pub status: PaletteStatus,
/// Holds information about the list, including the filtered items
pub list_data: ListData<PaletteItem, PaletteListData>,
pub proxy: Arc<LapceProxy>,
pub palette_type: PaletteType,
pub sender: Sender<(String, String, Vec<PaletteItem>)>,
pub receiver: Option<Receiver<(String, String, Vec<PaletteItem>)>>,
pub sender: Sender<(String, String, im::Vector<PaletteItem>)>,
pub receiver: Option<Receiver<(String, String, im::Vector<PaletteItem>)>>,
pub run_id: String,
pub input: String,
pub cursor: usize,
pub index: usize,
pub has_nonzero_default_index: bool,
pub items: Vec<PaletteItem>,
pub filtered_items: Vec<PaletteItem>,
/// The unfiltered items list
pub total_items: im::Vector<PaletteItem>,
pub preview_editor: WidgetId,
pub input_editor: WidgetId,
}
@ -361,38 +370,6 @@ fn check_condition(&self, condition: &str) -> bool {
matches!(condition, "list_focus" | "palette_focus" | "modal_focus")
}
// fn run_command(
// &mut self,
// ctx: &mut EventCtx,
// command: &LapceCommand,
// _count: Option<usize>,
// _mods: Modifiers,
// _env: &Env,
// ) -> CommandExecuted {
// match command {
// LapceCommand::ModalClose => {
// self.cancel(ctx);
// }
// LapceCommand::DeleteBackward => {
// self.delete_backward(ctx);
// }
// LapceCommand::DeleteToBeginningOfLine => {
// self.delete_to_beginning_of_line(ctx);
// }
// LapceCommand::ListNext => {
// self.next(ctx);
// }
// LapceCommand::ListPrevious => {
// self.previous(ctx);
// }
// LapceCommand::ListSelect => {
// self.select(ctx);
// }
// _ => return CommandExecuted::No,
// }
// CommandExecuted::Yes
// }
fn receive_char(&mut self, ctx: &mut EventCtx, c: &str) {
let palette = Arc::make_mut(&mut self.palette);
palette.input.insert_str(palette.cursor, c);
@ -408,28 +385,22 @@ fn run_command(
_mods: Modifiers,
_env: &Env,
) -> CommandExecuted {
let selected_index = self.palette.list_data.selected_index;
// Pass any commands, like list movement, to the selector list
Arc::make_mut(&mut self.palette)
.list_data
.run_command(ctx, command);
// If the selection changed, then update the preview
if selected_index != self.palette.list_data.selected_index {
self.palette.preview(ctx);
}
match &command.kind {
CommandKind::Focus(cmd) => match cmd {
FocusCommand::ModalClose => {
self.cancel(ctx);
}
FocusCommand::ListNext => {
self.next(ctx);
}
FocusCommand::ListNextPage => {
self.next_page(ctx);
}
FocusCommand::ListPrevious => {
self.previous(ctx);
}
FocusCommand::ListPreviousPage => {
self.previous_page(ctx);
}
FocusCommand::ListSelect => {
self.select(ctx);
}
_ => return CommandExecuted::No,
},
CommandKind::Focus(FocusCommand::ModalClose) => {
self.cancel(ctx);
}
CommandKind::Edit(cmd) => match cmd {
EditCommand::DeleteBackward => {
self.delete_backward(ctx);
@ -446,15 +417,21 @@ fn run_command(
}
impl PaletteData {
pub fn new(proxy: Arc<LapceProxy>) -> Self {
pub fn new(config: Arc<Config>, proxy: Arc<LapceProxy>) -> Self {
let (sender, receiver) = unbounded();
let widget_id = WidgetId::next();
let scroll_id = WidgetId::next();
let preview_editor = WidgetId::next();
let mut list_data =
ListData::new(config, widget_id, PaletteListData { workspace: None });
// TODO: Make these configurable
list_data.line_height = Some(25);
list_data.max_displayed_items = 15;
Self {
widget_id,
scroll_id,
status: PaletteStatus::Inactive,
list_data,
proxy,
palette_type: PaletteType::File,
sender,
@ -462,10 +439,8 @@ pub fn new(proxy: Arc<LapceProxy>) -> Self {
run_id: Uuid::new_v4().to_string(),
input: "".to_string(),
cursor: 0,
index: 0,
has_nonzero_default_index: false,
items: Vec::new(),
filtered_items: Vec::new(),
total_items: im::Vector::new(),
preview_editor,
input_editor: WidgetId::next(),
}
@ -479,28 +454,20 @@ pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn current_items(&self) -> &Vec<PaletteItem> {
pub fn current_items(&self) -> &im::Vector<PaletteItem> {
if self.get_input() == "" {
&self.items
&self.total_items
} else {
&self.filtered_items
&self.list_data.items
}
}
pub fn preview(&self, ctx: &mut EventCtx) {
if let Some(item) = self.get_item() {
if let Some(item) = self.list_data.current_selected_item() {
item.content.select(ctx, true, self.preview_editor);
}
}
pub fn get_item(&self) -> Option<&PaletteItem> {
let items = self.current_items();
if items.is_empty() {
return None;
}
Some(&items[self.index])
}
pub fn get_input(&self) -> &str {
match &self.palette_type {
PaletteType::File => &self.input,
@ -518,19 +485,15 @@ pub fn get_input(&self) -> &str {
}
}
// TODO: Make this configurable
/// The maximum number of palette items to display per 'page'
pub const MAX_PALETTE_ITEMS: usize = 15;
impl PaletteViewData {
pub fn cancel(&mut self, ctx: &mut EventCtx) {
let palette = Arc::make_mut(&mut self.palette);
palette.status = PaletteStatus::Inactive;
palette.input = "".to_string();
palette.cursor = 0;
palette.index = 0;
palette.palette_type = PaletteType::File;
palette.items.clear();
palette.filtered_items.clear();
palette.total_items.clear();
palette.list_data.clear_items();
if let Some(active) = *self.main_split.active_tab {
ctx.submit_command(Command::new(
LAPCE_UI_COMMAND,
@ -552,7 +515,7 @@ pub fn run_references(
locations: &[EditorLocation<Position>],
) {
self.run(ctx, Some(PaletteType::Reference), None);
let items: Vec<PaletteItem> = locations
let items = locations
.iter()
.map(|l| {
let full_path = l.path.clone();
@ -573,8 +536,9 @@ pub fn run_references(
})
.collect();
let palette = Arc::make_mut(&mut self.palette);
palette.items = items;
palette.total_items = items;
palette.preview(ctx);
self.fill_list();
}
pub fn run(
@ -592,11 +556,10 @@ pub fn run(
LapceUICommand::InitPaletteInput(palette.input.clone()),
Target::Widget(*self.main_split.tab_id),
));
palette.items = Vec::new();
palette.filtered_items = Vec::new();
palette.total_items.clear();
palette.list_data.clear_items();
palette.run_id = Uuid::new_v4().to_string();
palette.cursor = palette.input.len();
palette.index = 0;
if let Some(active_editor_content) =
self.main_split.active_editor().map(|e| e.content.clone())
@ -653,6 +616,16 @@ pub fn run(
}
}
}
self.fill_list();
}
/// Fill the list with the stored unfiltered total items
fn fill_list(&mut self) {
if self.palette.input.is_empty() {
Arc::make_mut(&mut self.palette).list_data.items =
self.palette.total_items.clone();
}
}
fn delete_backward(&mut self, ctx: &mut EventCtx) {
@ -696,51 +669,16 @@ pub fn delete_to_beginning_of_line(&mut self, ctx: &mut EventCtx) {
self.update_palette(ctx);
}
pub fn next(&mut self, ctx: &mut EventCtx) {
let palette = Arc::make_mut(&mut self.palette);
palette.index =
Movement::Down.update_index(palette.index, palette.len(), 1, true);
palette.preview(ctx);
}
pub fn next_page(&mut self, ctx: &mut EventCtx) {
let palette = Arc::make_mut(&mut self.palette);
palette.index = Movement::Down.update_index(
palette.index,
palette.len(),
MAX_PALETTE_ITEMS - 1,
false,
);
palette.preview(ctx);
}
pub fn previous(&mut self, ctx: &mut EventCtx) {
let palette = Arc::make_mut(&mut self.palette);
palette.index =
Movement::Up.update_index(palette.index, palette.len(), 1, true);
palette.preview(ctx);
}
pub fn previous_page(&mut self, ctx: &mut EventCtx) {
let palette = Arc::make_mut(&mut self.palette);
palette.index = Movement::Up.update_index(
palette.index,
palette.len(),
MAX_PALETTE_ITEMS - 1,
false,
);
palette.preview(ctx);
}
// TODO: This is a bit weird, its wanting to iterate over items, but it could be called before we fill the list!
fn preselect_matching(&mut self, ctx: &mut EventCtx, matching: &str) {
let palette = Arc::make_mut(&mut self.palette);
if let Some((id, _)) = palette
.items
.total_items
.iter()
.enumerate()
.find(|(_, item)| item.filter_text == matching)
{
palette.index = id;
palette.list_data.selected_index = id;
palette.has_nonzero_default_index = true;
palette.preview(ctx);
}
@ -759,7 +697,7 @@ pub fn select(&mut self, ctx: &mut EventCtx) {
));
}
let palette = Arc::make_mut(&mut self.palette);
if let Some(item) = palette.get_item() {
if let Some(item) = palette.list_data.current_selected_item() {
if item.content.select(ctx, false, palette.preview_editor) {
self.cancel(ctx);
}
@ -803,14 +741,13 @@ pub fn update_input(&mut self, ctx: &mut EventCtx, input: String) {
// Update the current input
palette.input = input;
self.update_palette(ctx)
self.update_palette(ctx);
}
pub fn update_palette(&mut self, ctx: &mut EventCtx) {
let palette = Arc::make_mut(&mut self.palette);
if !palette.has_nonzero_default_index {
palette.index = 0;
palette.list_data.selected_index = 0;
}
palette.has_nonzero_default_index = false;
@ -823,14 +760,17 @@ pub fn update_palette(&mut self, ctx: &mut EventCtx) {
return;
}
if self.palette.get_input() != "" {
if self.palette.get_input() == "" {
self.palette.preview(ctx);
Arc::make_mut(&mut self.palette).list_data.items =
self.palette.total_items.clone();
} else {
// Update the filtering with the input
let _ = self.palette.sender.send((
self.palette.run_id.clone(),
self.palette.get_input().to_string(),
self.palette.items.clone(),
self.palette.total_items.clone(),
));
} else {
self.palette.preview(ctx);
}
}
@ -841,7 +781,7 @@ fn get_files(&self, ctx: &mut EventCtx) {
let event_sink = ctx.get_external_handle();
self.palette.proxy.proxy_rpc.get_files(move |result| {
if let Ok(ProxyResponse::GetFilesResponse { items }) = result {
let items: Vec<PaletteItem> = items
let items: im::Vector<PaletteItem> = items
.iter()
.enumerate()
.map(|(_index, path)| {
@ -882,7 +822,7 @@ fn get_ssh_hosts(&mut self, _ctx: &mut EventCtx) {
}
let palette = Arc::make_mut(&mut self.palette);
palette.items = hosts
palette.total_items = hosts
.iter()
.map(|(user, host)| PaletteItem {
content: PaletteItemContent::SshHost(
@ -899,7 +839,7 @@ fn get_ssh_hosts(&mut self, _ctx: &mut EventCtx) {
fn get_workspaces(&mut self, _ctx: &mut EventCtx) {
let workspaces = Config::recent_workspaces().unwrap_or_default();
let palette = Arc::make_mut(&mut self.palette);
palette.items = workspaces
palette.total_items = workspaces
.into_iter()
.map(|w| {
let text = w
@ -930,7 +870,7 @@ fn get_workspaces(&mut self, _ctx: &mut EventCtx) {
fn get_themes(&mut self, _ctx: &mut EventCtx, config: &Config) {
let palette = Arc::make_mut(&mut self.palette);
palette.items = config
palette.total_items = config
.available_themes
.values()
.sorted_by_key(|(n, _)| n)
@ -947,7 +887,7 @@ fn get_languages(&mut self, _ctx: &mut EventCtx) {
let palette = Arc::make_mut(&mut self.palette);
let mut langs = LapceLanguage::languages();
langs.push("Plain Text".to_string());
palette.items = langs
palette.total_items = langs
.iter()
.sorted()
.map(|n| PaletteItem {
@ -963,7 +903,7 @@ fn get_commands(&mut self, _ctx: &mut EventCtx) {
const EXCLUDED_ITEMS: &[&str] = &["palette.command"];
let palette = Arc::make_mut(&mut self.palette);
palette.items = self
palette.total_items = self
.keypress
.commands
.iter()
@ -989,7 +929,7 @@ fn get_lines(&mut self, _ctx: &mut EventCtx) {
{
let raw = terminal.raw.lock();
let term = &raw.term;
let mut items = Vec::new();
let mut items = im::Vector::new();
let mut last_row: Option<String> = None;
let mut current_line = term.topmost_line().0;
for line in term.topmost_line().0..term.bottommost_line().0 {
@ -1020,11 +960,11 @@ fn get_lines(&mut self, _ctx: &mut EventCtx) {
score: 0,
indices: vec![],
};
items.push(item);
items.push_back(item);
}
}
let palette = Arc::make_mut(&mut self.palette);
palette.items = items;
palette.total_items = items;
}
return;
}
@ -1038,7 +978,7 @@ fn get_lines(&mut self, _ctx: &mut EventCtx) {
let last_line_number = doc.buffer().last_line() + 1;
let last_line_number_len = last_line_number.to_string().len();
let palette = Arc::make_mut(&mut self.palette);
palette.items = doc
palette.total_items = doc
.buffer()
.text()
.lines(0..doc.buffer().len())
@ -1083,7 +1023,7 @@ fn get_document_symbols(&mut self, ctx: &mut EventCtx) {
.proxy_rpc
.get_document_symbols(path, move |result| {
if let Ok(ProxyResponse::GetDocumentSymbols { resp }) = result {
let items: Vec<PaletteItem> = match resp {
let items: im::Vector<PaletteItem> = match resp {
DocumentSymbolResponse::Flat(symbols) => symbols
.iter()
.map(|s| {
@ -1156,7 +1096,7 @@ fn get_workspace_symbols(&mut self, ctx: &mut EventCtx) {
if let Ok(ProxyResponse::GetWorkspaceSymbols { symbols }) =
result
{
let items: Vec<PaletteItem> = symbols
let items: im::Vector<PaletteItem> = symbols
.iter()
.map(|s| {
// TODO: Should we be using filter text?
@ -1196,13 +1136,13 @@ fn get_workspace_symbols(&mut self, ctx: &mut EventCtx) {
}
pub fn update_process(
receiver: Receiver<(String, String, Vec<PaletteItem>)>,
receiver: Receiver<(String, String, im::Vector<PaletteItem>)>,
widget_id: WidgetId,
event_sink: ExtEventSink,
) {
fn receive_batch(
receiver: &Receiver<(String, String, Vec<PaletteItem>)>,
) -> Result<(String, String, Vec<PaletteItem>)> {
receiver: &Receiver<(String, String, im::Vector<PaletteItem>)>,
) -> Result<(String, String, im::Vector<PaletteItem>)> {
let (mut run_id, mut input, mut items) = receiver.recv()?;
loop {
match receiver.try_recv() {
@ -1242,10 +1182,10 @@ fn receive_batch(
fn filter_items(
_run_id: &str,
input: &str,
items: Vec<PaletteItem>,
items: im::Vector<PaletteItem>,
matcher: &SkimMatcherV2,
) -> Vec<PaletteItem> {
let mut items: Vec<PaletteItem> = items
) -> im::Vector<PaletteItem> {
let mut items: im::Vector<PaletteItem> = items
.iter()
.filter_map(|i| {
if let Some((score, indices)) =

View File

@ -1,4 +1,4 @@
use std::fmt::Display;
use std::{fmt::Display, sync::Arc};
use anyhow::Error;
use druid::{
@ -14,6 +14,7 @@
completion::{CompletionData, CompletionStatus, ScoredCompletionItem},
config::{Config, LapceTheme},
data::LapceTabData,
list::ListData,
markdown::parse_markdown,
rich_text::{RichText, RichTextBuilder},
};
@ -22,6 +23,7 @@
use std::str::FromStr;
use crate::{
list::{List, ListPaint},
scroll::{LapceIdentityWrapper, LapceScroll},
svg::completion_svg,
};
@ -182,8 +184,8 @@ pub struct CompletionContainer {
id: WidgetId,
scroll_id: WidgetId,
completion: WidgetPod<
LapceTabData,
LapceIdentityWrapper<LapceScroll<LapceTabData, Completion>>,
ListData<ScoredCompletionItem, ()>,
List<ScoredCompletionItem, ()>,
>,
completion_content_size: Size,
documentation: WidgetPod<
@ -195,10 +197,7 @@ pub struct CompletionContainer {
impl CompletionContainer {
pub fn new(data: &CompletionData) -> Self {
let completion = LapceIdentityWrapper::wrap(
LapceScroll::new(Completion::new()).vertical(),
data.scroll_id,
);
let completion = List::new(data.scroll_id);
let completion_doc = LapceIdentityWrapper::wrap(
LapceScroll::new(CompletionDocumentation::new()).vertical(),
data.documentation_scroll_id,
@ -220,19 +219,15 @@ pub fn ensure_item_visible(
env: &Env,
) {
let width = ctx.size().width;
let line_height = data.config.editor.line_height() as f64;
let line_height = data.completion.completion_list.line_height() as f64;
let rect = Size::new(width, line_height)
.to_rect()
.with_origin(Point::new(
0.0,
data.completion.index as f64 * line_height,
data.completion.completion_list.selected_index as f64 * line_height,
));
if self
.completion
.widget_mut()
.inner_mut()
.scroll_to_visible(rect, env)
{
if self.completion.widget_mut().scroll_to_visible(rect, env) {
ctx.submit_command(Command::new(
LAPCE_UI_COMMAND,
LapceUICommand::ResetFade,
@ -248,9 +243,12 @@ pub fn ensure_item_top_visible(
ctx: &mut EventCtx,
data: &LapceTabData,
) {
let line_height = data.config.editor.line_height() as f64;
let point = Point::new(0.0, data.completion.index as f64 * line_height);
if self.completion.widget_mut().inner_mut().scroll_to(point) {
let line_height = data.completion.completion_list.line_height() as f64;
let point = Point::new(
0.0,
data.completion.completion_list.selected_index as f64 * line_height,
);
if self.completion.widget_mut().scroll_to(point) {
ctx.submit_command(Command::new(
LAPCE_UI_COMMAND,
LapceUICommand::ResetFade,
@ -266,7 +264,8 @@ fn update_documentation(&mut self, data: &LapceTabData) {
let documentation = if data.config.editor.completion_show_documentation {
let current_item = (!data.completion.is_empty())
.then(|| data.completion.current_item());
.then(|| data.completion.current_item())
.flatten();
current_item.and_then(|item| item.item.documentation.as_ref())
} else {
@ -308,7 +307,35 @@ fn event(
data: &mut LapceTabData,
env: &Env,
) {
self.completion.event(ctx, event, data, env);
match event {
Event::Command(cmd) if cmd.is(LAPCE_UI_COMMAND) => {
let command = cmd.get_unchecked(LAPCE_UI_COMMAND);
if let LapceUICommand::ListItemSelected = command {
if let Some(editor) = data
.main_split
.active
.and_then(|active| data.main_split.editors.get(&active))
.cloned()
{
let mut editor_data =
data.editor_view_content(editor.view_id);
let doc = editor_data.doc.clone();
editor_data.completion_item_select(ctx);
data.update_from_editor_buffer_data(
editor_data,
&editor,
&doc,
);
}
}
}
_ => {}
}
let completion = Arc::make_mut(&mut data.completion);
completion.completion_list.update_data(data.config.clone());
self.completion
.event(ctx, event, &mut completion.completion_list, env);
self.documentation.event(ctx, event, data, env);
}
@ -319,7 +346,15 @@ fn lifecycle(
data: &LapceTabData,
env: &Env,
) {
self.completion.lifecycle(ctx, event, data, env);
self.completion.lifecycle(
ctx,
event,
&data
.completion
.completion_list
.clone_with(data.config.clone()),
env,
);
self.documentation.lifecycle(ctx, event, data, env);
}
@ -353,14 +388,8 @@ fn update(
if old_data.completion.input != data.completion.input
|| old_data.completion.request_id != data.completion.request_id
|| old_data.completion.status != data.completion.status
|| !old_data
.completion
.current_items()
.same(data.completion.current_items())
|| !old_data
.completion
.filtered_items
.same(&data.completion.filtered_items)
|| old_data.completion.completion_list.items
!= data.completion.completion_list.items
{
self.update_documentation(data);
ctx.request_layout();
@ -377,7 +406,9 @@ fn update(
));
}
if old_completion.index != completion.index {
if old_completion.completion_list.selected_index
!= completion.completion_list.selected_index
{
self.ensure_item_visible(ctx, data, env);
self.update_documentation(data);
ctx.request_paint();
@ -393,20 +424,38 @@ fn update(
{
ctx.request_layout();
}
self.completion.update(
ctx,
&data
.completion
.completion_list
.clone_with(data.config.clone()),
env,
);
}
}
fn layout(
&mut self,
ctx: &mut LayoutCtx,
_bc: &BoxConstraints,
bc: &BoxConstraints,
data: &LapceTabData,
env: &Env,
) -> Size {
let completion_size = data.completion.size;
let bc = BoxConstraints::new(Size::ZERO, completion_size);
self.completion_content_size = self.completion.layout(ctx, &bc, data, env);
self.completion.set_origin(ctx, data, env, Point::ZERO);
// TODO: Let this be configurable
let width = 400.0;
let bc = BoxConstraints::tight(Size::new(width, bc.max().height));
let completion_list = data
.completion
.completion_list
.clone_with(data.config.clone());
self.completion_content_size =
self.completion.layout(ctx, &bc, &completion_list, env);
self.completion
.set_origin(ctx, &completion_list, env, Point::ZERO);
// Position the documentation over the current completion item to the right
let documentation_size = data.completion.documentation_size;
@ -423,8 +472,10 @@ fn layout(
ctx.set_paint_insets((10.0, 10.0, 10.0, 10.0));
Size::new(
completion_size.width + documentation_size.width,
completion_size.height.max(documentation_size.height),
self.completion_content_size.width + documentation_size.width,
self.completion_content_size
.height
.max(documentation_size.height),
)
}
@ -433,6 +484,15 @@ fn paint(&mut self, ctx: &mut PaintCtx, data: &LapceTabData, env: &Env) {
&& data.completion.len() > 0
{
let rect = self.completion_content_size.to_rect();
// Draw the background
ctx.fill(
rect,
data.config
.get_color_unchecked(LapceTheme::COMPLETION_BACKGROUND),
);
// Draw the shadow
let shadow_width = data.config.ui.drop_shadow_width() as f64;
if shadow_width > 0.0 {
ctx.blurred_rect(
@ -448,160 +508,91 @@ fn paint(&mut self, ctx: &mut PaintCtx, data: &LapceTabData, env: &Env) {
1.0,
);
}
self.completion.paint(ctx, data, env);
// Draw the completion list over the background
self.completion.paint(
ctx,
&data
.completion
.completion_list
.clone_with(data.config.clone()),
env,
);
// Draw the documentation to the side
self.documentation.paint(ctx, data, env);
}
}
}
pub struct Completion {}
impl Completion {
pub fn new() -> Self {
Self {}
}
}
impl Default for Completion {
fn default() -> Self {
Self::new()
}
}
impl Widget<LapceTabData> for Completion {
fn event(
&mut self,
_ctx: &mut EventCtx,
_event: &Event,
_data: &mut LapceTabData,
impl<D: Data> ListPaint<D> for ScoredCompletionItem {
fn paint(
&self,
ctx: &mut PaintCtx,
data: &ListData<Self, D>,
_env: &Env,
line: usize,
) {
}
fn lifecycle(
&mut self,
_ctx: &mut LifeCycleCtx,
_event: &LifeCycle,
_data: &LapceTabData,
_env: &Env,
) {
}
fn update(
&mut self,
_ctx: &mut UpdateCtx,
_old_data: &LapceTabData,
_data: &LapceTabData,
_env: &Env,
) {
}
fn layout(
&mut self,
_ctx: &mut LayoutCtx,
bc: &BoxConstraints,
data: &LapceTabData,
_env: &Env,
) -> Size {
let line_height = data.config.editor.line_height() as f64;
let height = data.completion.len();
let height = height as f64 * line_height;
Size::new(bc.max().width, height)
}
fn paint(&mut self, ctx: &mut PaintCtx, data: &LapceTabData, _env: &Env) {
if data.completion.status == CompletionStatus::Inactive {
return;
}
let line_height = data.config.editor.line_height() as f64;
let rect = ctx.region().bounding_box();
let size = ctx.size();
let _input = &data.completion.input;
let items: &Vec<ScoredCompletionItem> = data.completion.current_items();
ctx.fill(
rect,
data.config
.get_color_unchecked(LapceTheme::COMPLETION_BACKGROUND),
);
let start_line = (rect.y0 / line_height).floor() as usize;
let end_line = (rect.y1 / line_height).ceil() as usize;
for line in start_line..end_line {
if line >= items.len() {
break;
}
if line == data.completion.index {
ctx.fill(
Rect::ZERO
.with_origin(Point::new(0.0, line as f64 * line_height))
.with_size(Size::new(size.width, line_height)),
data.config
.get_color_unchecked(LapceTheme::COMPLETION_CURRENT),
);
}
let item = &items[line];
if let Some((svg, color)) = completion_svg(item.item.kind, &data.config)
{
let color = color.unwrap_or_else(|| {
data.config
.get_color_unchecked(LapceTheme::EDITOR_FOREGROUND)
.clone()
});
let rect = Size::new(line_height, line_height)
.to_rect()
.with_origin(Point::new(0.0, line_height * line as f64));
ctx.fill(rect, &color.clone().with_alpha(0.3));
let width = 16.0;
let height = 16.0;
let rect =
Size::new(width, height).to_rect().with_origin(Point::new(
(line_height - width) / 2.0,
(line_height - height) / 2.0 + line_height * line as f64,
));
ctx.draw_svg(&svg, rect, Some(&color));
}
let focus_color =
data.config.get_color_unchecked(LapceTheme::EDITOR_FOCUS);
let content = item.item.label.as_str();
let mut text_layout = ctx
.text()
.new_text_layout(content.to_string())
.font(
FontFamily::new_unchecked(
data.config.editor.font_family.clone(),
),
data.config.editor.font_size as f64,
)
.text_color(
data.config
.get_color_unchecked(LapceTheme::EDITOR_FOREGROUND)
.clone(),
);
for i in &item.indices {
let i = *i;
text_layout = text_layout.range_attribute(
i..i + 1,
TextAttribute::TextColor(focus_color.clone()),
);
text_layout = text_layout.range_attribute(
i..i + 1,
TextAttribute::Weight(FontWeight::BOLD),
);
}
let text_layout = text_layout.build().unwrap();
let y = line_height * line as f64 + text_layout.y_offset(line_height);
let point = Point::new(line_height + 5.0, y);
ctx.draw_text(&text_layout, point);
let line_height = data.line_height() as f64;
if line == data.selected_index {
ctx.fill(
Rect::ZERO
.with_origin(Point::new(0.0, line as f64 * line_height))
.with_size(Size::new(size.width, line_height)),
data.config
.get_color_unchecked(LapceTheme::COMPLETION_CURRENT),
);
}
if let Some((svg, color)) = completion_svg(self.item.kind, &data.config) {
let color = color.unwrap_or_else(|| {
data.config
.get_color_unchecked(LapceTheme::EDITOR_FOREGROUND)
.clone()
});
let rect = Size::new(line_height, line_height)
.to_rect()
.with_origin(Point::new(0.0, line_height * line as f64));
ctx.fill(rect, &color.clone().with_alpha(0.3));
let width = 16.0;
let height = 16.0;
let rect = Size::new(width, height).to_rect().with_origin(Point::new(
(line_height - width) / 2.0,
(line_height - height) / 2.0 + line_height * line as f64,
));
ctx.draw_svg(&svg, rect, Some(&color));
}
let focus_color = data.config.get_color_unchecked(LapceTheme::EDITOR_FOCUS);
let content = self.item.label.as_str();
let mut text_layout = ctx
.text()
.new_text_layout(content.to_string())
.font(
FontFamily::new_unchecked(data.config.editor.font_family.clone()),
data.config.editor.font_size as f64,
)
.text_color(
data.config
.get_color_unchecked(LapceTheme::EDITOR_FOREGROUND)
.clone(),
);
for i in &self.indices {
let i = *i;
text_layout = text_layout.range_attribute(
i..i + 1,
TextAttribute::TextColor(focus_color.clone()),
);
text_layout = text_layout
.range_attribute(i..i + 1, TextAttribute::Weight(FontWeight::BOLD));
}
let text_layout = text_layout.build().unwrap();
let y = line_height * line as f64 + text_layout.y_offset(line_height);
let point = Point::new(line_height + 5.0, y);
ctx.draw_text(&text_layout, point);
}
}

View File

@ -8,6 +8,7 @@
pub mod find;
pub mod hover;
pub mod keymap;
pub mod list;
mod logging;
pub mod palette;
pub mod panel;

292
lapce-ui/src/list.rs Normal file
View File

@ -0,0 +1,292 @@
use std::marker::PhantomData;
use druid::{
BoxConstraints, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx,
PaintCtx, Point, Rect, RenderContext, Size, UpdateCtx, Widget, WidgetId,
WidgetPod,
};
use lapce_data::{config::LapceTheme, list::ListData};
use crate::scroll::{LapceIdentityWrapper, LapceScroll};
pub struct DropdownButton<T: Clone + ListPaint<D> + PartialEq + 'static, D: Data> {
pub list: WidgetPod<ListData<T, D>, List<T, D>>,
}
// TODO: Support optional multi-select
/// Contains a list of choices of type `T`
/// The type must be cloneable, list-paintable, and able to be compared.
/// `D` is associated data that users of `List` may need, since the painting is only given
/// `&ListData<T, D>` for use.
///
/// We let the `KeyPressFocus` be handled by the containing widget, since widgets like palette
/// want focus for themselves. You can call `ListData::run_command` to use its sensible
/// defaults for movement and the like.
pub struct List<T: Clone + ListPaint<D> + PartialEq + 'static, D: Data> {
content_rect: Rect,
// I don't see a way to break this apart that doesn't make it less clear
#[allow(clippy::type_complexity)]
content: WidgetPod<
ListData<T, D>,
LapceIdentityWrapper<LapceScroll<ListData<T, D>, ListContent<T, D>>>,
>,
}
impl<T: Clone + ListPaint<D> + PartialEq + 'static, D: Data> List<T, D> {
pub fn new(scroll_id: WidgetId) -> List<T, D> {
let content = LapceIdentityWrapper::wrap(
LapceScroll::new(ListContent::new()).vertical(),
scroll_id,
);
List {
content_rect: Rect::ZERO,
content: WidgetPod::new(content),
}
}
pub fn scroll_to(&mut self, point: Point) -> bool {
self.content.widget_mut().inner_mut().scroll_to(point)
}
pub fn scroll_to_visible(&mut self, rect: Rect, env: &Env) -> bool {
self.content
.widget_mut()
.inner_mut()
.scroll_to_visible(rect, env)
}
}
impl<T: Clone + ListPaint<D> + PartialEq + 'static, D: Data> Widget<ListData<T, D>>
for List<T, D>
{
fn event(
&mut self,
ctx: &mut EventCtx,
event: &Event,
data: &mut ListData<T, D>,
env: &Env,
) {
self.content.event(ctx, event, data, env);
}
fn lifecycle(
&mut self,
ctx: &mut LifeCycleCtx,
event: &LifeCycle,
data: &ListData<T, D>,
env: &Env,
) {
self.content.lifecycle(ctx, event, data, env);
}
fn update(
&mut self,
ctx: &mut UpdateCtx,
_old_data: &ListData<T, D>,
data: &ListData<T, D>,
env: &Env,
) {
self.content.update(ctx, data, env);
}
fn layout(
&mut self,
ctx: &mut LayoutCtx,
bc: &BoxConstraints,
data: &ListData<T, D>,
env: &Env,
) -> Size {
// TODO: Allow restricting the max height? Technically that can just be done by restricting the
// maximum number of displayed items
// The width are given by whatever widget contains the list
let width = bc.max().width;
let line_height = data.line_height() as f64;
let count = data.max_display_count();
// The height of the rendered entries
let height = count as f64 * line_height;
// TODO: Have an option to let us fill the rest of the space with empty background
// Since some lists may want to take up all the space they're given
// Create a bc which only contains the list we're actually rendering
let bc = BoxConstraints::tight(Size::new(width, height));
let content_size = self.content.layout(ctx, &bc, data, env);
self.content.set_origin(ctx, data, env, Point::ZERO);
let mut content_height = content_size.height;
// Add padding to the bottom
if content_height > 0.0 {
content_height += 5.0;
}
let self_size = Size::new(content_size.width, content_height);
self.content_rect = self_size.to_rect().with_origin(Point::ZERO);
self_size
}
fn paint(&mut self, ctx: &mut PaintCtx, data: &ListData<T, D>, env: &Env) {
let rect = self.content_rect;
// TODO: Put whether shadows should appear on the list behind a variable
let shadow_width = data.config.ui.drop_shadow_width() as f64;
if shadow_width > 0.0 {
ctx.blurred_rect(
rect,
shadow_width,
data.config
.get_color_unchecked(LapceTheme::LAPCE_DROPDOWN_SHADOW),
);
} else {
ctx.stroke(
rect.inflate(0.5, 0.5),
data.config.get_color_unchecked(LapceTheme::LAPCE_BORDER),
1.0,
);
}
// TODO: We could have the user of this provide a custom color
// Or we could say that they have to fill it? Since something like
// palette also wants to draw their background behind the other bits
// and so, if we painted here, we'd double-paint
// which would be annoying for transparent colors.
// ctx.fill(
// rect,
// data.config
// .get_color_unchecked(LapceTheme::PALETTE_BACKGROUND),
// );
self.content.paint(ctx, data, env);
}
}
/// The actual list of entries
struct ListContent<T: Clone + ListPaint<D> + 'static, D: Data> {
/// The line the mouse was last down upon
mouse_down: usize,
_marker: PhantomData<(*const T, *const D)>,
}
impl<T: Clone + ListPaint<D> + 'static, D: Data> ListContent<T, D> {
pub fn new() -> ListContent<T, D> {
ListContent {
mouse_down: 0,
_marker: PhantomData,
}
}
}
impl<T: Clone + ListPaint<D> + 'static, D: Data> Widget<ListData<T, D>>
for ListContent<T, D>
{
fn event(
&mut self,
ctx: &mut EventCtx,
event: &Event,
data: &mut ListData<T, D>,
_env: &Env,
) {
match event {
Event::MouseMove(_) => {
ctx.set_cursor(&druid::Cursor::Pointer);
ctx.set_handled();
}
Event::MouseDown(mouse_event) => {
let line =
(mouse_event.pos.y / data.line_height() as f64).floor() as usize;
self.mouse_down = line;
ctx.set_handled();
}
Event::MouseUp(mouse_event) => {
// TODO: function for translating mouse pos to the line, so that we don't repeat
// this calculation; which makes it harder to change later
let line =
(mouse_event.pos.y / data.line_height() as f64).floor() as usize;
if line == self.mouse_down {
data.selected_index = line;
data.select(ctx);
}
ctx.set_handled();
}
_ => {}
}
}
fn lifecycle(
&mut self,
_ctx: &mut LifeCycleCtx,
_event: &LifeCycle,
_data: &ListData<T, D>,
_env: &Env,
) {
}
fn update(
&mut self,
_ctx: &mut UpdateCtx,
_old_data: &ListData<T, D>,
_data: &ListData<T, D>,
_env: &Env,
) {
}
fn layout(
&mut self,
_ctx: &mut LayoutCtx,
bc: &BoxConstraints,
data: &ListData<T, D>,
_env: &Env,
) -> Size {
let line_height = data.line_height() as f64;
// We include the total number of items because we should be in a scroll widget
// and that needs the overall height, not just the rendered height.
let height = line_height * data.items.len() as f64;
Size::new(bc.max().width, height)
}
fn paint(&mut self, ctx: &mut PaintCtx, data: &ListData<T, D>, env: &Env) {
let rect = ctx.region().bounding_box();
let size = ctx.size();
let line_height = data.line_height() as f64;
let start_line = (rect.y0 / line_height).floor() as usize;
let end_line = (rect.y1 / line_height).ceil() as usize;
let count = end_line - start_line;
// Get the items, skip over all items before the start line, and
// ignore all items after the end line
for (line, item) in
data.items.iter().enumerate().skip(start_line).take(count)
{
if line == data.selected_index {
// Create a rect covering the entry at the selected index
let bg_rect = Rect::ZERO
.with_origin(Point::new(0.0, line as f64 * line_height))
.with_size(Size::new(size.width, line_height));
// TODO: Give this its own theme name entry
ctx.fill(
bg_rect,
data.config.get_color_unchecked(LapceTheme::PALETTE_CURRENT),
);
}
item.paint(ctx, data, env, line);
}
}
}
/// A trait for painting relatively simple elements that are put in a list
/// They don't get a say in their layout or custom handling of events
///
/// Takes an immutable reference, due to `data` containing this entry
pub trait ListPaint<D: Data>: Sized + Clone {
fn paint(
&self,
ctx: &mut PaintCtx,
data: &ListData<Self, D>,
env: &Env,
line: usize,
);
}

View File

@ -12,21 +12,21 @@
};
use druid::{FontWeight, Modifiers};
use lapce_data::command::LAPCE_COMMAND;
use lapce_data::config::Config;
use lapce_data::data::LapceWorkspaceType;
use lapce_data::palette::{PaletteItemContent, MAX_PALETTE_ITEMS};
use lapce_data::list::ListData;
use lapce_data::palette::{PaletteItem, PaletteItemContent, PaletteListData};
use lapce_data::{
command::{LapceUICommand, LAPCE_UI_COMMAND},
config::LapceTheme,
data::LapceTabData,
keypress::KeyPressFocus,
palette::{PaletteStatus, PaletteType, PaletteViewData, PaletteViewLens},
palette::{PaletteStatus, PaletteType, PaletteViewData},
};
use lsp_types::SymbolKind;
use crate::list::{List, ListPaint};
use crate::{
editor::view::LapceEditorView,
scroll::{LapceIdentityWrapper, LapceScroll},
svg::{file_svg, symbol_svg},
};
@ -135,13 +135,16 @@ fn event(
LapceUICommand::UpdatePaletteItems(run_id, items) => {
let palette = Arc::make_mut(&mut data.palette);
if &palette.run_id == run_id {
palette.items = items.to_owned();
palette.total_items = items.clone();
palette.preview(ctx);
if palette.get_input() != "" {
if palette.get_input() == "" {
palette.list_data.items =
palette.total_items.clone();
} else {
let _ = palette.sender.send((
palette.run_id.clone(),
palette.get_input().to_string(),
palette.items.clone(),
palette.total_items.clone(),
));
}
}
@ -154,10 +157,13 @@ fn event(
let palette = Arc::make_mut(&mut data.palette);
if &palette.run_id == run_id && palette.get_input() == input
{
palette.filtered_items = filtered_items.to_owned();
palette.list_data.items = filtered_items.clone();
palette.preview(ctx);
}
}
LapceUICommand::ListItemSelected => {
data.palette_view_data().select(ctx);
}
_ => {}
}
}
@ -215,14 +221,11 @@ fn paint(&mut self, ctx: &mut PaintCtx, data: &LapceTabData, env: &Env) {
struct PaletteContainer {
content_rect: Rect,
line_height: f64,
input: WidgetPod<LapceTabData, Box<dyn Widget<LapceTabData>>>,
#[allow(clippy::type_complexity)]
content: WidgetPod<
LapceTabData,
LapceIdentityWrapper<
LapceScroll<LapceTabData, Box<dyn Widget<LapceTabData>>>,
>,
ListData<PaletteItem, PaletteListData>,
List<PaletteItem, PaletteListData>,
>,
preview: WidgetPod<LapceTabData, Box<dyn Widget<LapceTabData>>>,
}
@ -239,11 +242,7 @@ pub fn new(data: &LapceTabData) -> Self {
.hide_header()
.hide_gutter()
.padding((10.0, 5.0, 10.0, 5.0));
let content = LapceIdentityWrapper::wrap(
LapceScroll::new(PaletteContent::new().lens(PaletteViewLens).boxed())
.vertical(),
data.palette.scroll_id,
);
let content = List::new(data.palette.scroll_id);
let preview =
LapceEditorView::new(preview_editor.view_id, WidgetId::next(), None);
Self {
@ -251,7 +250,6 @@ pub fn new(data: &LapceTabData) -> Self {
input: WidgetPod::new(input.boxed()),
content: WidgetPod::new(content),
preview: WidgetPod::new(preview.boxed()),
line_height: 25.0,
}
}
@ -262,19 +260,17 @@ fn ensure_item_visible(
env: &Env,
) {
let width = ctx.size().width;
// TODO: This function could just be on List?
let line_height = data.palette.list_data.line_height() as f64;
let rect =
Size::new(width, self.line_height)
Size::new(width, line_height as f64)
.to_rect()
.with_origin(Point::new(
0.0,
data.palette.index as f64 * self.line_height,
data.palette.list_data.selected_index as f64
* line_height as f64,
));
if self
.content
.widget_mut()
.inner_mut()
.scroll_to_visible(rect, env)
{
if self.content.widget_mut().scroll_to_visible(rect, env) {
ctx.submit_command(Command::new(
LAPCE_UI_COMMAND,
LapceUICommand::ResetFade,
@ -293,7 +289,11 @@ fn event(
env: &Env,
) {
self.input.event(ctx, event, data, env);
self.content.event(ctx, event, data, env);
let palette = Arc::make_mut(&mut data.palette);
palette.list_data.update_data(data.config.clone());
self.content.event(ctx, event, &mut palette.list_data, env);
self.preview.event(ctx, event, data, env);
}
@ -305,7 +305,12 @@ fn lifecycle(
env: &Env,
) {
self.input.lifecycle(ctx, event, data, env);
self.content.lifecycle(ctx, event, data, env);
self.content.lifecycle(
ctx,
event,
&data.palette.list_data.clone_with(data.config.clone()),
env,
);
self.preview.lifecycle(ctx, event, data, env);
}
@ -317,13 +322,18 @@ fn update(
env: &Env,
) {
if old_data.palette.input != data.palette.input
|| old_data.palette.index != data.palette.index
|| old_data.palette.list_data.selected_index
!= data.palette.list_data.selected_index
|| old_data.palette.run_id != data.palette.run_id
{
self.ensure_item_visible(ctx, data, env);
}
self.input.update(ctx, data, env);
self.content.update(ctx, data, env);
self.content.update(
ctx,
&data.palette.list_data.clone_with(data.config.clone()),
env,
);
self.preview.update(ctx, data, env);
}
@ -337,28 +347,29 @@ fn layout(
let width = bc.max().width;
let max_height = bc.max().height;
let bc = BoxConstraints::tight(Size::new(width, bc.max().height));
let bc = BoxConstraints::tight(Size::new(width, max_height));
let input_size = self.input.layout(ctx, &bc, data, env);
self.input.set_origin(ctx, data, env, Point::ZERO);
let height = MAX_PALETTE_ITEMS.min(data.palette.len());
let height = self.line_height * height as f64;
let height = max_height - input_size.height;
let bc = BoxConstraints::tight(Size::new(width, height));
let content_size = self.content.layout(ctx, &bc, data, env);
self.content
.set_origin(ctx, data, env, Point::new(0.0, input_size.height));
let mut content_height = content_size.height;
if content_height > 0.0 {
content_height += 5.0;
}
let content_size =
self.content.layout(ctx, &bc, &data.palette.list_data, env);
self.content.set_origin(
ctx,
&data.palette.list_data.clone_with(data.config.clone()),
env,
Point::new(0.0, input_size.height),
);
let max_preview_height = max_height
- input_size.height
- MAX_PALETTE_ITEMS as f64 * self.line_height
- data.palette.list_data.max_displayed_items as f64
* data.palette.list_data.line_height() as f64
- 5.0;
let preview_height = if data.palette.palette_type.has_preview() {
if content_height > 0.0 {
if content_size.height > 0.0 {
max_preview_height
} else {
0.0
@ -380,13 +391,15 @@ fn layout(
ctx,
data,
env,
Point::new(preview_width / 2.0, input_size.height + content_height),
Point::new(preview_width / 2.0, input_size.height + content_size.height),
);
ctx.set_paint_insets(4000.0);
let self_size =
Size::new(width, input_size.height + content_height + preview_height);
let self_size = Size::new(
width,
input_size.height + content_size.height + preview_height,
);
self.content_rect = Size::new(width, self_size.height)
.to_rect()
.with_origin(Point::new(0.0, 0.0));
@ -424,11 +437,13 @@ fn paint(&mut self, ctx: &mut PaintCtx, data: &LapceTabData, env: &Env) {
return;
}
self.content.paint(ctx, data, env);
self.content.paint(
ctx,
&data.palette.list_data.clone_with(data.config.clone()),
env,
);
if !data.palette.current_items().is_empty()
&& data.palette.palette_type.has_preview()
{
if !data.palette.is_empty() && data.palette.palette_type.has_preview() {
let rect = self.preview.layout_rect();
ctx.fill(
rect,
@ -544,420 +559,6 @@ fn paint(&mut self, ctx: &mut PaintCtx, data: &PaletteViewData, _env: &Env) {
}
}
pub struct PaletteContent {
mouse_down: usize,
line_height: f64,
}
impl PaletteContent {
pub fn new() -> Self {
Self {
mouse_down: 0,
line_height: 25.0,
}
}
fn paint_palette_item(
palette_item_content: &PaletteItemContent,
ctx: &mut PaintCtx,
workspace_path: Option<&Path>,
line: usize,
indices: &[usize],
line_height: f64,
config: &Config,
) {
let (svg, text, text_indices, hint, hint_indices) =
match palette_item_content {
PaletteItemContent::File(path, _) => {
Self::file_paint_items(path, indices)
}
PaletteItemContent::DocumentSymbol {
kind,
name,
container_name,
..
} => {
let text = name.to_string();
let hint =
container_name.clone().unwrap_or_else(|| "".to_string());
let text_indices = indices
.iter()
.filter_map(|i| {
let i = *i;
if i < text.len() {
Some(i)
} else {
None
}
})
.collect();
let hint_indices = indices
.iter()
.filter_map(|i| {
let i = *i;
if i >= text.len() {
Some(i - text.len())
} else {
None
}
})
.collect();
(symbol_svg(kind), text, text_indices, hint, hint_indices)
}
PaletteItemContent::WorkspaceSymbol {
kind,
name,
location,
..
} => Self::file_paint_symbols(
&location.path,
indices,
workspace_path,
name.as_str(),
*kind,
),
PaletteItemContent::Line(_, text) => {
(None, text.clone(), indices.to_vec(), "".to_string(), vec![])
}
PaletteItemContent::ReferenceLocation(rel_path, _location) => {
Self::file_paint_items(rel_path, indices)
}
PaletteItemContent::Workspace(w) => {
let text = w.path.as_ref().unwrap().to_str().unwrap();
let text = match &w.kind {
LapceWorkspaceType::Local => text.to_string(),
LapceWorkspaceType::RemoteSSH(user, host) => {
format!("[{user}@{host}] {text}")
}
LapceWorkspaceType::RemoteWSL => {
format!("[wsl] {text}")
}
};
(None, text, indices.to_vec(), "".to_string(), vec![])
}
PaletteItemContent::Command(command) => (
None,
command
.kind
.desc()
.map(|m| m.to_string())
.unwrap_or_else(|| "".to_string()),
indices.to_vec(),
"".to_string(),
vec![],
),
PaletteItemContent::Theme(theme) => (
None,
theme.to_string(),
indices.to_vec(),
"".to_string(),
vec![],
),
PaletteItemContent::Language(name) => (
None,
name.to_string(),
indices.to_vec(),
"".to_string(),
vec![],
),
PaletteItemContent::TerminalLine(_line, content) => (
None,
content.clone(),
indices.to_vec(),
"".to_string(),
vec![],
),
PaletteItemContent::SshHost(user, host) => (
None,
format!("{user}@{host}"),
indices.to_vec(),
"".to_string(),
vec![],
),
};
if let Some(svg) = svg.as_ref() {
let width = 14.0;
let height = 14.0;
let rect = Size::new(width, height).to_rect().with_origin(Point::new(
(line_height - width) / 2.0 + 5.0,
(line_height - height) / 2.0 + line_height * line as f64,
));
ctx.draw_svg(svg, rect, None);
}
let svg_x = match palette_item_content {
&PaletteItemContent::Line(_, _) | &PaletteItemContent::Workspace(_) => {
0.0
}
_ => line_height,
};
let focus_color = config.get_color_unchecked(LapceTheme::EDITOR_FOCUS);
let full_text = if hint.is_empty() {
text.clone()
} else {
text.clone() + " " + &hint
};
let mut text_layout = ctx
.text()
.new_text_layout(full_text.clone())
.font(config.ui.font_family(), config.ui.font_size() as f64)
.text_color(
config
.get_color_unchecked(LapceTheme::EDITOR_FOREGROUND)
.clone(),
);
for &i_start in &text_indices {
let i_end = full_text
.char_indices()
.find(|(i, _)| *i == i_start)
.map(|(_, c)| c.len_utf8() + i_start);
let i_end = if let Some(i_end) = i_end {
i_end
} else {
// Log a warning, but continue as we don't want to crash on a bug
log::warn!(
"Invalid text indices in palette: text: '{}', i_start: {}",
text,
i_start
);
continue;
};
text_layout = text_layout.range_attribute(
i_start..i_end,
TextAttribute::TextColor(focus_color.clone()),
);
text_layout = text_layout.range_attribute(
i_start..i_end,
TextAttribute::Weight(FontWeight::BOLD),
);
}
if !hint.is_empty() {
text_layout = text_layout
.range_attribute(
text.len() + 1..full_text.len(),
TextAttribute::FontSize(13.0),
)
.range_attribute(
text.len() + 1..full_text.len(),
TextAttribute::TextColor(
config.get_color_unchecked(LapceTheme::EDITOR_DIM).clone(),
),
);
for i in &hint_indices {
let i = *i + text.len() + 1;
text_layout = text_layout.range_attribute(
i..i + 1,
TextAttribute::TextColor(focus_color.clone()),
);
text_layout = text_layout.range_attribute(
i..i + 1,
TextAttribute::Weight(FontWeight::BOLD),
);
}
}
let text_layout = text_layout.build().unwrap();
let x = svg_x + 5.0;
let y = line_height * line as f64 + text_layout.y_offset(line_height);
let point = Point::new(x, y);
ctx.draw_text(&text_layout, point);
}
fn file_paint_symbols(
path: &Path,
indices: &[usize],
workspace_path: Option<&Path>,
name: &str,
kind: SymbolKind,
) -> (Option<Svg>, String, Vec<usize>, String, Vec<usize>) {
let text = name.to_string();
let hint = path.to_string_lossy();
// Remove the workspace prefix from the path
let hint = workspace_path
.and_then(Path::to_str)
.and_then(|x| hint.strip_prefix(x))
.map(|x| x.strip_prefix('/').unwrap_or(x))
.map(ToString::to_string)
.unwrap_or_else(|| hint.to_string());
let text_indices = indices
.iter()
.filter_map(|i| {
let i = *i;
if i < text.len() {
Some(i)
} else {
None
}
})
.collect();
let hint_indices = indices
.iter()
.filter_map(|i| {
let i = *i;
if i >= text.len() {
Some(i - text.len())
} else {
None
}
})
.collect();
(symbol_svg(&kind), text, text_indices, hint, hint_indices)
}
fn file_paint_items(
path: &Path,
indices: &[usize],
) -> (Option<Svg>, String, Vec<usize>, String, Vec<usize>) {
let (svg, _) = file_svg(path);
let file_name = path
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("")
.to_string();
let folder = path
.parent()
.and_then(|s| s.to_str())
.unwrap_or("")
.to_string();
let folder_len = folder.len();
let text_indices: Vec<usize> = indices
.iter()
.filter_map(|i| {
let i = *i;
if folder_len > 0 {
if i > folder_len {
Some(i - folder_len - 1)
} else {
None
}
} else {
Some(i)
}
})
.collect();
let hint_indices: Vec<usize> = indices
.iter()
.filter_map(|i| {
let i = *i;
if i < folder_len {
Some(i)
} else {
None
}
})
.collect();
(Some(svg), file_name, text_indices, folder, hint_indices)
}
}
impl Default for PaletteContent {
fn default() -> Self {
Self::new()
}
}
impl Widget<PaletteViewData> for PaletteContent {
fn event(
&mut self,
ctx: &mut EventCtx,
event: &Event,
data: &mut PaletteViewData,
_env: &Env,
) {
match event {
Event::MouseMove(_mouse_event) => {
ctx.set_cursor(&druid::Cursor::Pointer);
}
Event::MouseDown(mouse_event) => {
let line = (mouse_event.pos.y / self.line_height).floor() as usize;
self.mouse_down = line;
ctx.set_handled();
}
Event::MouseUp(mouse_event) => {
let line = (mouse_event.pos.y / self.line_height).floor() as usize;
if line == self.mouse_down {
let palette = Arc::make_mut(&mut data.palette);
palette.index = line;
data.select(ctx);
}
ctx.set_handled();
}
_ => (),
}
}
fn lifecycle(
&mut self,
_ctx: &mut LifeCycleCtx,
_event: &LifeCycle,
_data: &PaletteViewData,
_env: &Env,
) {
}
fn update(
&mut self,
_ctx: &mut UpdateCtx,
_old_data: &PaletteViewData,
_data: &PaletteViewData,
_env: &Env,
) {
}
fn layout(
&mut self,
_ctx: &mut LayoutCtx,
bc: &BoxConstraints,
data: &PaletteViewData,
_env: &Env,
) -> Size {
let height = self.line_height * data.palette.len() as f64;
Size::new(bc.max().width, height)
}
fn paint(&mut self, ctx: &mut PaintCtx, data: &PaletteViewData, _env: &Env) {
let rect = ctx.region().bounding_box();
let size = ctx.size();
let items = data.palette.current_items();
let start_line = (rect.y0 / self.line_height).floor() as usize;
let end_line = (rect.y1 / self.line_height).ceil() as usize;
let workspace_path = data.workspace.path.as_deref();
for line in start_line..end_line {
if line >= items.len() {
break;
}
if line == data.palette.index {
ctx.fill(
Rect::ZERO
.with_origin(Point::new(0.0, line as f64 * self.line_height))
.with_size(Size::new(size.width, self.line_height)),
data.config.get_color_unchecked(LapceTheme::PALETTE_CURRENT),
);
}
let item = &items[line];
Self::paint_palette_item(
&item.content,
ctx,
workspace_path,
line,
&item.indices,
self.line_height,
&data.config,
);
}
}
}
pub struct PalettePreview {}
impl PalettePreview {
@ -1016,3 +617,340 @@ fn layout(
fn paint(&mut self, _ctx: &mut PaintCtx, _data: &PaletteViewData, _env: &Env) {}
}
struct PaletteItemPaintInfo {
svg: Option<Svg>,
text: String,
text_indices: Vec<usize>,
hint: String,
hint_indices: Vec<usize>,
}
impl PaletteItemPaintInfo {
/// Construct paint info when there is only known text and text indices
fn new_text(text: String, text_indices: Vec<usize>) -> PaletteItemPaintInfo {
PaletteItemPaintInfo {
svg: None,
text,
text_indices,
hint: String::new(),
hint_indices: Vec::new(),
}
}
}
impl ListPaint<PaletteListData> for PaletteItem {
fn paint(
&self,
ctx: &mut PaintCtx,
data: &ListData<Self, PaletteListData>,
_env: &Env,
line: usize,
) {
let PaletteItemPaintInfo {
svg,
text,
text_indices,
hint,
hint_indices,
} = match &self.content {
PaletteItemContent::File(path, _) => {
file_paint_items(path, &self.indices)
}
PaletteItemContent::DocumentSymbol {
kind,
name,
container_name,
..
} => {
let text = name.to_string();
let hint = container_name.clone().unwrap_or_else(|| "".to_string());
let text_indices = self
.indices
.iter()
.filter_map(|i| {
let i = *i;
if i < text.len() {
Some(i)
} else {
None
}
})
.collect();
let hint_indices = self
.indices
.iter()
.filter_map(|i| {
let i = *i;
if i >= text.len() {
Some(i - text.len())
} else {
None
}
})
.collect();
PaletteItemPaintInfo {
svg: symbol_svg(kind),
text,
text_indices,
hint,
hint_indices,
}
}
PaletteItemContent::WorkspaceSymbol {
kind,
name,
location,
..
} => file_paint_symbols(
&location.path,
&self.indices,
data.data
.workspace
.as_ref()
.and_then(|workspace| workspace.path.as_deref()),
name.as_str(),
*kind,
),
PaletteItemContent::Line(_, text) => {
PaletteItemPaintInfo::new_text(text.clone(), self.indices.to_vec())
}
PaletteItemContent::ReferenceLocation(rel_path, _location) => {
file_paint_items(rel_path, &self.indices)
}
PaletteItemContent::Workspace(w) => {
let text = w.path.as_ref().unwrap().to_str().unwrap();
let text = match &w.kind {
LapceWorkspaceType::Local => text.to_string(),
LapceWorkspaceType::RemoteSSH(user, host) => {
format!("[{user}@{host}] {text}")
}
LapceWorkspaceType::RemoteWSL => {
format!("[wsl] {text}")
}
};
PaletteItemPaintInfo::new_text(text, self.indices.to_vec())
}
PaletteItemContent::Command(command) => {
let text = command
.kind
.desc()
.map(|m| m.to_string())
.unwrap_or_else(|| "".to_string());
PaletteItemPaintInfo::new_text(text, self.indices.to_vec())
}
PaletteItemContent::Theme(theme) => PaletteItemPaintInfo::new_text(
theme.to_string(),
self.indices.to_vec(),
),
PaletteItemContent::Language(name) => PaletteItemPaintInfo::new_text(
name.to_string(),
self.indices.to_vec(),
),
PaletteItemContent::TerminalLine(_line, content) => {
PaletteItemPaintInfo::new_text(
content.clone(),
self.indices.to_vec(),
)
}
PaletteItemContent::SshHost(user, host) => {
PaletteItemPaintInfo::new_text(
format!("{user}@{host}"),
self.indices.to_vec(),
)
}
};
let line_height = data.line_height() as f64;
if let Some(svg) = svg.as_ref() {
let width = 14.0;
let height = 14.0;
let rect = Size::new(width, height).to_rect().with_origin(Point::new(
(line_height - width) / 2.0 + 5.0,
(line_height - height) / 2.0 + line_height * line as f64,
));
ctx.draw_svg(svg, rect, None);
}
let svg_x = match &self.content {
&PaletteItemContent::Line(_, _) | &PaletteItemContent::Workspace(_) => {
0.0
}
_ => line_height,
};
let focus_color = data.config.get_color_unchecked(LapceTheme::EDITOR_FOCUS);
let full_text = if hint.is_empty() {
text.clone()
} else {
text.clone() + " " + &hint
};
let mut text_layout = ctx
.text()
.new_text_layout(full_text.clone())
.font(
data.config.ui.font_family(),
data.config.ui.font_size() as f64,
)
.text_color(
data.config
.get_color_unchecked(LapceTheme::EDITOR_FOREGROUND)
.clone(),
);
for &i_start in &text_indices {
let i_end = full_text
.char_indices()
.find(|(i, _)| *i == i_start)
.map(|(_, c)| c.len_utf8() + i_start);
let i_end = if let Some(i_end) = i_end {
i_end
} else {
// Log a warning, but continue as we don't want to crash on a bug
log::warn!(
"Invalid text indices in palette: text: '{}', i_start: {}",
text,
i_start
);
continue;
};
text_layout = text_layout.range_attribute(
i_start..i_end,
TextAttribute::TextColor(focus_color.clone()),
);
text_layout = text_layout.range_attribute(
i_start..i_end,
TextAttribute::Weight(FontWeight::BOLD),
);
}
if !hint.is_empty() {
text_layout = text_layout
.range_attribute(
text.len() + 1..full_text.len(),
TextAttribute::FontSize(13.0),
)
.range_attribute(
text.len() + 1..full_text.len(),
TextAttribute::TextColor(
data.config
.get_color_unchecked(LapceTheme::EDITOR_DIM)
.clone(),
),
);
for i in &hint_indices {
let i = *i + text.len() + 1;
text_layout = text_layout.range_attribute(
i..i + 1,
TextAttribute::TextColor(focus_color.clone()),
);
text_layout = text_layout.range_attribute(
i..i + 1,
TextAttribute::Weight(FontWeight::BOLD),
);
}
}
let text_layout = text_layout.build().unwrap();
let x = svg_x + 5.0;
let y = line_height * line as f64 + text_layout.y_offset(line_height);
let point = Point::new(x, y);
ctx.draw_text(&text_layout, point);
}
}
fn file_paint_symbols(
path: &Path,
indices: &[usize],
workspace_path: Option<&Path>,
name: &str,
kind: SymbolKind,
) -> PaletteItemPaintInfo {
let text = name.to_string();
let hint = path.to_string_lossy();
// Remove the workspace prefix from the path
let hint = workspace_path
.and_then(Path::to_str)
.and_then(|x| hint.strip_prefix(x))
.map(|x| x.strip_prefix('/').unwrap_or(x))
.map(ToString::to_string)
.unwrap_or_else(|| hint.to_string());
let text_indices = indices
.iter()
.filter_map(|i| {
let i = *i;
if i < text.len() {
Some(i)
} else {
None
}
})
.collect();
let hint_indices = indices
.iter()
.filter_map(|i| {
let i = *i;
if i >= text.len() {
Some(i - text.len())
} else {
None
}
})
.collect();
PaletteItemPaintInfo {
svg: symbol_svg(&kind),
text,
text_indices,
hint,
hint_indices,
}
}
fn file_paint_items(path: &Path, indices: &[usize]) -> PaletteItemPaintInfo {
let (svg, _) = file_svg(path);
let file_name = path
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("")
.to_string();
let folder = path
.parent()
.and_then(|s| s.to_str())
.unwrap_or("")
.to_string();
let folder_len = folder.len();
let text_indices: Vec<usize> = indices
.iter()
.filter_map(|i| {
let i = *i;
if folder_len > 0 {
if i > folder_len {
Some(i - folder_len - 1)
} else {
None
}
} else {
Some(i)
}
})
.collect();
let hint_indices: Vec<usize> = indices
.iter()
.filter_map(|i| {
let i = *i;
if i < folder_len {
Some(i)
} else {
None
}
})
.collect();
PaletteItemPaintInfo {
svg: Some(svg),
text: file_name,
text_indices,
hint: folder,
hint_indices,
}
}

View File

@ -76,7 +76,7 @@ pub struct LapceTab {
id: WidgetId,
pub title: WidgetPod<LapceTabData, Box<dyn Widget<LapceTabData>>>,
main_split: WidgetPod<LapceTabData, Box<dyn Widget<LapceTabData>>>,
completion: WidgetPod<LapceTabData, Box<dyn Widget<LapceTabData>>>,
completion: WidgetPod<LapceTabData, CompletionContainer>,
hover: WidgetPod<LapceTabData, Box<dyn Widget<LapceTabData>>>,
rename: WidgetPod<LapceTabData, Box<dyn Widget<LapceTabData>>>,
status: WidgetPod<LapceTabData, Box<dyn Widget<LapceTabData>>>,
@ -179,7 +179,7 @@ pub fn new(data: &mut LapceTabData) -> Self {
id: data.id,
title,
main_split: WidgetPod::new(main_split.boxed()),
completion: WidgetPod::new(completion.boxed()),
completion: WidgetPod::new(completion),
hover: WidgetPod::new(hover.boxed()),
rename: WidgetPod::new(rename.boxed()),
picker: WidgetPod::new(picker.boxed()),
@ -2197,9 +2197,13 @@ fn layout(
.set_origin(ctx, data, env, main_split_origin);
if data.completion.status != CompletionStatus::Inactive {
let completion_origin =
data.completion_origin(ctx.text(), self_size, &data.config);
self.completion.layout(ctx, bc, data, env);
let completion_size = self.completion.layout(ctx, bc, data, env);
let completion_origin = data.completion_origin(
ctx.text(),
self_size,
completion_size,
&data.config,
);
self.completion
.set_origin(ctx, data, env, completion_origin);
}