Implement project search in context drawer
This commit is contained in:
parent
8996394c75
commit
7b0d59785c
10 changed files with 469 additions and 15 deletions
142
Cargo.lock
generated
142
Cargo.lock
generated
|
|
@ -630,6 +630,17 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "542f33a8835a0884b006a0c3df3dadd99c0c3f296ed26c2fdc8028e01ad6230c"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.14.0"
|
||||
|
|
@ -982,8 +993,10 @@ dependencies = [
|
|||
"cosmic-text 0.10.0",
|
||||
"env_logger",
|
||||
"fork",
|
||||
"grep",
|
||||
"i18n-embed",
|
||||
"i18n-embed-fl",
|
||||
"ignore",
|
||||
"lazy_static",
|
||||
"libcosmic",
|
||||
"log",
|
||||
|
|
@ -1020,7 +1033,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "cosmic-text"
|
||||
version = "0.10.0"
|
||||
source = "git+https://github.com/pop-os/cosmic-text#cbd567d2387d1d9803c8f056fab12e66fcf8f044"
|
||||
source = "git+https://github.com/pop-os/cosmic-text#daa5a6615c52d352e9c87d30e1ab35b8dd14bd91"
|
||||
dependencies = [
|
||||
"cosmic_undo_2",
|
||||
"fontdb 0.16.0",
|
||||
|
|
@ -1500,6 +1513,24 @@ version = "1.9.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs_io"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2"
|
||||
version = "0.7.8"
|
||||
|
|
@ -2151,6 +2182,19 @@ version = "0.3.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"bstr",
|
||||
"log",
|
||||
"regex-automata",
|
||||
"regex-syntax 0.8.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glow"
|
||||
version = "0.12.3"
|
||||
|
|
@ -2238,6 +2282,85 @@ dependencies = [
|
|||
"bitflags 2.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "grep"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e2b024ec1e686cb64d78beb852030b0e632af93817f1ed25be0173af0e94939"
|
||||
dependencies = [
|
||||
"grep-cli",
|
||||
"grep-matcher",
|
||||
"grep-printer",
|
||||
"grep-regex",
|
||||
"grep-searcher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "grep-cli"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea40788c059ab8b622c4d074732750bfb3bd2912e2dd58eabc11798a4d5ad725"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"globset",
|
||||
"libc",
|
||||
"log",
|
||||
"termcolor",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "grep-matcher"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47a3141a10a43acfedc7c98a60a834d7ba00dfe7bec9071cbfc19b55b292ac02"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "grep-printer"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "743c12a03c8aee38b6e5bd0168d8ebb09345751323df4a01c56e792b1f38ceb2"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"grep-matcher",
|
||||
"grep-searcher",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "grep-regex"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f748bb135ca835da5cbc67ca0e6955f968db9c5df74ca4f56b18e1ddbc68230d"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"grep-matcher",
|
||||
"log",
|
||||
"regex-automata",
|
||||
"regex-syntax 0.8.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "grep-searcher"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba536ae4f69bec62d8839584dd3153d3028ef31bb229f04e09fb5a9e5a193c54"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"encoding_rs",
|
||||
"encoding_rs_io",
|
||||
"grep-matcher",
|
||||
"log",
|
||||
"memchr",
|
||||
"memmap2 0.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "grid"
|
||||
version = "0.11.0"
|
||||
|
|
@ -2605,6 +2728,22 @@ dependencies = [
|
|||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ignore"
|
||||
version = "0.4.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "747ad1b4ae841a78e8aba0d63adbfbeaea26b517b63705d47856b73015d27060"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"globset",
|
||||
"log",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"same-file",
|
||||
"walkdir",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.23.14"
|
||||
|
|
@ -2890,7 +3029,6 @@ dependencies = [
|
|||
"iced_runtime",
|
||||
"iced_style",
|
||||
"iced_tiny_skia",
|
||||
"iced_wgpu",
|
||||
"iced_widget",
|
||||
"iced_winit",
|
||||
"lazy_static",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ license = "GPL-3.0-only"
|
|||
|
||||
[dependencies]
|
||||
env_logger = "0.10.0"
|
||||
grep = "0.3.1"
|
||||
ignore = "0.4.21"
|
||||
lazy_static = "1.4.0"
|
||||
log = "0.4.20"
|
||||
notify = "6.1.1"
|
||||
|
|
@ -30,7 +32,7 @@ features = ["syntect", "vi"]
|
|||
[dependencies.libcosmic]
|
||||
git = "https://github.com/pop-os/libcosmic"
|
||||
default-features = false
|
||||
features = ["tokio", "winit", "wgpu"]
|
||||
features = ["tokio", "winit"]
|
||||
#path = "../libcosmic"
|
||||
|
||||
#TODO: clean up and send changes upstream
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ character-count = Characters
|
|||
character-count-no-spaces = Characters (without spaces)
|
||||
line-count = Lines
|
||||
|
||||
## Project search
|
||||
project-search = Project search
|
||||
|
||||
## Settings
|
||||
settings = Settings
|
||||
|
||||
|
|
@ -57,9 +60,10 @@ redo = Redo
|
|||
cut = Cut
|
||||
copy = Copy
|
||||
paste = Paste
|
||||
select-all = Select All
|
||||
select-all = Select all
|
||||
find = Find
|
||||
replace = Replace
|
||||
find-in-project = Find in project...
|
||||
spell-check = Spell check...
|
||||
|
||||
## View
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic::{
|
||||
cosmic_config::{self, cosmic_config_derive::CosmicConfigEntry, CosmicConfigEntry},
|
||||
iced::keyboard::{KeyCode, Modifiers},
|
||||
|
|
@ -26,6 +28,7 @@ pub enum Action {
|
|||
Redo,
|
||||
Save,
|
||||
SelectAll,
|
||||
ToggleProjectSearch,
|
||||
ToggleSettingsPage,
|
||||
ToggleWordWrap,
|
||||
Undo,
|
||||
|
|
@ -47,6 +50,7 @@ impl Action {
|
|||
Self::Redo => Message::Redo,
|
||||
Self::Save => Message::Save,
|
||||
Self::SelectAll => Message::SelectAll,
|
||||
Self::ToggleProjectSearch => Message::ToggleContextPage(ContextPage::ProjectSearch),
|
||||
Self::ToggleSettingsPage => Message::ToggleContextPage(ContextPage::Settings),
|
||||
Self::ToggleWordWrap => Message::ToggleWordWrap,
|
||||
Self::Undo => Message::Undo,
|
||||
|
|
@ -114,6 +118,7 @@ impl KeyBind {
|
|||
bind!([Ctrl, Shift], Z, Redo);
|
||||
bind!([Ctrl], S, Save);
|
||||
bind!([Ctrl], A, SelectAll);
|
||||
bind!([Ctrl, Shift], F, ToggleProjectSearch);
|
||||
bind!([Ctrl], Comma, ToggleSettingsPage);
|
||||
bind!([Alt], Z, ToggleWordWrap);
|
||||
bind!([Ctrl], Z, Undo);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic::widget::icon;
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use i18n_embed::{
|
||||
fluent::{fluent_language_loader, FluentLanguageLoader},
|
||||
DefaultLocalizer, LanguageLoader, Localizer,
|
||||
|
|
|
|||
196
src/main.rs
196
src/main.rs
|
|
@ -4,6 +4,7 @@ use cosmic::{
|
|||
app::{message, Command, Core, Settings},
|
||||
cosmic_config::{self, CosmicConfigEntry},
|
||||
cosmic_theme, executor,
|
||||
font::Font,
|
||||
iced::{
|
||||
clipboard, event,
|
||||
futures::{self, SinkExt},
|
||||
|
|
@ -42,6 +43,9 @@ mod menu;
|
|||
use self::project::ProjectNode;
|
||||
mod project;
|
||||
|
||||
use self::search::ProjectSearchResult;
|
||||
mod search;
|
||||
|
||||
use self::tab::Tab;
|
||||
mod tab;
|
||||
|
||||
|
|
@ -164,8 +168,12 @@ pub enum Message {
|
|||
OpenFile(PathBuf),
|
||||
OpenProjectDialog,
|
||||
OpenProject(PathBuf),
|
||||
OpenSearchResult(usize, usize),
|
||||
Paste,
|
||||
PasteValue(String),
|
||||
ProjectSearchResult(ProjectSearchResult),
|
||||
ProjectSearchSubmit,
|
||||
ProjectSearchValue(String),
|
||||
Quit,
|
||||
Redo,
|
||||
Save,
|
||||
|
|
@ -177,6 +185,7 @@ pub enum Message {
|
|||
TabClose(segmented_button::Entity),
|
||||
TabContextAction(segmented_button::Entity, Action),
|
||||
TabContextMenu(segmented_button::Entity, Option<Point>),
|
||||
TabSetCursor(segmented_button::Entity, Cursor),
|
||||
TabWidth(u16),
|
||||
Todo,
|
||||
ToggleAutoIndent,
|
||||
|
|
@ -189,6 +198,8 @@ pub enum Message {
|
|||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum ContextPage {
|
||||
DocumentStatistics,
|
||||
//TODO: Move search to pop-up
|
||||
ProjectSearch,
|
||||
Settings,
|
||||
}
|
||||
|
||||
|
|
@ -196,6 +207,7 @@ impl ContextPage {
|
|||
fn title(&self) -> String {
|
||||
match self {
|
||||
Self::DocumentStatistics => fl!("document-statistics"),
|
||||
Self::ProjectSearch => fl!("project-search"),
|
||||
Self::Settings => fl!("settings"),
|
||||
}
|
||||
}
|
||||
|
|
@ -213,6 +225,8 @@ pub struct App {
|
|||
font_sizes: Vec<u16>,
|
||||
theme_names: Vec<String>,
|
||||
context_page: ContextPage,
|
||||
project_search_value: String,
|
||||
project_search_result: Option<ProjectSearchResult>,
|
||||
watcher_opt: Option<notify::RecommendedWatcher>,
|
||||
}
|
||||
|
||||
|
|
@ -316,14 +330,14 @@ impl App {
|
|||
self.open_folder(&path, position + 1, 1);
|
||||
}
|
||||
|
||||
pub fn open_tab(&mut self, path_opt: Option<PathBuf>) {
|
||||
pub fn open_tab(&mut self, path_opt: Option<PathBuf>) -> Option<segmented_button::Entity> {
|
||||
let tab = match path_opt {
|
||||
Some(path) => {
|
||||
let canonical = match fs::canonicalize(&path) {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
log::error!("failed to canonicalize {:?}: {}", path, err);
|
||||
return;
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -342,7 +356,7 @@ impl App {
|
|||
}
|
||||
if let Some(entity) = activate_opt {
|
||||
self.tab_model.activate(entity);
|
||||
return;
|
||||
return Some(entity);
|
||||
}
|
||||
|
||||
let mut tab = Tab::new(&self.config);
|
||||
|
|
@ -353,13 +367,16 @@ impl App {
|
|||
None => Tab::new(&self.config),
|
||||
};
|
||||
|
||||
self.tab_model
|
||||
.insert()
|
||||
.text(tab.title())
|
||||
.icon(tab.icon(16))
|
||||
.data::<Tab>(tab)
|
||||
.closable()
|
||||
.activate();
|
||||
Some(
|
||||
self.tab_model
|
||||
.insert()
|
||||
.text(tab.title())
|
||||
.icon(tab.icon(16))
|
||||
.data::<Tab>(tab)
|
||||
.closable()
|
||||
.activate()
|
||||
.id(),
|
||||
)
|
||||
}
|
||||
|
||||
fn update_config(&mut self) -> Command<Message> {
|
||||
|
|
@ -531,6 +548,8 @@ impl Application for App {
|
|||
font_sizes,
|
||||
theme_names,
|
||||
context_page: ContextPage::Settings,
|
||||
project_search_value: String::new(),
|
||||
project_search_result: None,
|
||||
watcher_opt: None,
|
||||
};
|
||||
|
||||
|
|
@ -844,6 +863,43 @@ impl Application for App {
|
|||
Message::OpenProject(path) => {
|
||||
self.open_project(path);
|
||||
}
|
||||
Message::OpenSearchResult(file_i, line_i) => {
|
||||
let path_cursor_opt = match &self.project_search_result {
|
||||
Some(project_search_result) => match project_search_result.files.get(file_i) {
|
||||
Some(file_search_result) => match file_search_result.lines.get(line_i) {
|
||||
Some(line_search_result) => Some((
|
||||
file_search_result.path.to_path_buf(),
|
||||
Cursor::new(
|
||||
line_search_result.number.saturating_sub(1),
|
||||
line_search_result.first.start(),
|
||||
),
|
||||
)),
|
||||
None => {
|
||||
log::warn!("failed to find search result {}, {}", file_i, line_i);
|
||||
None
|
||||
}
|
||||
},
|
||||
None => {
|
||||
log::warn!("failed to find search result {}", file_i);
|
||||
None
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
|
||||
if let Some((path, cursor)) = path_cursor_opt {
|
||||
if let Some(entity) = self.open_tab(Some(path)) {
|
||||
return Command::batch([
|
||||
//TODO: why must this be done in a command?
|
||||
Command::perform(
|
||||
async move { message::app(Message::TabSetCursor(entity, cursor)) },
|
||||
|x| x,
|
||||
),
|
||||
self.update_tab(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::Paste => {
|
||||
return clipboard::read(|value_opt| match value_opt {
|
||||
Some(value) => message::app(Message::PasteValue(value)),
|
||||
|
|
@ -857,6 +913,51 @@ impl Application for App {
|
|||
}
|
||||
None => {}
|
||||
},
|
||||
Message::ProjectSearchResult(project_search_result) => {
|
||||
self.project_search_result = Some(project_search_result);
|
||||
}
|
||||
Message::ProjectSearchSubmit => {
|
||||
//TODO: cache projects outside of nav model?
|
||||
let mut project_paths = Vec::new();
|
||||
for id in self.nav_model.iter() {
|
||||
match self.nav_model.data(id) {
|
||||
Some(ProjectNode::Folder { path, root, .. }) => {
|
||||
if *root {
|
||||
project_paths.push(path.clone())
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let project_search_value = self.project_search_value.clone();
|
||||
let mut project_search_result = ProjectSearchResult {
|
||||
value: project_search_value.clone(),
|
||||
in_progress: true,
|
||||
files: Vec::new(),
|
||||
};
|
||||
self.project_search_result = Some(project_search_result.clone());
|
||||
return Command::perform(
|
||||
async move {
|
||||
let task_res = tokio::task::spawn_blocking(move || {
|
||||
project_search_result.search_projects(project_paths);
|
||||
message::app(Message::ProjectSearchResult(project_search_result))
|
||||
})
|
||||
.await;
|
||||
match task_res {
|
||||
Ok(message) => message,
|
||||
Err(err) => {
|
||||
log::error!("failed to run search task: {}", err);
|
||||
message::none()
|
||||
}
|
||||
}
|
||||
},
|
||||
|x| x,
|
||||
);
|
||||
}
|
||||
Message::ProjectSearchValue(value) => {
|
||||
self.project_search_value = value;
|
||||
}
|
||||
Message::Quit => {
|
||||
//TODO: prompt for save?
|
||||
return window::close();
|
||||
|
|
@ -977,6 +1078,13 @@ impl Application for App {
|
|||
None => {}
|
||||
}
|
||||
}
|
||||
Message::TabSetCursor(entity, cursor) => match self.tab_model.data::<Tab>(entity) {
|
||||
Some(tab) => {
|
||||
let mut editor = tab.editor.lock().unwrap();
|
||||
editor.set_cursor(cursor);
|
||||
}
|
||||
None => {}
|
||||
},
|
||||
Message::TabWidth(tab_width) => {
|
||||
self.config.tab_width = tab_width;
|
||||
return self.save_config();
|
||||
|
|
@ -1066,6 +1174,72 @@ impl Application for App {
|
|||
.into()])
|
||||
.into()
|
||||
}
|
||||
ContextPage::ProjectSearch => {
|
||||
let search_input = widget::text_input::search_input(
|
||||
&fl!("project-search"),
|
||||
&self.project_search_value,
|
||||
);
|
||||
|
||||
let items = match &self.project_search_result {
|
||||
Some(project_search_result) => {
|
||||
let mut items =
|
||||
Vec::with_capacity(project_search_result.files.len().saturating_add(1));
|
||||
|
||||
if project_search_result.in_progress {
|
||||
items.push(search_input.into());
|
||||
} else {
|
||||
items.push(
|
||||
search_input
|
||||
.on_input(Message::ProjectSearchValue)
|
||||
.on_submit(Message::ProjectSearchSubmit)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
for (file_i, file_search_result) in
|
||||
project_search_result.files.iter().enumerate()
|
||||
{
|
||||
let mut column =
|
||||
widget::column::with_capacity(file_search_result.lines.len());
|
||||
for (line_i, line_search_result) in
|
||||
file_search_result.lines.iter().enumerate()
|
||||
{
|
||||
column = column.push(
|
||||
widget::button(
|
||||
widget::text(format!(
|
||||
"{}: {}",
|
||||
line_search_result.number, line_search_result.text
|
||||
))
|
||||
.font(Font::MONOSPACE),
|
||||
)
|
||||
.on_press(Message::OpenSearchResult(file_i, line_i))
|
||||
.width(Length::Fill)
|
||||
.style(theme::Button::AppletMenu),
|
||||
);
|
||||
}
|
||||
|
||||
items.push(
|
||||
widget::settings::view_section(format!(
|
||||
"{}",
|
||||
file_search_result.path.display(),
|
||||
))
|
||||
.add(column)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
items
|
||||
}
|
||||
None => {
|
||||
vec![search_input
|
||||
.on_input(Message::ProjectSearchValue)
|
||||
.on_submit(Message::ProjectSearchSubmit)
|
||||
.into()]
|
||||
}
|
||||
};
|
||||
|
||||
widget::settings::view_column(items).into()
|
||||
}
|
||||
ContextPage::Settings => {
|
||||
let app_theme_selected = match self.config.app_theme {
|
||||
AppTheme::Dark => 1,
|
||||
|
|
@ -1222,7 +1396,7 @@ impl Application for App {
|
|||
None => text_box.into(),
|
||||
};
|
||||
tab_column = tab_column.push(tab_element);
|
||||
tab_column = tab_column.push(text(status).font(cosmic::font::Font::MONOSPACE));
|
||||
tab_column = tab_column.push(text(status).font(Font::MONOSPACE));
|
||||
}
|
||||
None => {
|
||||
log::warn!("TODO: No tab open");
|
||||
|
|
|
|||
|
|
@ -195,6 +195,10 @@ pub fn menu_bar<'a>(config: &Config) -> Element<'a, Message> {
|
|||
MenuTree::new(horizontal_rule(1)),
|
||||
menu_key(fl!("find"), "Ctrl + F", Message::Todo),
|
||||
menu_key(fl!("replace"), "Ctrl + H", Message::Todo),
|
||||
menu_item(
|
||||
fl!("find-in-project"),
|
||||
Message::ToggleContextPage(ContextPage::ProjectSearch),
|
||||
),
|
||||
MenuTree::new(horizontal_rule(1)),
|
||||
menu_item(fl!("spell-check"), Message::Todo),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use cosmic::widget::icon;
|
||||
use std::{collections::HashMap, path::Path, sync::Mutex};
|
||||
|
||||
|
|
|
|||
121
src/search.rs
Normal file
121
src/search.rs
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use grep::matcher::{Match, Matcher};
|
||||
use grep::regex::RegexMatcher;
|
||||
use grep::searcher::{sinks::UTF8, Searcher};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct LineSearchResult {
|
||||
pub number: usize,
|
||||
pub text: String,
|
||||
pub first: Match,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct FileSearchResult {
|
||||
pub path: PathBuf,
|
||||
pub lines: Vec<LineSearchResult>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ProjectSearchResult {
|
||||
//TODO: should this be included?
|
||||
pub value: String,
|
||||
pub in_progress: bool,
|
||||
pub files: Vec<FileSearchResult>,
|
||||
}
|
||||
|
||||
impl ProjectSearchResult {
|
||||
pub fn search_projects(&mut self, project_paths: Vec<PathBuf>) {
|
||||
//TODO: support literal search
|
||||
//TODO: use ignore::WalkParallel?
|
||||
match RegexMatcher::new(&self.value) {
|
||||
Ok(matcher) => {
|
||||
let mut searcher = Searcher::new();
|
||||
let mut walk_builder_opt: Option<ignore::WalkBuilder> = None;
|
||||
for project_path in project_paths.iter() {
|
||||
walk_builder_opt = match walk_builder_opt.take() {
|
||||
Some(mut walk_builder) => {
|
||||
walk_builder.add(project_path);
|
||||
Some(walk_builder)
|
||||
}
|
||||
None => Some(ignore::WalkBuilder::new(project_path)),
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(walk_builder) = walk_builder_opt {
|
||||
for entry_res in walk_builder.build() {
|
||||
let entry = match entry_res {
|
||||
Ok(ok) => ok,
|
||||
Err(err) => {
|
||||
log::error!("failed to walk projects {:?}: {}", project_paths, err);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
match entry.file_type() {
|
||||
Some(file_type) => {
|
||||
if file_type.is_dir() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
let entry_path = entry.path();
|
||||
|
||||
let mut lines = Vec::new();
|
||||
match searcher.search_path(
|
||||
&matcher,
|
||||
&entry_path,
|
||||
UTF8(|number_u64, text| {
|
||||
match usize::try_from(number_u64) {
|
||||
Ok(number) => match matcher.find(text.as_bytes()) {
|
||||
Ok(Some(first)) => {
|
||||
lines.push(LineSearchResult {
|
||||
number,
|
||||
text: text.to_string(),
|
||||
first,
|
||||
});
|
||||
},
|
||||
Ok(None) => {
|
||||
log::error!("first match in file {:?} line {} not found", entry_path, number);
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("failed to find first match in file {:?} line {}: {}", entry_path, number, err);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
log::error!("failed to convert file {:?} line {} to usize: {}", entry_path, number_u64, err);
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}),
|
||||
) {
|
||||
Ok(()) => {
|
||||
if !lines.is_empty() {
|
||||
self.files.push(FileSearchResult {
|
||||
path: entry_path.to_path_buf(),
|
||||
lines,
|
||||
});
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("failed to search file {:?}: {}", entry_path, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
"failed to create regex matcher with value {:?}: {}",
|
||||
self.value,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
self.in_progress = false;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue