Implement find and improvements for focus
This commit is contained in:
parent
81a0bbc094
commit
f3a8fef8f8
10 changed files with 358 additions and 105 deletions
32
Cargo.lock
generated
32
Cargo.lock
generated
|
|
@ -998,7 +998,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cosmic-config"
|
name = "cosmic-config"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/pop-os/libcosmic#5b2ac941c36e1b7e2b4fa9a9162b64dc10afffef"
|
source = "git+https://github.com/pop-os/libcosmic#68c760e65203c5124844c2083224d8c83010ed1e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atomicwrites",
|
"atomicwrites",
|
||||||
"cosmic-config-derive",
|
"cosmic-config-derive",
|
||||||
|
|
@ -1013,7 +1013,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cosmic-config-derive"
|
name = "cosmic-config-derive"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/pop-os/libcosmic#5b2ac941c36e1b7e2b4fa9a9162b64dc10afffef"
|
source = "git+https://github.com/pop-os/libcosmic#68c760e65203c5124844c2083224d8c83010ed1e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
|
|
@ -1083,7 +1083,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cosmic-theme"
|
name = "cosmic-theme"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/pop-os/libcosmic#5b2ac941c36e1b7e2b4fa9a9162b64dc10afffef"
|
source = "git+https://github.com/pop-os/libcosmic#68c760e65203c5124844c2083224d8c83010ed1e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"almost",
|
"almost",
|
||||||
"cosmic-config",
|
"cosmic-config",
|
||||||
|
|
@ -2612,7 +2612,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iced"
|
name = "iced"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
source = "git+https://github.com/pop-os/libcosmic#5b2ac941c36e1b7e2b4fa9a9162b64dc10afffef"
|
source = "git+https://github.com/pop-os/libcosmic#68c760e65203c5124844c2083224d8c83010ed1e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"iced_accessibility",
|
"iced_accessibility",
|
||||||
"iced_core",
|
"iced_core",
|
||||||
|
|
@ -2627,7 +2627,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iced_accessibility"
|
name = "iced_accessibility"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/pop-os/libcosmic#5b2ac941c36e1b7e2b4fa9a9162b64dc10afffef"
|
source = "git+https://github.com/pop-os/libcosmic#68c760e65203c5124844c2083224d8c83010ed1e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"accesskit",
|
"accesskit",
|
||||||
"accesskit_winit",
|
"accesskit_winit",
|
||||||
|
|
@ -2636,7 +2636,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iced_core"
|
name = "iced_core"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
source = "git+https://github.com/pop-os/libcosmic#5b2ac941c36e1b7e2b4fa9a9162b64dc10afffef"
|
source = "git+https://github.com/pop-os/libcosmic#68c760e65203c5124844c2083224d8c83010ed1e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.3.2",
|
"bitflags 1.3.2",
|
||||||
"instant",
|
"instant",
|
||||||
|
|
@ -2652,7 +2652,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iced_futures"
|
name = "iced_futures"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
source = "git+https://github.com/pop-os/libcosmic#5b2ac941c36e1b7e2b4fa9a9162b64dc10afffef"
|
source = "git+https://github.com/pop-os/libcosmic#68c760e65203c5124844c2083224d8c83010ed1e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures",
|
"futures",
|
||||||
"iced_core",
|
"iced_core",
|
||||||
|
|
@ -2665,7 +2665,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iced_graphics"
|
name = "iced_graphics"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
source = "git+https://github.com/pop-os/libcosmic#5b2ac941c36e1b7e2b4fa9a9162b64dc10afffef"
|
source = "git+https://github.com/pop-os/libcosmic#68c760e65203c5124844c2083224d8c83010ed1e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.3.2",
|
"bitflags 1.3.2",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
|
|
@ -2688,7 +2688,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iced_renderer"
|
name = "iced_renderer"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
source = "git+https://github.com/pop-os/libcosmic#5b2ac941c36e1b7e2b4fa9a9162b64dc10afffef"
|
source = "git+https://github.com/pop-os/libcosmic#68c760e65203c5124844c2083224d8c83010ed1e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"iced_graphics",
|
"iced_graphics",
|
||||||
"iced_tiny_skia",
|
"iced_tiny_skia",
|
||||||
|
|
@ -2701,7 +2701,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iced_runtime"
|
name = "iced_runtime"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
source = "git+https://github.com/pop-os/libcosmic#5b2ac941c36e1b7e2b4fa9a9162b64dc10afffef"
|
source = "git+https://github.com/pop-os/libcosmic#68c760e65203c5124844c2083224d8c83010ed1e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"iced_core",
|
"iced_core",
|
||||||
"iced_futures",
|
"iced_futures",
|
||||||
|
|
@ -2711,7 +2711,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iced_style"
|
name = "iced_style"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
source = "git+https://github.com/pop-os/libcosmic#5b2ac941c36e1b7e2b4fa9a9162b64dc10afffef"
|
source = "git+https://github.com/pop-os/libcosmic#68c760e65203c5124844c2083224d8c83010ed1e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"iced_core",
|
"iced_core",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
|
@ -2721,7 +2721,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iced_tiny_skia"
|
name = "iced_tiny_skia"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
source = "git+https://github.com/pop-os/libcosmic#5b2ac941c36e1b7e2b4fa9a9162b64dc10afffef"
|
source = "git+https://github.com/pop-os/libcosmic#68c760e65203c5124844c2083224d8c83010ed1e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"cosmic-text",
|
"cosmic-text",
|
||||||
|
|
@ -2739,7 +2739,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iced_wgpu"
|
name = "iced_wgpu"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
source = "git+https://github.com/pop-os/libcosmic#5b2ac941c36e1b7e2b4fa9a9162b64dc10afffef"
|
source = "git+https://github.com/pop-os/libcosmic#68c760e65203c5124844c2083224d8c83010ed1e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.3.2",
|
"bitflags 1.3.2",
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
|
|
@ -2759,7 +2759,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iced_widget"
|
name = "iced_widget"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
source = "git+https://github.com/pop-os/libcosmic#5b2ac941c36e1b7e2b4fa9a9162b64dc10afffef"
|
source = "git+https://github.com/pop-os/libcosmic#68c760e65203c5124844c2083224d8c83010ed1e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"iced_renderer",
|
"iced_renderer",
|
||||||
"iced_runtime",
|
"iced_runtime",
|
||||||
|
|
@ -2773,7 +2773,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iced_winit"
|
name = "iced_winit"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
source = "git+https://github.com/pop-os/libcosmic#5b2ac941c36e1b7e2b4fa9a9162b64dc10afffef"
|
source = "git+https://github.com/pop-os/libcosmic#68c760e65203c5124844c2083224d8c83010ed1e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"iced_graphics",
|
"iced_graphics",
|
||||||
"iced_runtime",
|
"iced_runtime",
|
||||||
|
|
@ -3092,7 +3092,7 @@ checksum = "13e3bf6590cbc649f4d1a3eefc9d5d6eb746f5200ffb04e5e142700b8faa56e7"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libcosmic"
|
name = "libcosmic"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = "git+https://github.com/pop-os/libcosmic#5b2ac941c36e1b7e2b4fa9a9162b64dc10afffef"
|
source = "git+https://github.com/pop-os/libcosmic#68c760e65203c5124844c2083224d8c83010ed1e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"apply",
|
"apply",
|
||||||
"ashpd",
|
"ashpd",
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,9 @@ default-font-size = Default font size
|
||||||
keyboard-shortcuts = Keyboard shortcuts
|
keyboard-shortcuts = Keyboard shortcuts
|
||||||
enable-vim-bindings = Enable Vim bindings
|
enable-vim-bindings = Enable Vim bindings
|
||||||
|
|
||||||
|
# Find
|
||||||
|
find-placeholder = Find...
|
||||||
|
|
||||||
# Menu
|
# Menu
|
||||||
|
|
||||||
## File
|
## File
|
||||||
|
|
|
||||||
3
res/icons/edit-clear-symbolic.svg
Normal file
3
res/icons/edit-clear-symbolic.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M5 2.00305L0 8.00305L5 14.0031H16V2.00305H5ZM7 5.00305H8C8.277 5.00305 8.526 5.11505 8.707 5.29605L10 6.58905L11.293 5.29605C11.3856 5.20288 11.4958 5.129 11.6171 5.0787C11.7385 5.02841 11.8686 5.0027 12 5.00305H13V6.00305C13.0002 6.1344 12.9744 6.26448 12.9241 6.38582C12.8738 6.50715 12.8 6.61735 12.707 6.71005L11.414 8.00305L12.707 9.29605C12.887 9.47605 13 9.72605 13 10.0031V11.0031H12C11.8687 11.0032 11.7386 10.9775 11.6172 10.9272C11.4959 10.8769 11.3857 10.8031 11.293 10.7101L10 9.41705L8.707 10.7101C8.6143 10.8031 8.50409 10.8769 8.38275 10.9272C8.26141 10.9775 8.13134 11.0032 8 11.0031H7V10.0031C7 9.72605 7.112 9.47605 7.293 9.29605L8.586 8.00305L7.293 6.71005C7.19996 6.61735 7.12618 6.50715 7.0759 6.38582C7.02561 6.26448 6.99981 6.1344 7 6.00305V5.00305Z" fill="#232323"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 904 B |
3
res/icons/go-up-symbolic.svg
Normal file
3
res/icons/go-up-symbolic.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M1.44039 10.063L7.50339 4L13.5654 10.063C15.0034 11.5 13.5034 12.75 12.4404 11.688L7.50339 6.75L2.56539 11.688C1.75339 12.5 0.253389 11.25 1.44039 10.063Z" fill="#232323"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 285 B |
|
|
@ -19,6 +19,8 @@ pub enum Action {
|
||||||
CloseProject,
|
CloseProject,
|
||||||
Copy,
|
Copy,
|
||||||
Cut,
|
Cut,
|
||||||
|
Find,
|
||||||
|
FindAndReplace,
|
||||||
NewFile,
|
NewFile,
|
||||||
NewWindow,
|
NewWindow,
|
||||||
OpenFileDialog,
|
OpenFileDialog,
|
||||||
|
|
@ -42,6 +44,8 @@ impl Action {
|
||||||
Self::CloseProject => Message::CloseProject,
|
Self::CloseProject => Message::CloseProject,
|
||||||
Self::Copy => Message::Copy,
|
Self::Copy => Message::Copy,
|
||||||
Self::Cut => Message::Cut,
|
Self::Cut => Message::Cut,
|
||||||
|
Self::Find => Message::Find(Some(false)),
|
||||||
|
Self::FindAndReplace => Message::Find(Some(true)),
|
||||||
Self::NewFile => Message::NewFile,
|
Self::NewFile => Message::NewFile,
|
||||||
Self::NewWindow => Message::NewWindow,
|
Self::NewWindow => Message::NewWindow,
|
||||||
Self::OpenFileDialog => Message::OpenFileDialog,
|
Self::OpenFileDialog => Message::OpenFileDialog,
|
||||||
|
|
@ -111,6 +115,8 @@ impl KeyBind {
|
||||||
bind!([Ctrl], W, CloseFile);
|
bind!([Ctrl], W, CloseFile);
|
||||||
bind!([Ctrl], X, Cut);
|
bind!([Ctrl], X, Cut);
|
||||||
bind!([Ctrl], C, Copy);
|
bind!([Ctrl], C, Copy);
|
||||||
|
bind!([Ctrl], F, Find);
|
||||||
|
bind!([Ctrl], H, FindAndReplace);
|
||||||
bind!([Ctrl], V, Paste);
|
bind!([Ctrl], V, Paste);
|
||||||
bind!([Ctrl], T, NewFile);
|
bind!([Ctrl], T, NewFile);
|
||||||
bind!([Ctrl], N, NewWindow);
|
bind!([Ctrl], N, NewWindow);
|
||||||
|
|
|
||||||
|
|
@ -30,9 +30,11 @@ impl IconCache {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bundle!("edit-clear-symbolic", 16);
|
||||||
bundle!("folder-open-symbolic", 16);
|
bundle!("folder-open-symbolic", 16);
|
||||||
bundle!("go-down-symbolic", 16);
|
bundle!("go-down-symbolic", 16);
|
||||||
bundle!("go-next-symbolic", 16);
|
bundle!("go-next-symbolic", 16);
|
||||||
|
bundle!("go-up-symbolic", 16);
|
||||||
bundle!("list-add-symbolic", 16);
|
bundle!("list-add-symbolic", 16);
|
||||||
bundle!("object-select-symbolic", 16);
|
bundle!("object-select-symbolic", 16);
|
||||||
bundle!("window-close-symbolic", 16);
|
bundle!("window-close-symbolic", 16);
|
||||||
|
|
|
||||||
161
src/main.rs
161
src/main.rs
|
|
@ -181,6 +181,11 @@ pub enum Message {
|
||||||
Cut,
|
Cut,
|
||||||
DefaultFont(usize),
|
DefaultFont(usize),
|
||||||
DefaultFontSize(usize),
|
DefaultFontSize(usize),
|
||||||
|
Find(Option<bool>),
|
||||||
|
FindNext,
|
||||||
|
FindPrevious,
|
||||||
|
FindReplaceValueChanged(String),
|
||||||
|
FindSearchValueChanged(String),
|
||||||
GitProjectStatus(Vec<(String, PathBuf, Vec<GitStatus>)>),
|
GitProjectStatus(Vec<(String, PathBuf, Vec<GitStatus>)>),
|
||||||
Key(keyboard::Modifiers, keyboard::KeyCode),
|
Key(keyboard::Modifiers, keyboard::KeyCode),
|
||||||
NewFile,
|
NewFile,
|
||||||
|
|
@ -241,6 +246,13 @@ impl ContextPage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum Find {
|
||||||
|
None,
|
||||||
|
Find,
|
||||||
|
FindAndReplace,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
core: Core,
|
core: Core,
|
||||||
nav_model: segmented_button::SingleSelectModel,
|
nav_model: segmented_button::SingleSelectModel,
|
||||||
|
|
@ -253,6 +265,11 @@ pub struct App {
|
||||||
font_sizes: Vec<u16>,
|
font_sizes: Vec<u16>,
|
||||||
theme_names: Vec<String>,
|
theme_names: Vec<String>,
|
||||||
context_page: ContextPage,
|
context_page: ContextPage,
|
||||||
|
text_box_id: widget::Id,
|
||||||
|
find_opt: Option<bool>,
|
||||||
|
find_replace_value: String,
|
||||||
|
find_search_id: widget::Id,
|
||||||
|
find_search_value: String,
|
||||||
git_project_status: Option<Vec<(String, PathBuf, Vec<GitStatus>)>>,
|
git_project_status: Option<Vec<(String, PathBuf, Vec<GitStatus>)>>,
|
||||||
projects: Vec<(String, PathBuf)>,
|
projects: Vec<(String, PathBuf)>,
|
||||||
project_search_id: widget::Id,
|
project_search_id: widget::Id,
|
||||||
|
|
@ -440,6 +457,21 @@ impl App {
|
||||||
self.update_config()
|
self.update_config()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_focus(&self) -> Command<Message> {
|
||||||
|
if self.core.window.show_context {
|
||||||
|
match self.context_page {
|
||||||
|
ContextPage::ProjectSearch => {
|
||||||
|
widget::text_input::focus(self.project_search_id.clone())
|
||||||
|
}
|
||||||
|
_ => Command::none(),
|
||||||
|
}
|
||||||
|
} else if self.find_opt.is_some() {
|
||||||
|
widget::text_input::focus(self.find_search_id.clone())
|
||||||
|
} else {
|
||||||
|
widget::text_input::focus(self.text_box_id.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn update_nav_bar_active(&mut self) {
|
fn update_nav_bar_active(&mut self) {
|
||||||
let tab_path_opt = match self.active_tab() {
|
let tab_path_opt = match self.active_tab() {
|
||||||
Some(Tab::Editor(tab)) => tab.path_opt.clone(),
|
Some(Tab::Editor(tab)) => tab.path_opt.clone(),
|
||||||
|
|
@ -504,7 +536,7 @@ impl App {
|
||||||
|
|
||||||
let window_title = format!("{title} - COSMIC Text Editor");
|
let window_title = format!("{title} - COSMIC Text Editor");
|
||||||
self.set_header_title(title.clone());
|
self.set_header_title(title.clone());
|
||||||
self.set_window_title(window_title)
|
Command::batch([self.set_window_title(window_title), self.update_focus()])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn document_statistics(&self) -> Element<Message> {
|
fn document_statistics(&self) -> Element<Message> {
|
||||||
|
|
@ -942,6 +974,11 @@ impl Application for App {
|
||||||
font_sizes,
|
font_sizes,
|
||||||
theme_names,
|
theme_names,
|
||||||
context_page: ContextPage::Settings,
|
context_page: ContextPage::Settings,
|
||||||
|
text_box_id: widget::Id::unique(),
|
||||||
|
find_opt: None,
|
||||||
|
find_replace_value: String::new(),
|
||||||
|
find_search_id: widget::Id::unique(),
|
||||||
|
find_search_value: String::new(),
|
||||||
git_project_status: None,
|
git_project_status: None,
|
||||||
projects: Vec::new(),
|
projects: Vec::new(),
|
||||||
project_search_id: widget::Id::unique(),
|
project_search_id: widget::Id::unique(),
|
||||||
|
|
@ -1021,6 +1058,25 @@ impl Application for App {
|
||||||
Some(&self.nav_model)
|
Some(&self.nav_model)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn on_context_drawer(&mut self) -> Command<Message> {
|
||||||
|
// Focus correct widget
|
||||||
|
self.update_focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO: currently the first escape unfocuses, and the second calls this function
|
||||||
|
fn on_escape(&mut self) -> Command<Message> {
|
||||||
|
if self.core.window.show_context {
|
||||||
|
// Close context drawer if open
|
||||||
|
self.core.window.show_context = false;
|
||||||
|
} else if self.find_opt.is_some() {
|
||||||
|
// Close find if open
|
||||||
|
self.find_opt = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus correct widget
|
||||||
|
self.update_focus()
|
||||||
|
}
|
||||||
|
|
||||||
fn on_nav_select(&mut self, id: nav_bar::Id) -> Command<Message> {
|
fn on_nav_select(&mut self, id: nav_bar::Id) -> Command<Message> {
|
||||||
// Toggle open state and get clone of node data
|
// Toggle open state and get clone of node data
|
||||||
let node_opt = match self.nav_model.data_mut::<ProjectNode>(id) {
|
let node_opt = match self.nav_model.data_mut::<ProjectNode>(id) {
|
||||||
|
|
@ -1160,6 +1216,38 @@ impl Application for App {
|
||||||
log::warn!("failed to find font with index {}", index);
|
log::warn!("failed to find font with index {}", index);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Message::Find(find_opt) => {
|
||||||
|
self.find_opt = find_opt;
|
||||||
|
|
||||||
|
// Focus correct input
|
||||||
|
return self.update_focus();
|
||||||
|
}
|
||||||
|
Message::FindNext => {
|
||||||
|
if !self.find_search_value.is_empty() {
|
||||||
|
if let Some(Tab::Editor(tab)) = self.active_tab() {
|
||||||
|
tab.search(&self.find_search_value, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus correct input
|
||||||
|
return self.update_focus();
|
||||||
|
}
|
||||||
|
Message::FindPrevious => {
|
||||||
|
if !self.find_search_value.is_empty() {
|
||||||
|
if let Some(Tab::Editor(tab)) = self.active_tab() {
|
||||||
|
tab.search(&self.find_search_value, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus correct input
|
||||||
|
return self.update_focus();
|
||||||
|
}
|
||||||
|
Message::FindReplaceValueChanged(value) => {
|
||||||
|
self.find_replace_value = value;
|
||||||
|
}
|
||||||
|
Message::FindSearchValueChanged(value) => {
|
||||||
|
self.find_search_value = value;
|
||||||
|
}
|
||||||
Message::GitProjectStatus(project_status) => {
|
Message::GitProjectStatus(project_status) => {
|
||||||
self.git_project_status = Some(project_status);
|
self.git_project_status = Some(project_status);
|
||||||
}
|
}
|
||||||
|
|
@ -1384,8 +1472,8 @@ impl Application for App {
|
||||||
Message::ProjectSearchResult(project_search_result) => {
|
Message::ProjectSearchResult(project_search_result) => {
|
||||||
self.project_search_result = Some(project_search_result);
|
self.project_search_result = Some(project_search_result);
|
||||||
|
|
||||||
// Ensure input remains focused
|
// Focus correct input
|
||||||
return widget::text_input::focus(self.project_search_id.clone());
|
return self.update_focus();
|
||||||
}
|
}
|
||||||
Message::ProjectSearchSubmit => {
|
Message::ProjectSearchSubmit => {
|
||||||
//TODO: Figure out length requirements?
|
//TODO: Figure out length requirements?
|
||||||
|
|
@ -1595,13 +1683,12 @@ impl Application for App {
|
||||||
|x| x,
|
|x| x,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
ContextPage::ProjectSearch => {
|
|
||||||
// Ensure focus of correct input
|
|
||||||
return widget::text_input::focus(self.project_search_id.clone());
|
|
||||||
}
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure focus of correct input
|
||||||
|
return self.update_focus();
|
||||||
}
|
}
|
||||||
Message::ToggleLineNumbers => {
|
Message::ToggleLineNumbers => {
|
||||||
self.config.line_numbers = !self.config.line_numbers;
|
self.config.line_numbers = !self.config.line_numbers;
|
||||||
|
|
@ -1713,23 +1800,22 @@ impl Application for App {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mut text_box = text_box(&tab.editor, self.config.metrics())
|
let mut text_box = text_box(&tab.editor, self.config.metrics())
|
||||||
|
.id(self.text_box_id.clone())
|
||||||
.on_changed(Message::TabChanged(tab_id))
|
.on_changed(Message::TabChanged(tab_id))
|
||||||
|
.has_context_menu(tab.context_menu.is_some())
|
||||||
.on_context_menu(move |position_opt| {
|
.on_context_menu(move |position_opt| {
|
||||||
Message::TabContextMenu(tab_id, position_opt)
|
Message::TabContextMenu(tab_id, position_opt)
|
||||||
});
|
});
|
||||||
if self.config.line_numbers {
|
if self.config.line_numbers {
|
||||||
text_box = text_box.line_numbers();
|
text_box = text_box.line_numbers();
|
||||||
}
|
}
|
||||||
let tab_element: Element<'_, Message> = match tab.context_menu {
|
let mut popover =
|
||||||
Some(position) => widget::popover(
|
widget::popover(text_box, menu::context_menu(&self.config, tab_id));
|
||||||
text_box.context_menu(position),
|
popover = match tab.context_menu {
|
||||||
menu::context_menu(&self.config, tab_id),
|
Some(position) => popover.position(position),
|
||||||
)
|
None => popover.show_popup(false),
|
||||||
.position(position)
|
|
||||||
.into(),
|
|
||||||
None => text_box.into(),
|
|
||||||
};
|
};
|
||||||
tab_column = tab_column.push(tab_element);
|
tab_column = tab_column.push(popover);
|
||||||
tab_column = tab_column.push(text(status).font(Font::MONOSPACE));
|
tab_column = tab_column.push(text(status).font(Font::MONOSPACE));
|
||||||
}
|
}
|
||||||
Some(Tab::GitDiff(tab)) => {
|
Some(Tab::GitDiff(tab)) => {
|
||||||
|
|
@ -1789,6 +1875,49 @@ impl Application for App {
|
||||||
None => {}
|
None => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(replace) = &self.find_opt {
|
||||||
|
let text_input =
|
||||||
|
widget::text_input::text_input(fl!("find-placeholder"), &self.find_search_value)
|
||||||
|
.id(self.find_search_id.clone())
|
||||||
|
.on_input(Message::FindSearchValueChanged)
|
||||||
|
//TODO: shift+enter for FindPrevious
|
||||||
|
.on_submit(Message::FindNext)
|
||||||
|
.width(Length::Fixed(320.0))
|
||||||
|
.trailing_icon(
|
||||||
|
button(icon_cache_get("edit-clear-symbolic", 16))
|
||||||
|
.on_press(Message::FindSearchValueChanged(String::new()))
|
||||||
|
.style(style::Button::Icon)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
let find_widget = widget::row::with_children(vec![
|
||||||
|
text_input.into(),
|
||||||
|
button(icon_cache_get("go-up-symbolic", 16))
|
||||||
|
.on_press(Message::FindPrevious)
|
||||||
|
.padding(space_xxs)
|
||||||
|
.style(style::Button::Icon)
|
||||||
|
.into(),
|
||||||
|
button(icon_cache_get("go-down-symbolic", 16))
|
||||||
|
.on_press(Message::FindNext)
|
||||||
|
.padding(space_xxs)
|
||||||
|
.style(style::Button::Icon)
|
||||||
|
.into(),
|
||||||
|
widget::horizontal_space(Length::Fill).into(),
|
||||||
|
button(icon_cache_get("window-close-symbolic", 16))
|
||||||
|
.on_press(Message::Find(None))
|
||||||
|
.padding(space_xxs)
|
||||||
|
.style(style::Button::Icon)
|
||||||
|
.into(),
|
||||||
|
])
|
||||||
|
.align_items(Alignment::Center)
|
||||||
|
.padding(space_xxs)
|
||||||
|
.spacing(space_xxs);
|
||||||
|
|
||||||
|
tab_column = tab_column.push(
|
||||||
|
widget::cosmic_container::container(find_widget)
|
||||||
|
.layer(cosmic_theme::Layer::Primary),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let content: Element<_> = tab_column.into();
|
let content: Element<_> = tab_column.into();
|
||||||
|
|
||||||
// Uncomment to debug layout:
|
// Uncomment to debug layout:
|
||||||
|
|
|
||||||
|
|
@ -197,8 +197,8 @@ pub fn menu_bar<'a>(config: &Config) -> Element<'a, Message> {
|
||||||
menu_item(fl!("paste"), Message::Paste),
|
menu_item(fl!("paste"), Message::Paste),
|
||||||
menu_item(fl!("select-all"), Message::SelectAll),
|
menu_item(fl!("select-all"), Message::SelectAll),
|
||||||
MenuTree::new(horizontal_rule(1)),
|
MenuTree::new(horizontal_rule(1)),
|
||||||
menu_key(fl!("find"), "Ctrl + F", Message::Todo),
|
menu_item(fl!("find"), Message::Find(Some(false))),
|
||||||
menu_key(fl!("replace"), "Ctrl + H", Message::Todo),
|
menu_item(fl!("replace"), Message::Find(Some(true))),
|
||||||
menu_item(
|
menu_item(
|
||||||
fl!("find-in-project"),
|
fl!("find-in-project"),
|
||||||
Message::ToggleContextPage(ContextPage::ProjectSearch),
|
Message::ToggleContextPage(ContextPage::ProjectSearch),
|
||||||
|
|
|
||||||
54
src/tab.rs
54
src/tab.rs
|
|
@ -199,4 +199,58 @@ impl EditorTab {
|
||||||
fl!("new-document")
|
fl!("new-document")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Code adapted from cosmic-text ViEditor search
|
||||||
|
pub fn search(&self, value: &str, forwards: bool) -> bool {
|
||||||
|
let mut editor = self.editor.lock().unwrap();
|
||||||
|
let mut cursor = editor.cursor();
|
||||||
|
let start_line = cursor.line;
|
||||||
|
if forwards {
|
||||||
|
while cursor.line < editor.with_buffer(|buffer| buffer.lines.len()) {
|
||||||
|
if let Some(index) = editor.with_buffer(|buffer| {
|
||||||
|
buffer.lines[cursor.line]
|
||||||
|
.text()
|
||||||
|
.match_indices(value)
|
||||||
|
.filter_map(|(i, _)| {
|
||||||
|
if cursor.line != start_line || i > cursor.index {
|
||||||
|
Some(i)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.next()
|
||||||
|
}) {
|
||||||
|
cursor.index = index;
|
||||||
|
editor.set_cursor(cursor);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor.line += 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cursor.line += 1;
|
||||||
|
while cursor.line > 0 {
|
||||||
|
cursor.line -= 1;
|
||||||
|
|
||||||
|
if let Some(index) = editor.with_buffer(|buffer| {
|
||||||
|
buffer.lines[cursor.line]
|
||||||
|
.text()
|
||||||
|
.rmatch_indices(value)
|
||||||
|
.filter_map(|(i, _)| {
|
||||||
|
if cursor.line != start_line || i < cursor.index {
|
||||||
|
Some(i)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.next()
|
||||||
|
}) {
|
||||||
|
cursor.index = index;
|
||||||
|
editor.set_cursor(cursor);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
195
src/text_box.rs
195
src/text_box.rs
|
|
@ -12,7 +12,11 @@ use cosmic::{
|
||||||
image,
|
image,
|
||||||
layout::{self, Layout},
|
layout::{self, Layout},
|
||||||
renderer::{self, Quad},
|
renderer::{self, Quad},
|
||||||
widget::{self, tree, Widget},
|
widget::{
|
||||||
|
self,
|
||||||
|
operation::{self, Operation, OperationOutputWrapper},
|
||||||
|
tree, Id, Widget,
|
||||||
|
},
|
||||||
Shell,
|
Shell,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -29,10 +33,11 @@ use crate::{line_number::LineNumberKey, FONT_SYSTEM, LINE_NUMBER_CACHE, SWASH_CA
|
||||||
pub struct TextBox<'a, Message> {
|
pub struct TextBox<'a, Message> {
|
||||||
editor: &'a Mutex<ViEditor<'static, 'static>>,
|
editor: &'a Mutex<ViEditor<'static, 'static>>,
|
||||||
metrics: Metrics,
|
metrics: Metrics,
|
||||||
|
id: Option<Id>,
|
||||||
padding: Padding,
|
padding: Padding,
|
||||||
on_changed: Option<Message>,
|
on_changed: Option<Message>,
|
||||||
click_timing: Duration,
|
click_timing: Duration,
|
||||||
context_menu: Option<Point>,
|
has_context_menu: bool,
|
||||||
on_context_menu: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
|
on_context_menu: Option<Box<dyn Fn(Option<Point>) -> Message + 'a>>,
|
||||||
line_numbers: bool,
|
line_numbers: bool,
|
||||||
}
|
}
|
||||||
|
|
@ -45,15 +50,21 @@ where
|
||||||
Self {
|
Self {
|
||||||
editor,
|
editor,
|
||||||
metrics,
|
metrics,
|
||||||
|
id: None,
|
||||||
padding: Padding::new(0.0),
|
padding: Padding::new(0.0),
|
||||||
on_changed: None,
|
on_changed: None,
|
||||||
click_timing: Duration::from_millis(500),
|
click_timing: Duration::from_millis(500),
|
||||||
context_menu: None,
|
has_context_menu: false,
|
||||||
on_context_menu: None,
|
on_context_menu: None,
|
||||||
line_numbers: false,
|
line_numbers: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn id(mut self, id: Id) -> Self {
|
||||||
|
self.id = Some(id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
||||||
self.padding = padding.into();
|
self.padding = padding.into();
|
||||||
self
|
self
|
||||||
|
|
@ -69,8 +80,8 @@ where
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn context_menu(mut self, position: Point) -> Self {
|
pub fn has_context_menu(mut self, has_context_menu: bool) -> Self {
|
||||||
self.context_menu = Some(position);
|
self.has_context_menu = has_context_menu;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -252,6 +263,18 @@ where
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn operate(
|
||||||
|
&self,
|
||||||
|
tree: &mut widget::Tree,
|
||||||
|
_layout: Layout<'_>,
|
||||||
|
_renderer: &Renderer,
|
||||||
|
operation: &mut dyn Operation<OperationOutputWrapper<Message>>,
|
||||||
|
) {
|
||||||
|
let state = tree.state.downcast_mut::<State>();
|
||||||
|
|
||||||
|
operation.focusable(state, self.id.as_ref());
|
||||||
|
}
|
||||||
|
|
||||||
fn mouse_interaction(
|
fn mouse_interaction(
|
||||||
&self,
|
&self,
|
||||||
tree: &widget::Tree,
|
tree: &widget::Tree,
|
||||||
|
|
@ -611,79 +634,92 @@ where
|
||||||
Event::Keyboard(KeyEvent::KeyPressed {
|
Event::Keyboard(KeyEvent::KeyPressed {
|
||||||
key_code,
|
key_code,
|
||||||
modifiers,
|
modifiers,
|
||||||
}) => match key_code {
|
}) => {
|
||||||
KeyCode::Left => {
|
// Only parse keys when focused
|
||||||
editor.action(Action::Motion(Motion::Left));
|
if state.is_focused {
|
||||||
status = Status::Captured;
|
match key_code {
|
||||||
}
|
KeyCode::Left => {
|
||||||
KeyCode::Right => {
|
editor.action(Action::Motion(Motion::Left));
|
||||||
editor.action(Action::Motion(Motion::Right));
|
status = Status::Captured;
|
||||||
status = Status::Captured;
|
}
|
||||||
}
|
KeyCode::Right => {
|
||||||
KeyCode::Up => {
|
editor.action(Action::Motion(Motion::Right));
|
||||||
editor.action(Action::Motion(Motion::Up));
|
status = Status::Captured;
|
||||||
status = Status::Captured;
|
}
|
||||||
}
|
KeyCode::Up => {
|
||||||
KeyCode::Down => {
|
editor.action(Action::Motion(Motion::Up));
|
||||||
editor.action(Action::Motion(Motion::Down));
|
status = Status::Captured;
|
||||||
status = Status::Captured;
|
}
|
||||||
}
|
KeyCode::Down => {
|
||||||
KeyCode::Home => {
|
editor.action(Action::Motion(Motion::Down));
|
||||||
editor.action(Action::Motion(Motion::Home));
|
status = Status::Captured;
|
||||||
status = Status::Captured;
|
}
|
||||||
}
|
KeyCode::Home => {
|
||||||
KeyCode::End => {
|
editor.action(Action::Motion(Motion::Home));
|
||||||
editor.action(Action::Motion(Motion::End));
|
status = Status::Captured;
|
||||||
status = Status::Captured;
|
}
|
||||||
}
|
KeyCode::End => {
|
||||||
KeyCode::PageUp => {
|
editor.action(Action::Motion(Motion::End));
|
||||||
editor.action(Action::Motion(Motion::PageUp));
|
status = Status::Captured;
|
||||||
status = Status::Captured;
|
}
|
||||||
}
|
KeyCode::PageUp => {
|
||||||
KeyCode::PageDown => {
|
editor.action(Action::Motion(Motion::PageUp));
|
||||||
editor.action(Action::Motion(Motion::PageDown));
|
status = Status::Captured;
|
||||||
status = Status::Captured;
|
}
|
||||||
}
|
KeyCode::PageDown => {
|
||||||
KeyCode::Escape => {
|
editor.action(Action::Motion(Motion::PageDown));
|
||||||
editor.action(Action::Escape);
|
status = Status::Captured;
|
||||||
status = Status::Captured;
|
}
|
||||||
}
|
KeyCode::Escape => {
|
||||||
KeyCode::Enter => {
|
editor.action(Action::Escape);
|
||||||
editor.action(Action::Enter);
|
status = Status::Captured;
|
||||||
status = Status::Captured;
|
}
|
||||||
}
|
KeyCode::Enter => {
|
||||||
KeyCode::Backspace => {
|
editor.action(Action::Enter);
|
||||||
editor.action(Action::Backspace);
|
status = Status::Captured;
|
||||||
status = Status::Captured;
|
}
|
||||||
}
|
KeyCode::Backspace => {
|
||||||
KeyCode::Delete => {
|
editor.action(Action::Backspace);
|
||||||
editor.action(Action::Delete);
|
status = Status::Captured;
|
||||||
status = Status::Captured;
|
}
|
||||||
}
|
KeyCode::Delete => {
|
||||||
KeyCode::Tab => {
|
editor.action(Action::Delete);
|
||||||
if modifiers.shift() {
|
status = Status::Captured;
|
||||||
editor.action(Action::Unindent);
|
}
|
||||||
} else {
|
KeyCode::Tab => {
|
||||||
editor.action(Action::Indent);
|
if modifiers.shift() {
|
||||||
|
editor.action(Action::Unindent);
|
||||||
|
} else {
|
||||||
|
editor.action(Action::Indent);
|
||||||
|
}
|
||||||
|
status = Status::Captured;
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
}
|
}
|
||||||
status = Status::Captured;
|
|
||||||
}
|
}
|
||||||
_ => (),
|
}
|
||||||
},
|
|
||||||
Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => {
|
Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => {
|
||||||
state.modifiers = modifiers;
|
state.modifiers = modifiers;
|
||||||
}
|
}
|
||||||
Event::Keyboard(KeyEvent::CharacterReceived(character)) => {
|
Event::Keyboard(KeyEvent::CharacterReceived(character)) => {
|
||||||
// Only parse keys when Super, Ctrl, and Alt are not pressed
|
// Only parse keys when focused
|
||||||
if !state.modifiers.logo() && !state.modifiers.control() && !state.modifiers.alt() {
|
if state.is_focused {
|
||||||
if !character.is_control() {
|
// Only parse keys when Super, Ctrl, and Alt are not pressed
|
||||||
editor.action(Action::Insert(character));
|
if !state.modifiers.logo()
|
||||||
|
&& !state.modifiers.control()
|
||||||
|
&& !state.modifiers.alt()
|
||||||
|
{
|
||||||
|
if !character.is_control() {
|
||||||
|
editor.action(Action::Insert(character));
|
||||||
|
}
|
||||||
|
status = Status::Captured;
|
||||||
}
|
}
|
||||||
status = Status::Captured;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::Mouse(MouseEvent::ButtonPressed(button)) => {
|
Event::Mouse(MouseEvent::ButtonPressed(button)) => {
|
||||||
if let Some(p) = cursor_position.position_in(layout.bounds()) {
|
if let Some(p) = cursor_position.position_in(layout.bounds()) {
|
||||||
|
state.is_focused = true;
|
||||||
|
|
||||||
// Handle left click drag
|
// Handle left click drag
|
||||||
if let Button::Left = button {
|
if let Button::Left = button {
|
||||||
let x_logical = p.x - self.padding.left;
|
let x_logical = p.x - self.padding.left;
|
||||||
|
|
@ -746,12 +782,13 @@ where
|
||||||
|
|
||||||
// Update context menu state
|
// Update context menu state
|
||||||
if let Some(on_context_menu) = &self.on_context_menu {
|
if let Some(on_context_menu) = &self.on_context_menu {
|
||||||
shell.publish((on_context_menu)(match self.context_menu {
|
shell.publish((on_context_menu)(if self.has_context_menu {
|
||||||
Some(_) => None,
|
None
|
||||||
None => match button {
|
} else {
|
||||||
|
match button {
|
||||||
Button::Right => Some(p),
|
Button::Right => Some(p),
|
||||||
_ => None,
|
_ => None,
|
||||||
},
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -869,6 +906,7 @@ pub struct State {
|
||||||
click: Option<(ClickKind, Instant)>,
|
click: Option<(ClickKind, Instant)>,
|
||||||
dragging: Option<Dragging>,
|
dragging: Option<Dragging>,
|
||||||
editor_offset_x: Cell<i32>,
|
editor_offset_x: Cell<i32>,
|
||||||
|
is_focused: bool,
|
||||||
scale_factor: Cell<f32>,
|
scale_factor: Cell<f32>,
|
||||||
scroll_pixels: f32,
|
scroll_pixels: f32,
|
||||||
scrollbar_rect: Cell<Rectangle<f32>>,
|
scrollbar_rect: Cell<Rectangle<f32>>,
|
||||||
|
|
@ -883,6 +921,7 @@ impl State {
|
||||||
click: None,
|
click: None,
|
||||||
dragging: None,
|
dragging: None,
|
||||||
editor_offset_x: Cell::new(0),
|
editor_offset_x: Cell::new(0),
|
||||||
|
is_focused: false,
|
||||||
scale_factor: Cell::new(1.0),
|
scale_factor: Cell::new(1.0),
|
||||||
scroll_pixels: 0.0,
|
scroll_pixels: 0.0,
|
||||||
scrollbar_rect: Cell::new(Rectangle::default()),
|
scrollbar_rect: Cell::new(Rectangle::default()),
|
||||||
|
|
@ -890,3 +929,17 @@ impl State {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl operation::Focusable for State {
|
||||||
|
fn is_focused(&self) -> bool {
|
||||||
|
self.is_focused
|
||||||
|
}
|
||||||
|
|
||||||
|
fn focus(&mut self) {
|
||||||
|
self.is_focused = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unfocus(&mut self) {
|
||||||
|
self.is_focused = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue