commit
08ee8b9443
24 changed files with 1857 additions and 978 deletions
17
Cargo.lock
generated
17
Cargo.lock
generated
|
@ -209,12 +209,6 @@ version = "0.8.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
|
||||
|
||||
[[package]]
|
||||
name = "cov-mark"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ffa3d3e0138386cd4361f63537765cac7ee40698028844635a54495a92f67f3"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.3.2"
|
||||
|
@ -1337,6 +1331,7 @@ dependencies = [
|
|||
"unicode-general-category",
|
||||
"unicode-segmentation",
|
||||
"unicode-width",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1467,6 +1462,7 @@ dependencies = [
|
|||
"smallvec",
|
||||
"tempfile",
|
||||
"termini",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"toml",
|
||||
|
@ -1784,9 +1780,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "nucleo"
|
||||
version = "0.2.1"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae5331f4bcce475cf28cb29c95366c3091af4b0aa7703f1a6bc858f29718fdf3"
|
||||
checksum = "5262af4c94921c2646c5ac6ff7900c2af9cbb08dc26a797e18130a7019c039d4"
|
||||
dependencies = [
|
||||
"nucleo-matcher",
|
||||
"parking_lot",
|
||||
|
@ -1795,11 +1791,10 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "nucleo-matcher"
|
||||
version = "0.2.0"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b702b402fe286162d1f00b552a046ce74365d2ac473a2607ff36ba650f9bd57"
|
||||
checksum = "bf33f538733d1a5a3494b836ba913207f14d9d4a1d3cd67030c5061bdd2cac85"
|
||||
dependencies = [
|
||||
"cov-mark",
|
||||
"memchr",
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
|
|
@ -38,7 +38,7 @@ package.helix-term.opt-level = 2
|
|||
|
||||
[workspace.dependencies]
|
||||
tree-sitter = { version = "0.22" }
|
||||
nucleo = "0.2.0"
|
||||
nucleo = "0.5.0"
|
||||
slotmap = "1.0.7"
|
||||
thiserror = "1.0"
|
||||
|
||||
|
|
|
@ -297,6 +297,8 @@ These scopes are used for theming the editor interface:
|
|||
| `ui.bufferline.background` | Style for bufferline background |
|
||||
| `ui.popup` | Documentation popups (e.g. Space + k) |
|
||||
| `ui.popup.info` | Prompt for multiple key options |
|
||||
| `ui.picker.header` | Column names in pickers with multiple columns |
|
||||
| `ui.picker.header.active` | The column name in pickers with multiple columns where the cursor is entering into. |
|
||||
| `ui.window` | Borderlines separating splits |
|
||||
| `ui.help` | Description box for commands |
|
||||
| `ui.text` | Default text style, command prompts, popup text, etc. |
|
||||
|
|
|
@ -34,6 +34,7 @@ bitflags = "2.6"
|
|||
ahash = "0.8.11"
|
||||
hashbrown = { version = "0.14.5", features = ["raw"] }
|
||||
dunce = "1.0"
|
||||
url = "2.5.0"
|
||||
|
||||
log = "0.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::ops::DerefMut;
|
||||
|
||||
use nucleo::pattern::{Atom, AtomKind, CaseMatching};
|
||||
use nucleo::pattern::{Atom, AtomKind, CaseMatching, Normalization};
|
||||
use nucleo::Config;
|
||||
use parking_lot::Mutex;
|
||||
|
||||
|
@ -38,6 +38,12 @@ pub fn fuzzy_match<T: AsRef<str>>(
|
|||
if path {
|
||||
matcher.config.set_match_paths();
|
||||
}
|
||||
let pattern = Atom::new(pattern, CaseMatching::Smart, AtomKind::Fuzzy, false);
|
||||
let pattern = Atom::new(
|
||||
pattern,
|
||||
CaseMatching::Smart,
|
||||
Normalization::Smart,
|
||||
AtomKind::Fuzzy,
|
||||
false,
|
||||
);
|
||||
pattern.match_list(items, &mut matcher)
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ pub mod test;
|
|||
pub mod text_annotations;
|
||||
pub mod textobject;
|
||||
mod transaction;
|
||||
pub mod uri;
|
||||
pub mod wrap;
|
||||
|
||||
pub mod unicode {
|
||||
|
@ -66,3 +67,5 @@ pub use diagnostic::Diagnostic;
|
|||
|
||||
pub use line_ending::{LineEnding, NATIVE_LINE_ENDING};
|
||||
pub use transaction::{Assoc, Change, ChangeSet, Deletion, Operation, Transaction};
|
||||
|
||||
pub use uri::Uri;
|
||||
|
|
122
helix-core/src/uri.rs
Normal file
122
helix-core/src/uri.rs
Normal file
|
@ -0,0 +1,122 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
/// A generic pointer to a file location.
|
||||
///
|
||||
/// Currently this type only supports paths to local files.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[non_exhaustive]
|
||||
pub enum Uri {
|
||||
File(PathBuf),
|
||||
}
|
||||
|
||||
impl Uri {
|
||||
// This clippy allow mirrors url::Url::from_file_path
|
||||
#[allow(clippy::result_unit_err)]
|
||||
pub fn to_url(&self) -> Result<url::Url, ()> {
|
||||
match self {
|
||||
Uri::File(path) => url::Url::from_file_path(path),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_path(&self) -> Option<&Path> {
|
||||
match self {
|
||||
Self::File(path) => Some(path),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_path_buf(self) -> Option<PathBuf> {
|
||||
match self {
|
||||
Self::File(path) => Some(path),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PathBuf> for Uri {
|
||||
fn from(path: PathBuf) -> Self {
|
||||
Self::File(path)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Uri> for PathBuf {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(uri: Uri) -> Result<Self, Self::Error> {
|
||||
match uri {
|
||||
Uri::File(path) => Ok(path),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UrlConversionError {
|
||||
source: url::Url,
|
||||
kind: UrlConversionErrorKind,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum UrlConversionErrorKind {
|
||||
UnsupportedScheme,
|
||||
UnableToConvert,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for UrlConversionError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.kind {
|
||||
UrlConversionErrorKind::UnsupportedScheme => {
|
||||
write!(f, "unsupported scheme in URL: {}", self.source.scheme())
|
||||
}
|
||||
UrlConversionErrorKind::UnableToConvert => {
|
||||
write!(f, "unable to convert URL to file path: {}", self.source)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for UrlConversionError {}
|
||||
|
||||
fn convert_url_to_uri(url: &url::Url) -> Result<Uri, UrlConversionErrorKind> {
|
||||
if url.scheme() == "file" {
|
||||
url.to_file_path()
|
||||
.map(|path| Uri::File(helix_stdx::path::normalize(path)))
|
||||
.map_err(|_| UrlConversionErrorKind::UnableToConvert)
|
||||
} else {
|
||||
Err(UrlConversionErrorKind::UnsupportedScheme)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<url::Url> for Uri {
|
||||
type Error = UrlConversionError;
|
||||
|
||||
fn try_from(url: url::Url) -> Result<Self, Self::Error> {
|
||||
convert_url_to_uri(&url).map_err(|kind| Self::Error { source: url, kind })
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&url::Url> for Uri {
|
||||
type Error = UrlConversionError;
|
||||
|
||||
fn try_from(url: &url::Url) -> Result<Self, Self::Error> {
|
||||
convert_url_to_uri(url).map_err(|kind| Self::Error {
|
||||
source: url.clone(),
|
||||
kind,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use url::Url;
|
||||
|
||||
#[test]
|
||||
fn unknown_scheme() {
|
||||
let url = Url::parse("csharp:/metadata/foo/bar/Baz.cs").unwrap();
|
||||
assert!(matches!(
|
||||
Uri::try_from(url),
|
||||
Err(UrlConversionError {
|
||||
kind: UrlConversionErrorKind::UnsupportedScheme,
|
||||
..
|
||||
})
|
||||
));
|
||||
}
|
||||
}
|
|
@ -34,7 +34,9 @@
|
|||
use anyhow::Result;
|
||||
pub use cancel::{cancelable_future, cancelation, CancelRx, CancelTx};
|
||||
pub use debounce::{send_blocking, AsyncHook};
|
||||
pub use redraw::{lock_frame, redraw_requested, request_redraw, start_frame, RenderLockGuard};
|
||||
pub use redraw::{
|
||||
lock_frame, redraw_requested, request_redraw, start_frame, RenderLockGuard, RequestRedrawOnDrop,
|
||||
};
|
||||
pub use registry::Event;
|
||||
|
||||
mod cancel;
|
||||
|
|
|
@ -51,3 +51,12 @@ pub fn start_frame() {
|
|||
pub fn lock_frame() -> RenderLockGuard {
|
||||
RENDER_LOCK.read()
|
||||
}
|
||||
|
||||
/// A zero sized type that requests a redraw via [request_redraw] when the type [Drop]s.
|
||||
pub struct RequestRedrawOnDrop;
|
||||
|
||||
impl Drop for RequestRedrawOnDrop {
|
||||
fn drop(&mut self) {
|
||||
request_redraw();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -56,6 +56,7 @@ ignore = "0.4"
|
|||
pulldown-cmark = { version = "0.11", default-features = false }
|
||||
# file type detection
|
||||
content_inspector = "0.2.4"
|
||||
thiserror = "1.0"
|
||||
|
||||
# opening URLs
|
||||
open = "5.2.0"
|
||||
|
|
|
@ -735,10 +735,10 @@ impl Application {
|
|||
}
|
||||
}
|
||||
Notification::PublishDiagnostics(mut params) => {
|
||||
let path = match params.uri.to_file_path() {
|
||||
Ok(path) => helix_stdx::path::normalize(path),
|
||||
Err(_) => {
|
||||
log::error!("Unsupported file URI: {}", params.uri);
|
||||
let uri = match helix_core::Uri::try_from(params.uri) {
|
||||
Ok(uri) => uri,
|
||||
Err(err) => {
|
||||
log::error!("{err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
@ -749,11 +749,11 @@ impl Application {
|
|||
}
|
||||
// have to inline the function because of borrow checking...
|
||||
let doc = self.editor.documents.values_mut()
|
||||
.find(|doc| doc.path().map(|p| p == &path).unwrap_or(false))
|
||||
.find(|doc| doc.uri().is_some_and(|u| u == uri))
|
||||
.filter(|doc| {
|
||||
if let Some(version) = params.version {
|
||||
if version != doc.version() {
|
||||
log::info!("Version ({version}) is out of date for {path:?} (expected ({}), dropping PublishDiagnostic notification", doc.version());
|
||||
log::info!("Version ({version}) is out of date for {uri:?} (expected ({}), dropping PublishDiagnostic notification", doc.version());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -765,7 +765,7 @@ impl Application {
|
|||
let lang_conf = doc.language.clone();
|
||||
|
||||
if let Some(lang_conf) = &lang_conf {
|
||||
if let Some(old_diagnostics) = self.editor.diagnostics.get(&path) {
|
||||
if let Some(old_diagnostics) = self.editor.diagnostics.get(&uri) {
|
||||
if !lang_conf.persistent_diagnostic_sources.is_empty() {
|
||||
// Sort diagnostics first by severity and then by line numbers.
|
||||
// Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order
|
||||
|
@ -798,7 +798,7 @@ impl Application {
|
|||
// Insert the original lsp::Diagnostics here because we may have no open document
|
||||
// for diagnosic message and so we can't calculate the exact position.
|
||||
// When using them later in the diagnostics picker, we calculate them on-demand.
|
||||
let diagnostics = match self.editor.diagnostics.entry(path) {
|
||||
let diagnostics = match self.editor.diagnostics.entry(uri) {
|
||||
Entry::Occupied(o) => {
|
||||
let current_diagnostics = o.into_mut();
|
||||
// there may entries of other language servers, which is why we can't overwrite the whole entry
|
||||
|
@ -1132,20 +1132,22 @@ impl Application {
|
|||
..
|
||||
} = params;
|
||||
|
||||
let path = match uri.to_file_path() {
|
||||
Ok(path) => path,
|
||||
let uri = match helix_core::Uri::try_from(uri) {
|
||||
Ok(uri) => uri,
|
||||
Err(err) => {
|
||||
log::error!("unsupported file URI: {}: {:?}", uri, err);
|
||||
log::error!("{err}");
|
||||
return lsp::ShowDocumentResult { success: false };
|
||||
}
|
||||
};
|
||||
// If `Uri` gets another variant other than `Path` this may not be valid.
|
||||
let path = uri.as_path().expect("URIs are valid paths");
|
||||
|
||||
let action = match take_focus {
|
||||
Some(true) => helix_view::editor::Action::Replace,
|
||||
_ => helix_view::editor::Action::VerticalSplit,
|
||||
};
|
||||
|
||||
let doc_id = match self.editor.open(&path, action) {
|
||||
let doc_id = match self.editor.open(path, action) {
|
||||
Ok(id) => id,
|
||||
Err(err) => {
|
||||
log::error!("failed to open path: {:?}: {:?}", uri, err);
|
||||
|
|
|
@ -3,6 +3,7 @@ pub(crate) mod lsp;
|
|||
pub(crate) mod typed;
|
||||
|
||||
pub use dap::*;
|
||||
use futures_util::FutureExt;
|
||||
use helix_event::status;
|
||||
use helix_stdx::{
|
||||
path::expand_tilde,
|
||||
|
@ -10,10 +11,7 @@ use helix_stdx::{
|
|||
};
|
||||
use helix_vcs::{FileChange, Hunk};
|
||||
pub use lsp::*;
|
||||
use tui::{
|
||||
text::Span,
|
||||
widgets::{Cell, Row},
|
||||
};
|
||||
use tui::text::Span;
|
||||
pub use typed::*;
|
||||
|
||||
use helix_core::{
|
||||
|
@ -61,8 +59,7 @@ use crate::{
|
|||
compositor::{self, Component, Compositor},
|
||||
filter_picker_entry,
|
||||
job::Callback,
|
||||
keymap::ReverseKeymap,
|
||||
ui::{self, menu::Item, overlay::overlaid, Picker, Popup, Prompt, PromptEvent},
|
||||
ui::{self, overlay::overlaid, Picker, PickerColumn, Popup, Prompt, PromptEvent},
|
||||
};
|
||||
|
||||
use crate::job::{self, Jobs};
|
||||
|
@ -2257,216 +2254,193 @@ fn global_search(cx: &mut Context) {
|
|||
}
|
||||
}
|
||||
|
||||
impl ui::menu::Item for FileResult {
|
||||
type Data = Option<PathBuf>;
|
||||
|
||||
fn format(&self, current_path: &Self::Data) -> Row {
|
||||
let relative_path = helix_stdx::path::get_relative_path(&self.path)
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
if current_path
|
||||
.as_ref()
|
||||
.map(|p| p == &self.path)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
format!("{} (*)", relative_path).into()
|
||||
} else {
|
||||
relative_path.into()
|
||||
}
|
||||
}
|
||||
struct GlobalSearchConfig {
|
||||
smart_case: bool,
|
||||
file_picker_config: helix_view::editor::FilePickerConfig,
|
||||
}
|
||||
|
||||
let config = cx.editor.config();
|
||||
let smart_case = config.search.smart_case;
|
||||
let file_picker_config = config.file_picker.clone();
|
||||
let config = GlobalSearchConfig {
|
||||
smart_case: config.search.smart_case,
|
||||
file_picker_config: config.file_picker.clone(),
|
||||
};
|
||||
|
||||
let reg = cx.register.unwrap_or('/');
|
||||
let completions = search_completions(cx, Some(reg));
|
||||
ui::raw_regex_prompt(
|
||||
cx,
|
||||
"global-search:".into(),
|
||||
Some(reg),
|
||||
move |_editor: &Editor, input: &str| {
|
||||
completions
|
||||
.iter()
|
||||
.filter(|comp| comp.starts_with(input))
|
||||
.map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
|
||||
.collect()
|
||||
},
|
||||
move |cx, _, input, event| {
|
||||
if event != PromptEvent::Validate {
|
||||
return;
|
||||
let columns = [
|
||||
PickerColumn::new("path", |item: &FileResult, _| {
|
||||
let path = helix_stdx::path::get_relative_path(&item.path);
|
||||
format!("{}:{}", path.to_string_lossy(), item.line_num + 1).into()
|
||||
}),
|
||||
PickerColumn::hidden("contents"),
|
||||
];
|
||||
|
||||
let get_files = |query: &str,
|
||||
editor: &mut Editor,
|
||||
config: std::sync::Arc<GlobalSearchConfig>,
|
||||
injector: &ui::picker::Injector<_, _>| {
|
||||
if query.is_empty() {
|
||||
return async { Ok(()) }.boxed();
|
||||
}
|
||||
|
||||
let search_root = helix_stdx::env::current_working_dir();
|
||||
if !search_root.exists() {
|
||||
return async { Err(anyhow::anyhow!("Current working directory does not exist")) }
|
||||
.boxed();
|
||||
}
|
||||
|
||||
let documents: Vec<_> = editor
|
||||
.documents()
|
||||
.map(|doc| (doc.path().cloned(), doc.text().to_owned()))
|
||||
.collect();
|
||||
|
||||
let matcher = match RegexMatcherBuilder::new()
|
||||
.case_smart(config.smart_case)
|
||||
.build(query)
|
||||
{
|
||||
Ok(matcher) => {
|
||||
// Clear any "Failed to compile regex" errors out of the statusline.
|
||||
editor.clear_status();
|
||||
matcher
|
||||
}
|
||||
cx.editor.registers.last_search_register = reg;
|
||||
Err(err) => {
|
||||
log::info!("Failed to compile search pattern in global search: {}", err);
|
||||
return async { Err(anyhow::anyhow!("Failed to compile regex")) }.boxed();
|
||||
}
|
||||
};
|
||||
|
||||
let current_path = doc_mut!(cx.editor).path().cloned();
|
||||
let documents: Vec<_> = cx
|
||||
.editor
|
||||
.documents()
|
||||
.map(|doc| (doc.path().cloned(), doc.text().to_owned()))
|
||||
.collect();
|
||||
let dedup_symlinks = config.file_picker_config.deduplicate_links;
|
||||
let absolute_root = search_root
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| search_root.clone());
|
||||
|
||||
if let Ok(matcher) = RegexMatcherBuilder::new()
|
||||
.case_smart(smart_case)
|
||||
.build(input)
|
||||
{
|
||||
let search_root = helix_stdx::env::current_working_dir();
|
||||
if !search_root.exists() {
|
||||
cx.editor
|
||||
.set_error("Current working directory does not exist");
|
||||
return;
|
||||
}
|
||||
let injector = injector.clone();
|
||||
async move {
|
||||
let searcher = SearcherBuilder::new()
|
||||
.binary_detection(BinaryDetection::quit(b'\x00'))
|
||||
.build();
|
||||
WalkBuilder::new(search_root)
|
||||
.hidden(config.file_picker_config.hidden)
|
||||
.parents(config.file_picker_config.parents)
|
||||
.ignore(config.file_picker_config.ignore)
|
||||
.follow_links(config.file_picker_config.follow_symlinks)
|
||||
.git_ignore(config.file_picker_config.git_ignore)
|
||||
.git_global(config.file_picker_config.git_global)
|
||||
.git_exclude(config.file_picker_config.git_exclude)
|
||||
.max_depth(config.file_picker_config.max_depth)
|
||||
.filter_entry(move |entry| {
|
||||
filter_picker_entry(entry, &absolute_root, dedup_symlinks)
|
||||
})
|
||||
.add_custom_ignore_filename(helix_loader::config_dir().join("ignore"))
|
||||
.add_custom_ignore_filename(".helix/ignore")
|
||||
.build_parallel()
|
||||
.run(|| {
|
||||
let mut searcher = searcher.clone();
|
||||
let matcher = matcher.clone();
|
||||
let injector = injector.clone();
|
||||
let documents = &documents;
|
||||
Box::new(move |entry: Result<DirEntry, ignore::Error>| -> WalkState {
|
||||
let entry = match entry {
|
||||
Ok(entry) => entry,
|
||||
Err(_) => return WalkState::Continue,
|
||||
};
|
||||
|
||||
let (picker, injector) = Picker::stream(current_path);
|
||||
match entry.file_type() {
|
||||
Some(entry) if entry.is_file() => {}
|
||||
// skip everything else
|
||||
_ => return WalkState::Continue,
|
||||
};
|
||||
|
||||
let dedup_symlinks = file_picker_config.deduplicate_links;
|
||||
let absolute_root = search_root
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| search_root.clone());
|
||||
let injector_ = injector.clone();
|
||||
let mut stop = false;
|
||||
let sink = sinks::UTF8(|line_num, _line_content| {
|
||||
stop = injector
|
||||
.push(FileResult::new(entry.path(), line_num as usize - 1))
|
||||
.is_err();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let searcher = SearcherBuilder::new()
|
||||
.binary_detection(BinaryDetection::quit(b'\x00'))
|
||||
.build();
|
||||
|
||||
let mut walk_builder = WalkBuilder::new(search_root);
|
||||
|
||||
walk_builder
|
||||
.hidden(file_picker_config.hidden)
|
||||
.parents(file_picker_config.parents)
|
||||
.ignore(file_picker_config.ignore)
|
||||
.follow_links(file_picker_config.follow_symlinks)
|
||||
.git_ignore(file_picker_config.git_ignore)
|
||||
.git_global(file_picker_config.git_global)
|
||||
.git_exclude(file_picker_config.git_exclude)
|
||||
.max_depth(file_picker_config.max_depth)
|
||||
.filter_entry(move |entry| {
|
||||
filter_picker_entry(entry, &absolute_root, dedup_symlinks)
|
||||
Ok(!stop)
|
||||
});
|
||||
let doc = documents.iter().find(|&(doc_path, _)| {
|
||||
doc_path
|
||||
.as_ref()
|
||||
.map_or(false, |doc_path| doc_path == entry.path())
|
||||
});
|
||||
|
||||
walk_builder
|
||||
.add_custom_ignore_filename(helix_loader::config_dir().join("ignore"));
|
||||
walk_builder.add_custom_ignore_filename(".helix/ignore");
|
||||
|
||||
walk_builder.build_parallel().run(|| {
|
||||
let mut searcher = searcher.clone();
|
||||
let matcher = matcher.clone();
|
||||
let injector = injector_.clone();
|
||||
let documents = &documents;
|
||||
Box::new(move |entry: Result<DirEntry, ignore::Error>| -> WalkState {
|
||||
let entry = match entry {
|
||||
Ok(entry) => entry,
|
||||
Err(_) => return WalkState::Continue,
|
||||
};
|
||||
|
||||
match entry.file_type() {
|
||||
Some(entry) if entry.is_file() => {}
|
||||
// skip everything else
|
||||
_ => return WalkState::Continue,
|
||||
};
|
||||
|
||||
let mut stop = false;
|
||||
let sink = sinks::UTF8(|line_num, _| {
|
||||
stop = injector
|
||||
.push(FileResult::new(entry.path(), line_num as usize - 1))
|
||||
.is_err();
|
||||
|
||||
Ok(!stop)
|
||||
});
|
||||
let doc = documents.iter().find(|&(doc_path, _)| {
|
||||
doc_path
|
||||
.as_ref()
|
||||
.map_or(false, |doc_path| doc_path == entry.path())
|
||||
});
|
||||
|
||||
let result = if let Some((_, doc)) = doc {
|
||||
// there is already a buffer for this file
|
||||
// search the buffer instead of the file because it's faster
|
||||
// and captures new edits without requiring a save
|
||||
if searcher.multi_line_with_matcher(&matcher) {
|
||||
// in this case a continous buffer is required
|
||||
// convert the rope to a string
|
||||
let text = doc.to_string();
|
||||
searcher.search_slice(&matcher, text.as_bytes(), sink)
|
||||
} else {
|
||||
searcher.search_reader(
|
||||
&matcher,
|
||||
RopeReader::new(doc.slice(..)),
|
||||
sink,
|
||||
)
|
||||
}
|
||||
let result = if let Some((_, doc)) = doc {
|
||||
// there is already a buffer for this file
|
||||
// search the buffer instead of the file because it's faster
|
||||
// and captures new edits without requiring a save
|
||||
if searcher.multi_line_with_matcher(&matcher) {
|
||||
// in this case a continous buffer is required
|
||||
// convert the rope to a string
|
||||
let text = doc.to_string();
|
||||
searcher.search_slice(&matcher, text.as_bytes(), sink)
|
||||
} else {
|
||||
searcher.search_path(&matcher, entry.path(), sink)
|
||||
};
|
||||
searcher.search_reader(
|
||||
&matcher,
|
||||
RopeReader::new(doc.slice(..)),
|
||||
sink,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
searcher.search_path(&matcher, entry.path(), sink)
|
||||
};
|
||||
|
||||
if let Err(err) = result {
|
||||
log::error!(
|
||||
"Global search error: {}, {}",
|
||||
entry.path().display(),
|
||||
err
|
||||
);
|
||||
}
|
||||
if stop {
|
||||
WalkState::Quit
|
||||
} else {
|
||||
WalkState::Continue
|
||||
}
|
||||
})
|
||||
});
|
||||
if let Err(err) = result {
|
||||
log::error!("Global search error: {}, {}", entry.path().display(), err);
|
||||
}
|
||||
if stop {
|
||||
WalkState::Quit
|
||||
} else {
|
||||
WalkState::Continue
|
||||
}
|
||||
})
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
.boxed()
|
||||
};
|
||||
|
||||
cx.jobs.callback(async move {
|
||||
let call = move |_: &mut Editor, compositor: &mut Compositor| {
|
||||
let picker = Picker::with_stream(
|
||||
picker,
|
||||
injector,
|
||||
move |cx, FileResult { path, line_num }, action| {
|
||||
let doc = match cx.editor.open(path, action) {
|
||||
Ok(id) => doc_mut!(cx.editor, &id),
|
||||
Err(e) => {
|
||||
cx.editor.set_error(format!(
|
||||
"Failed to open file '{}': {}",
|
||||
path.display(),
|
||||
e
|
||||
));
|
||||
return;
|
||||
}
|
||||
};
|
||||
let reg = cx.register.unwrap_or('/');
|
||||
cx.editor.registers.last_search_register = reg;
|
||||
|
||||
let line_num = *line_num;
|
||||
let view = view_mut!(cx.editor);
|
||||
let text = doc.text();
|
||||
if line_num >= text.len_lines() {
|
||||
cx.editor.set_error(
|
||||
let picker = Picker::new(
|
||||
columns,
|
||||
1, // contents
|
||||
[],
|
||||
config,
|
||||
move |cx, FileResult { path, line_num, .. }, action| {
|
||||
let doc = match cx.editor.open(path, action) {
|
||||
Ok(id) => doc_mut!(cx.editor, &id),
|
||||
Err(e) => {
|
||||
cx.editor
|
||||
.set_error(format!("Failed to open file '{}': {}", path.display(), e));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let line_num = *line_num;
|
||||
let view = view_mut!(cx.editor);
|
||||
let text = doc.text();
|
||||
if line_num >= text.len_lines() {
|
||||
cx.editor.set_error(
|
||||
"The line you jumped to does not exist anymore because the file has changed.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
let start = text.line_to_char(line_num);
|
||||
let end = text.line_to_char((line_num + 1).min(text.len_lines()));
|
||||
return;
|
||||
}
|
||||
let start = text.line_to_char(line_num);
|
||||
let end = text.line_to_char((line_num + 1).min(text.len_lines()));
|
||||
|
||||
doc.set_selection(view.id, Selection::single(start, end));
|
||||
if action.align_view(view, doc.id()) {
|
||||
align_view(doc, view, Align::Center);
|
||||
}
|
||||
},
|
||||
)
|
||||
.with_preview(
|
||||
|_editor, FileResult { path, line_num }| {
|
||||
Some((path.clone().into(), Some((*line_num, *line_num))))
|
||||
},
|
||||
);
|
||||
compositor.push(Box::new(overlaid(picker)))
|
||||
};
|
||||
Ok(Callback::EditorCompositor(Box::new(call)))
|
||||
})
|
||||
} else {
|
||||
// Otherwise do nothing
|
||||
// log::warn!("Global Search Invalid Pattern")
|
||||
doc.set_selection(view.id, Selection::single(start, end));
|
||||
if action.align_view(view, doc.id()) {
|
||||
align_view(doc, view, Align::Center);
|
||||
}
|
||||
},
|
||||
);
|
||||
)
|
||||
.with_preview(|_editor, FileResult { path, line_num, .. }| {
|
||||
Some((path.as_path().into(), Some((*line_num, *line_num))))
|
||||
})
|
||||
.with_history_register(Some(reg))
|
||||
.with_dynamic_query(get_files, Some(275));
|
||||
|
||||
cx.push_layer(Box::new(overlaid(picker)));
|
||||
}
|
||||
|
||||
enum Extend {
|
||||
|
@ -2894,31 +2868,6 @@ fn buffer_picker(cx: &mut Context) {
|
|||
focused_at: std::time::Instant,
|
||||
}
|
||||
|
||||
impl ui::menu::Item for BufferMeta {
|
||||
type Data = ();
|
||||
|
||||
fn format(&self, _data: &Self::Data) -> Row {
|
||||
let path = self
|
||||
.path
|
||||
.as_deref()
|
||||
.map(helix_stdx::path::get_relative_path);
|
||||
let path = match path.as_deref().and_then(Path::to_str) {
|
||||
Some(path) => path,
|
||||
None => SCRATCH_BUFFER_NAME,
|
||||
};
|
||||
|
||||
let mut flags = String::new();
|
||||
if self.is_modified {
|
||||
flags.push('+');
|
||||
}
|
||||
if self.is_current {
|
||||
flags.push('*');
|
||||
}
|
||||
|
||||
Row::new([self.id.to_string(), flags, path.to_string()])
|
||||
}
|
||||
}
|
||||
|
||||
let new_meta = |doc: &Document| BufferMeta {
|
||||
id: doc.id(),
|
||||
path: doc.path().cloned(),
|
||||
|
@ -2937,7 +2886,31 @@ fn buffer_picker(cx: &mut Context) {
|
|||
// mru
|
||||
items.sort_unstable_by_key(|item| std::cmp::Reverse(item.focused_at));
|
||||
|
||||
let picker = Picker::new(items, (), |cx, meta, action| {
|
||||
let columns = [
|
||||
PickerColumn::new("id", |meta: &BufferMeta, _| meta.id.to_string().into()),
|
||||
PickerColumn::new("flags", |meta: &BufferMeta, _| {
|
||||
let mut flags = String::new();
|
||||
if meta.is_modified {
|
||||
flags.push('+');
|
||||
}
|
||||
if meta.is_current {
|
||||
flags.push('*');
|
||||
}
|
||||
flags.into()
|
||||
}),
|
||||
PickerColumn::new("path", |meta: &BufferMeta, _| {
|
||||
let path = meta
|
||||
.path
|
||||
.as_deref()
|
||||
.map(helix_stdx::path::get_relative_path);
|
||||
path.as_deref()
|
||||
.and_then(Path::to_str)
|
||||
.unwrap_or(SCRATCH_BUFFER_NAME)
|
||||
.to_string()
|
||||
.into()
|
||||
}),
|
||||
];
|
||||
let picker = Picker::new(columns, 2, items, (), |cx, meta, action| {
|
||||
cx.editor.switch(meta.id, action);
|
||||
})
|
||||
.with_preview(|editor, meta| {
|
||||
|
@ -2961,33 +2934,6 @@ fn jumplist_picker(cx: &mut Context) {
|
|||
is_current: bool,
|
||||
}
|
||||
|
||||
impl ui::menu::Item for JumpMeta {
|
||||
type Data = ();
|
||||
|
||||
fn format(&self, _data: &Self::Data) -> Row {
|
||||
let path = self
|
||||
.path
|
||||
.as_deref()
|
||||
.map(helix_stdx::path::get_relative_path);
|
||||
let path = match path.as_deref().and_then(Path::to_str) {
|
||||
Some(path) => path,
|
||||
None => SCRATCH_BUFFER_NAME,
|
||||
};
|
||||
|
||||
let mut flags = Vec::new();
|
||||
if self.is_current {
|
||||
flags.push("*");
|
||||
}
|
||||
|
||||
let flag = if flags.is_empty() {
|
||||
"".into()
|
||||
} else {
|
||||
format!(" ({})", flags.join(""))
|
||||
};
|
||||
format!("{} {}{} {}", self.id, path, flag, self.text).into()
|
||||
}
|
||||
}
|
||||
|
||||
for (view, _) in cx.editor.tree.views_mut() {
|
||||
for doc_id in view.jumps.iter().map(|e| e.0).collect::<Vec<_>>().iter() {
|
||||
let doc = doc_mut!(cx.editor, doc_id);
|
||||
|
@ -3014,17 +2960,43 @@ fn jumplist_picker(cx: &mut Context) {
|
|||
}
|
||||
};
|
||||
|
||||
let columns = [
|
||||
ui::PickerColumn::new("id", |item: &JumpMeta, _| item.id.to_string().into()),
|
||||
ui::PickerColumn::new("path", |item: &JumpMeta, _| {
|
||||
let path = item
|
||||
.path
|
||||
.as_deref()
|
||||
.map(helix_stdx::path::get_relative_path);
|
||||
path.as_deref()
|
||||
.and_then(Path::to_str)
|
||||
.unwrap_or(SCRATCH_BUFFER_NAME)
|
||||
.to_string()
|
||||
.into()
|
||||
}),
|
||||
ui::PickerColumn::new("flags", |item: &JumpMeta, _| {
|
||||
let mut flags = Vec::new();
|
||||
if item.is_current {
|
||||
flags.push("*");
|
||||
}
|
||||
|
||||
if flags.is_empty() {
|
||||
"".into()
|
||||
} else {
|
||||
format!(" ({})", flags.join("")).into()
|
||||
}
|
||||
}),
|
||||
ui::PickerColumn::new("contents", |item: &JumpMeta, _| item.text.as_str().into()),
|
||||
];
|
||||
|
||||
let picker = Picker::new(
|
||||
cx.editor
|
||||
.tree
|
||||
.views()
|
||||
.flat_map(|(view, _)| {
|
||||
view.jumps
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|(doc_id, selection)| new_meta(view, *doc_id, selection.clone()))
|
||||
})
|
||||
.collect(),
|
||||
columns,
|
||||
1, // path
|
||||
cx.editor.tree.views().flat_map(|(view, _)| {
|
||||
view.jumps
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|(doc_id, selection)| new_meta(view, *doc_id, selection.clone()))
|
||||
}),
|
||||
(),
|
||||
|cx, meta, action| {
|
||||
cx.editor.switch(meta.id, action);
|
||||
|
@ -3054,33 +3026,6 @@ fn changed_file_picker(cx: &mut Context) {
|
|||
style_renamed: Style,
|
||||
}
|
||||
|
||||
impl Item for FileChange {
|
||||
type Data = FileChangeData;
|
||||
|
||||
fn format(&self, data: &Self::Data) -> Row {
|
||||
let process_path = |path: &PathBuf| {
|
||||
path.strip_prefix(&data.cwd)
|
||||
.unwrap_or(path)
|
||||
.display()
|
||||
.to_string()
|
||||
};
|
||||
|
||||
let (sign, style, content) = match self {
|
||||
Self::Untracked { path } => ("[+]", data.style_untracked, process_path(path)),
|
||||
Self::Modified { path } => ("[~]", data.style_modified, process_path(path)),
|
||||
Self::Conflict { path } => ("[x]", data.style_conflict, process_path(path)),
|
||||
Self::Deleted { path } => ("[-]", data.style_deleted, process_path(path)),
|
||||
Self::Renamed { from_path, to_path } => (
|
||||
"[>]",
|
||||
data.style_renamed,
|
||||
format!("{} -> {}", process_path(from_path), process_path(to_path)),
|
||||
),
|
||||
};
|
||||
|
||||
Row::new([Cell::from(Span::styled(sign, style)), Cell::from(content)])
|
||||
}
|
||||
}
|
||||
|
||||
let cwd = helix_stdx::env::current_working_dir();
|
||||
if !cwd.exists() {
|
||||
cx.editor
|
||||
|
@ -3094,8 +3039,41 @@ fn changed_file_picker(cx: &mut Context) {
|
|||
let deleted = cx.editor.theme.get("diff.minus");
|
||||
let renamed = cx.editor.theme.get("diff.delta.moved");
|
||||
|
||||
let columns = [
|
||||
PickerColumn::new("change", |change: &FileChange, data: &FileChangeData| {
|
||||
match change {
|
||||
FileChange::Untracked { .. } => Span::styled("+ untracked", data.style_untracked),
|
||||
FileChange::Modified { .. } => Span::styled("~ modified", data.style_modified),
|
||||
FileChange::Conflict { .. } => Span::styled("x conflict", data.style_conflict),
|
||||
FileChange::Deleted { .. } => Span::styled("- deleted", data.style_deleted),
|
||||
FileChange::Renamed { .. } => Span::styled("> renamed", data.style_renamed),
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
PickerColumn::new("path", |change: &FileChange, data: &FileChangeData| {
|
||||
let display_path = |path: &PathBuf| {
|
||||
path.strip_prefix(&data.cwd)
|
||||
.unwrap_or(path)
|
||||
.display()
|
||||
.to_string()
|
||||
};
|
||||
match change {
|
||||
FileChange::Untracked { path } => display_path(path),
|
||||
FileChange::Modified { path } => display_path(path),
|
||||
FileChange::Conflict { path } => display_path(path),
|
||||
FileChange::Deleted { path } => display_path(path),
|
||||
FileChange::Renamed { from_path, to_path } => {
|
||||
format!("{} -> {}", display_path(from_path), display_path(to_path))
|
||||
}
|
||||
}
|
||||
.into()
|
||||
}),
|
||||
];
|
||||
|
||||
let picker = Picker::new(
|
||||
Vec::new(),
|
||||
columns,
|
||||
1, // path
|
||||
[],
|
||||
FileChangeData {
|
||||
cwd: cwd.clone(),
|
||||
style_untracked: added,
|
||||
|
@ -3116,7 +3094,7 @@ fn changed_file_picker(cx: &mut Context) {
|
|||
}
|
||||
},
|
||||
)
|
||||
.with_preview(|_editor, meta| Some((meta.path().to_path_buf().into(), None)));
|
||||
.with_preview(|_editor, meta| Some((meta.path().into(), None)));
|
||||
let injector = picker.injector();
|
||||
|
||||
cx.editor
|
||||
|
@ -3132,35 +3110,6 @@ fn changed_file_picker(cx: &mut Context) {
|
|||
cx.push_layer(Box::new(overlaid(picker)));
|
||||
}
|
||||
|
||||
impl ui::menu::Item for MappableCommand {
|
||||
type Data = ReverseKeymap;
|
||||
|
||||
fn format(&self, keymap: &Self::Data) -> Row {
|
||||
let fmt_binding = |bindings: &Vec<Vec<KeyEvent>>| -> String {
|
||||
bindings.iter().fold(String::new(), |mut acc, bind| {
|
||||
if !acc.is_empty() {
|
||||
acc.push(' ');
|
||||
}
|
||||
for key in bind {
|
||||
acc.push_str(&key.key_sequence_format());
|
||||
}
|
||||
acc
|
||||
})
|
||||
};
|
||||
|
||||
match self {
|
||||
MappableCommand::Typable { doc, name, .. } => match keymap.get(name as &String) {
|
||||
Some(bindings) => format!("{} ({}) [:{}]", doc, fmt_binding(bindings), name).into(),
|
||||
None => format!("{} [:{}]", doc, name).into(),
|
||||
},
|
||||
MappableCommand::Static { doc, name, .. } => match keymap.get(*name) {
|
||||
Some(bindings) => format!("{} ({}) [{}]", doc, fmt_binding(bindings), name).into(),
|
||||
None => format!("{} [{}]", doc, name).into(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn command_palette(cx: &mut Context) {
|
||||
let register = cx.register;
|
||||
let count = cx.count;
|
||||
|
@ -3171,16 +3120,45 @@ pub fn command_palette(cx: &mut Context) {
|
|||
[&cx.editor.mode]
|
||||
.reverse_map();
|
||||
|
||||
let mut commands: Vec<MappableCommand> = MappableCommand::STATIC_COMMAND_LIST.into();
|
||||
commands.extend(typed::TYPABLE_COMMAND_LIST.iter().map(|cmd| {
|
||||
MappableCommand::Typable {
|
||||
name: cmd.name.to_owned(),
|
||||
doc: cmd.doc.to_owned(),
|
||||
args: Vec::new(),
|
||||
}
|
||||
}));
|
||||
let commands = MappableCommand::STATIC_COMMAND_LIST.iter().cloned().chain(
|
||||
typed::TYPABLE_COMMAND_LIST
|
||||
.iter()
|
||||
.map(|cmd| MappableCommand::Typable {
|
||||
name: cmd.name.to_owned(),
|
||||
args: Vec::new(),
|
||||
doc: cmd.doc.to_owned(),
|
||||
}),
|
||||
);
|
||||
|
||||
let picker = Picker::new(commands, keymap, move |cx, command, _action| {
|
||||
let columns = [
|
||||
ui::PickerColumn::new("name", |item, _| match item {
|
||||
MappableCommand::Typable { name, .. } => format!(":{name}").into(),
|
||||
MappableCommand::Static { name, .. } => (*name).into(),
|
||||
}),
|
||||
ui::PickerColumn::new(
|
||||
"bindings",
|
||||
|item: &MappableCommand, keymap: &crate::keymap::ReverseKeymap| {
|
||||
keymap
|
||||
.get(item.name())
|
||||
.map(|bindings| {
|
||||
bindings.iter().fold(String::new(), |mut acc, bind| {
|
||||
if !acc.is_empty() {
|
||||
acc.push(' ');
|
||||
}
|
||||
for key in bind {
|
||||
acc.push_str(&key.key_sequence_format());
|
||||
}
|
||||
acc
|
||||
})
|
||||
})
|
||||
.unwrap_or_default()
|
||||
.into()
|
||||
},
|
||||
),
|
||||
ui::PickerColumn::new("doc", |item: &MappableCommand, _| item.doc().into()),
|
||||
];
|
||||
|
||||
let picker = Picker::new(columns, 0, commands, keymap, move |cx, command, _action| {
|
||||
let mut ctx = Context {
|
||||
register,
|
||||
count,
|
||||
|
|
|
@ -12,7 +12,7 @@ use helix_view::editor::Breakpoint;
|
|||
|
||||
use serde_json::{to_value, Value};
|
||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||
use tui::{text::Spans, widgets::Row};
|
||||
use tui::text::Spans;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::future::Future;
|
||||
|
@ -22,38 +22,6 @@ use anyhow::{anyhow, bail};
|
|||
|
||||
use helix_view::handlers::dap::{breakpoints_changed, jump_to_stack_frame, select_thread_id};
|
||||
|
||||
impl ui::menu::Item for StackFrame {
|
||||
type Data = ();
|
||||
|
||||
fn format(&self, _data: &Self::Data) -> Row {
|
||||
self.name.as_str().into() // TODO: include thread_states in the label
|
||||
}
|
||||
}
|
||||
|
||||
impl ui::menu::Item for DebugTemplate {
|
||||
type Data = ();
|
||||
|
||||
fn format(&self, _data: &Self::Data) -> Row {
|
||||
self.name.as_str().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl ui::menu::Item for Thread {
|
||||
type Data = ThreadStates;
|
||||
|
||||
fn format(&self, thread_states: &Self::Data) -> Row {
|
||||
format!(
|
||||
"{} ({})",
|
||||
self.name,
|
||||
thread_states
|
||||
.get(&self.id)
|
||||
.map(|state| state.as_str())
|
||||
.unwrap_or("unknown")
|
||||
)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
fn thread_picker(
|
||||
cx: &mut Context,
|
||||
callback_fn: impl Fn(&mut Editor, &dap::Thread) + Send + 'static,
|
||||
|
@ -73,13 +41,27 @@ fn thread_picker(
|
|||
let debugger = debugger!(editor);
|
||||
|
||||
let thread_states = debugger.thread_states.clone();
|
||||
let picker = Picker::new(threads, thread_states, move |cx, thread, _action| {
|
||||
callback_fn(cx.editor, thread)
|
||||
})
|
||||
let columns = [
|
||||
ui::PickerColumn::new("name", |item: &Thread, _| item.name.as_str().into()),
|
||||
ui::PickerColumn::new("state", |item: &Thread, thread_states: &ThreadStates| {
|
||||
thread_states
|
||||
.get(&item.id)
|
||||
.map(|state| state.as_str())
|
||||
.unwrap_or("unknown")
|
||||
.into()
|
||||
}),
|
||||
];
|
||||
let picker = Picker::new(
|
||||
columns,
|
||||
0,
|
||||
threads,
|
||||
thread_states,
|
||||
move |cx, thread, _action| callback_fn(cx.editor, thread),
|
||||
)
|
||||
.with_preview(move |editor, thread| {
|
||||
let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?;
|
||||
let frame = frames.first()?;
|
||||
let path = frame.source.as_ref()?.path.clone()?;
|
||||
let path = frame.source.as_ref()?.path.as_ref()?.as_path();
|
||||
let pos = Some((
|
||||
frame.line.saturating_sub(1),
|
||||
frame.end_line.unwrap_or(frame.line).saturating_sub(1),
|
||||
|
@ -268,7 +250,14 @@ pub fn dap_launch(cx: &mut Context) {
|
|||
|
||||
let templates = config.templates.clone();
|
||||
|
||||
let columns = [ui::PickerColumn::new(
|
||||
"template",
|
||||
|item: &DebugTemplate, _| item.name.as_str().into(),
|
||||
)];
|
||||
|
||||
cx.push_layer(Box::new(overlaid(Picker::new(
|
||||
columns,
|
||||
0,
|
||||
templates,
|
||||
(),
|
||||
|cx, template, _action| {
|
||||
|
@ -736,7 +725,10 @@ pub fn dap_switch_stack_frame(cx: &mut Context) {
|
|||
|
||||
let frames = debugger.stack_frames[&thread_id].clone();
|
||||
|
||||
let picker = Picker::new(frames, (), move |cx, frame, _action| {
|
||||
let columns = [ui::PickerColumn::new("frame", |item: &StackFrame, _| {
|
||||
item.name.as_str().into() // TODO: include thread_states in the label
|
||||
})];
|
||||
let picker = Picker::new(columns, 0, frames, (), move |cx, frame, _action| {
|
||||
let debugger = debugger!(cx.editor);
|
||||
// TODO: this should be simpler to find
|
||||
let pos = debugger.stack_frames[&thread_id]
|
||||
|
@ -755,10 +747,10 @@ pub fn dap_switch_stack_frame(cx: &mut Context) {
|
|||
frame
|
||||
.source
|
||||
.as_ref()
|
||||
.and_then(|source| source.path.clone())
|
||||
.and_then(|source| source.path.as_ref())
|
||||
.map(|path| {
|
||||
(
|
||||
path.into(),
|
||||
path.as_path().into(),
|
||||
Some((
|
||||
frame.line.saturating_sub(1),
|
||||
frame.end_line.unwrap_or(frame.line).saturating_sub(1),
|
||||
|
|
|
@ -9,14 +9,13 @@ use helix_lsp::{
|
|||
Client, LanguageServerId, OffsetEncoding,
|
||||
};
|
||||
use tokio_stream::StreamExt;
|
||||
use tui::{
|
||||
text::{Span, Spans},
|
||||
widgets::Row,
|
||||
};
|
||||
use tui::{text::Span, widgets::Row};
|
||||
|
||||
use super::{align_view, push_jump, Align, Context, Editor};
|
||||
|
||||
use helix_core::{syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Selection};
|
||||
use helix_core::{
|
||||
syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Selection, Uri,
|
||||
};
|
||||
use helix_stdx::path;
|
||||
use helix_view::{
|
||||
document::{DocumentInlayHints, DocumentInlayHintsId},
|
||||
|
@ -29,7 +28,7 @@ use helix_view::{
|
|||
use crate::{
|
||||
compositor::{self, Compositor},
|
||||
job::Callback,
|
||||
ui::{self, overlay::overlaid, DynamicPicker, FileLocation, Picker, Popup, PromptEvent},
|
||||
ui::{self, overlay::overlaid, FileLocation, Picker, Popup, PromptEvent},
|
||||
};
|
||||
|
||||
use std::{
|
||||
|
@ -37,7 +36,7 @@ use std::{
|
|||
collections::{BTreeMap, HashSet},
|
||||
fmt::Write,
|
||||
future::Future,
|
||||
path::{Path, PathBuf},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
/// Gets the first language server that is attached to a document which supports a specific feature.
|
||||
|
@ -62,67 +61,10 @@ macro_rules! language_server_with_feature {
|
|||
}};
|
||||
}
|
||||
|
||||
impl ui::menu::Item for lsp::Location {
|
||||
/// Current working directory.
|
||||
type Data = PathBuf;
|
||||
|
||||
fn format(&self, cwdir: &Self::Data) -> Row {
|
||||
// The preallocation here will overallocate a few characters since it will account for the
|
||||
// URL's scheme, which is not used most of the time since that scheme will be "file://".
|
||||
// Those extra chars will be used to avoid allocating when writing the line number (in the
|
||||
// common case where it has 5 digits or less, which should be enough for a cast majority
|
||||
// of usages).
|
||||
let mut res = String::with_capacity(self.uri.as_str().len());
|
||||
|
||||
if self.uri.scheme() == "file" {
|
||||
// With the preallocation above and UTF-8 paths already, this closure will do one (1)
|
||||
// allocation, for `to_file_path`, else there will be two (2), with `to_string_lossy`.
|
||||
let mut write_path_to_res = || -> Option<()> {
|
||||
let path = self.uri.to_file_path().ok()?;
|
||||
res.push_str(&path.strip_prefix(cwdir).unwrap_or(&path).to_string_lossy());
|
||||
Some(())
|
||||
};
|
||||
write_path_to_res();
|
||||
} else {
|
||||
// Never allocates since we declared the string with this capacity already.
|
||||
res.push_str(self.uri.as_str());
|
||||
}
|
||||
|
||||
// Most commonly, this will not allocate, especially on Unix systems where the root prefix
|
||||
// is a simple `/` and not `C:\` (with whatever drive letter)
|
||||
write!(&mut res, ":{}", self.range.start.line + 1)
|
||||
.expect("Will only failed if allocating fail");
|
||||
res.into()
|
||||
}
|
||||
}
|
||||
|
||||
struct SymbolInformationItem {
|
||||
symbol: lsp::SymbolInformation,
|
||||
offset_encoding: OffsetEncoding,
|
||||
}
|
||||
|
||||
impl ui::menu::Item for SymbolInformationItem {
|
||||
/// Path to currently focussed document
|
||||
type Data = Option<lsp::Url>;
|
||||
|
||||
fn format(&self, current_doc_path: &Self::Data) -> Row {
|
||||
if current_doc_path.as_ref() == Some(&self.symbol.location.uri) {
|
||||
self.symbol.name.as_str().into()
|
||||
} else {
|
||||
match self.symbol.location.uri.to_file_path() {
|
||||
Ok(path) => {
|
||||
let get_relative_path = path::get_relative_path(path.as_path());
|
||||
format!(
|
||||
"{} ({})",
|
||||
&self.symbol.name,
|
||||
get_relative_path.to_string_lossy()
|
||||
)
|
||||
.into()
|
||||
}
|
||||
Err(_) => format!("{} ({})", &self.symbol.name, &self.symbol.location.uri).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
uri: Uri,
|
||||
}
|
||||
|
||||
struct DiagnosticStyles {
|
||||
|
@ -133,60 +75,15 @@ struct DiagnosticStyles {
|
|||
}
|
||||
|
||||
struct PickerDiagnostic {
|
||||
path: PathBuf,
|
||||
uri: Uri,
|
||||
diag: lsp::Diagnostic,
|
||||
offset_encoding: OffsetEncoding,
|
||||
}
|
||||
|
||||
impl ui::menu::Item for PickerDiagnostic {
|
||||
type Data = (DiagnosticStyles, DiagnosticsFormat);
|
||||
|
||||
fn format(&self, (styles, format): &Self::Data) -> Row {
|
||||
let mut style = self
|
||||
.diag
|
||||
.severity
|
||||
.map(|s| match s {
|
||||
DiagnosticSeverity::HINT => styles.hint,
|
||||
DiagnosticSeverity::INFORMATION => styles.info,
|
||||
DiagnosticSeverity::WARNING => styles.warning,
|
||||
DiagnosticSeverity::ERROR => styles.error,
|
||||
_ => Style::default(),
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// remove background as it is distracting in the picker list
|
||||
style.bg = None;
|
||||
|
||||
let code = match self.diag.code.as_ref() {
|
||||
Some(NumberOrString::Number(n)) => format!(" ({n})"),
|
||||
Some(NumberOrString::String(s)) => format!(" ({s})"),
|
||||
None => String::new(),
|
||||
};
|
||||
|
||||
let path = match format {
|
||||
DiagnosticsFormat::HideSourcePath => String::new(),
|
||||
DiagnosticsFormat::ShowSourcePath => {
|
||||
let path = path::get_truncated_path(&self.path);
|
||||
format!("{}: ", path.to_string_lossy())
|
||||
}
|
||||
};
|
||||
|
||||
Spans::from(vec![
|
||||
Span::raw(path),
|
||||
Span::styled(&self.diag.message, style),
|
||||
Span::styled(code, style),
|
||||
])
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
fn location_to_file_location(location: &lsp::Location) -> FileLocation {
|
||||
let path = location.uri.to_file_path().unwrap();
|
||||
let line = Some((
|
||||
location.range.start.line as usize,
|
||||
location.range.end.line as usize,
|
||||
));
|
||||
(path.into(), line)
|
||||
fn uri_to_file_location<'a>(uri: &'a Uri, range: &lsp::Range) -> Option<FileLocation<'a>> {
|
||||
let path = uri.as_path()?;
|
||||
let line = Some((range.start.line as usize, range.end.line as usize));
|
||||
Some((path.into(), line))
|
||||
}
|
||||
|
||||
fn jump_to_location(
|
||||
|
@ -241,20 +138,39 @@ fn jump_to_position(
|
|||
}
|
||||
}
|
||||
|
||||
type SymbolPicker = Picker<SymbolInformationItem>;
|
||||
|
||||
fn sym_picker(symbols: Vec<SymbolInformationItem>, current_path: Option<lsp::Url>) -> SymbolPicker {
|
||||
// TODO: drop current_path comparison and instead use workspace: bool flag?
|
||||
Picker::new(symbols, current_path, move |cx, item, action| {
|
||||
jump_to_location(
|
||||
cx.editor,
|
||||
&item.symbol.location,
|
||||
item.offset_encoding,
|
||||
action,
|
||||
);
|
||||
})
|
||||
.with_preview(move |_editor, item| Some(location_to_file_location(&item.symbol.location)))
|
||||
.truncate_start(false)
|
||||
fn display_symbol_kind(kind: lsp::SymbolKind) -> &'static str {
|
||||
match kind {
|
||||
lsp::SymbolKind::FILE => "file",
|
||||
lsp::SymbolKind::MODULE => "module",
|
||||
lsp::SymbolKind::NAMESPACE => "namespace",
|
||||
lsp::SymbolKind::PACKAGE => "package",
|
||||
lsp::SymbolKind::CLASS => "class",
|
||||
lsp::SymbolKind::METHOD => "method",
|
||||
lsp::SymbolKind::PROPERTY => "property",
|
||||
lsp::SymbolKind::FIELD => "field",
|
||||
lsp::SymbolKind::CONSTRUCTOR => "construct",
|
||||
lsp::SymbolKind::ENUM => "enum",
|
||||
lsp::SymbolKind::INTERFACE => "interface",
|
||||
lsp::SymbolKind::FUNCTION => "function",
|
||||
lsp::SymbolKind::VARIABLE => "variable",
|
||||
lsp::SymbolKind::CONSTANT => "constant",
|
||||
lsp::SymbolKind::STRING => "string",
|
||||
lsp::SymbolKind::NUMBER => "number",
|
||||
lsp::SymbolKind::BOOLEAN => "boolean",
|
||||
lsp::SymbolKind::ARRAY => "array",
|
||||
lsp::SymbolKind::OBJECT => "object",
|
||||
lsp::SymbolKind::KEY => "key",
|
||||
lsp::SymbolKind::NULL => "null",
|
||||
lsp::SymbolKind::ENUM_MEMBER => "enummem",
|
||||
lsp::SymbolKind::STRUCT => "struct",
|
||||
lsp::SymbolKind::EVENT => "event",
|
||||
lsp::SymbolKind::OPERATOR => "operator",
|
||||
lsp::SymbolKind::TYPE_PARAMETER => "typeparam",
|
||||
_ => {
|
||||
log::warn!("Unknown symbol kind: {:?}", kind);
|
||||
""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
|
@ -263,22 +179,24 @@ enum DiagnosticsFormat {
|
|||
HideSourcePath,
|
||||
}
|
||||
|
||||
type DiagnosticsPicker = Picker<PickerDiagnostic, DiagnosticStyles>;
|
||||
|
||||
fn diag_picker(
|
||||
cx: &Context,
|
||||
diagnostics: BTreeMap<PathBuf, Vec<(lsp::Diagnostic, LanguageServerId)>>,
|
||||
diagnostics: BTreeMap<Uri, Vec<(lsp::Diagnostic, LanguageServerId)>>,
|
||||
format: DiagnosticsFormat,
|
||||
) -> Picker<PickerDiagnostic> {
|
||||
) -> DiagnosticsPicker {
|
||||
// TODO: drop current_path comparison and instead use workspace: bool flag?
|
||||
|
||||
// flatten the map to a vec of (url, diag) pairs
|
||||
let mut flat_diag = Vec::new();
|
||||
for (path, diags) in diagnostics {
|
||||
for (uri, diags) in diagnostics {
|
||||
flat_diag.reserve(diags.len());
|
||||
|
||||
for (diag, ls) in diags {
|
||||
if let Some(ls) = cx.editor.language_server_by_id(ls) {
|
||||
flat_diag.push(PickerDiagnostic {
|
||||
path: path.clone(),
|
||||
uri: uri.clone(),
|
||||
diag,
|
||||
offset_encoding: ls.offset_encoding(),
|
||||
});
|
||||
|
@ -293,22 +211,72 @@ fn diag_picker(
|
|||
error: cx.editor.theme.get("error"),
|
||||
};
|
||||
|
||||
let mut columns = vec![
|
||||
ui::PickerColumn::new(
|
||||
"severity",
|
||||
|item: &PickerDiagnostic, styles: &DiagnosticStyles| {
|
||||
match item.diag.severity {
|
||||
Some(DiagnosticSeverity::HINT) => Span::styled("HINT", styles.hint),
|
||||
Some(DiagnosticSeverity::INFORMATION) => Span::styled("INFO", styles.info),
|
||||
Some(DiagnosticSeverity::WARNING) => Span::styled("WARN", styles.warning),
|
||||
Some(DiagnosticSeverity::ERROR) => Span::styled("ERROR", styles.error),
|
||||
_ => Span::raw(""),
|
||||
}
|
||||
.into()
|
||||
},
|
||||
),
|
||||
ui::PickerColumn::new("code", |item: &PickerDiagnostic, _| {
|
||||
match item.diag.code.as_ref() {
|
||||
Some(NumberOrString::Number(n)) => n.to_string().into(),
|
||||
Some(NumberOrString::String(s)) => s.as_str().into(),
|
||||
None => "".into(),
|
||||
}
|
||||
}),
|
||||
ui::PickerColumn::new("message", |item: &PickerDiagnostic, _| {
|
||||
item.diag.message.as_str().into()
|
||||
}),
|
||||
];
|
||||
let mut primary_column = 2; // message
|
||||
|
||||
if format == DiagnosticsFormat::ShowSourcePath {
|
||||
columns.insert(
|
||||
// between message code and message
|
||||
2,
|
||||
ui::PickerColumn::new("path", |item: &PickerDiagnostic, _| {
|
||||
if let Some(path) = item.uri.as_path() {
|
||||
path::get_truncated_path(path)
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
.into()
|
||||
} else {
|
||||
Default::default()
|
||||
}
|
||||
}),
|
||||
);
|
||||
primary_column += 1;
|
||||
}
|
||||
|
||||
Picker::new(
|
||||
columns,
|
||||
primary_column,
|
||||
flat_diag,
|
||||
(styles, format),
|
||||
styles,
|
||||
move |cx,
|
||||
PickerDiagnostic {
|
||||
path,
|
||||
uri,
|
||||
diag,
|
||||
offset_encoding,
|
||||
},
|
||||
action| {
|
||||
let Some(path) = uri.as_path() else {
|
||||
return;
|
||||
};
|
||||
jump_to_position(cx.editor, path, diag.range, *offset_encoding, action)
|
||||
},
|
||||
)
|
||||
.with_preview(move |_editor, PickerDiagnostic { path, diag, .. }| {
|
||||
.with_preview(move |_editor, PickerDiagnostic { uri, diag, .. }| {
|
||||
let line = Some((diag.range.start.line as usize, diag.range.end.line as usize));
|
||||
Some((path.clone().into(), line))
|
||||
Some((uri.as_path()?.into(), line))
|
||||
})
|
||||
.truncate_start(false)
|
||||
}
|
||||
|
@ -317,6 +285,7 @@ pub fn symbol_picker(cx: &mut Context) {
|
|||
fn nested_to_flat(
|
||||
list: &mut Vec<SymbolInformationItem>,
|
||||
file: &lsp::TextDocumentIdentifier,
|
||||
uri: &Uri,
|
||||
symbol: lsp::DocumentSymbol,
|
||||
offset_encoding: OffsetEncoding,
|
||||
) {
|
||||
|
@ -331,9 +300,10 @@ pub fn symbol_picker(cx: &mut Context) {
|
|||
container_name: None,
|
||||
},
|
||||
offset_encoding,
|
||||
uri: uri.clone(),
|
||||
});
|
||||
for child in symbol.children.into_iter().flatten() {
|
||||
nested_to_flat(list, file, child, offset_encoding);
|
||||
nested_to_flat(list, file, uri, child, offset_encoding);
|
||||
}
|
||||
}
|
||||
let doc = doc!(cx.editor);
|
||||
|
@ -347,6 +317,9 @@ pub fn symbol_picker(cx: &mut Context) {
|
|||
let request = language_server.document_symbols(doc.identifier()).unwrap();
|
||||
let offset_encoding = language_server.offset_encoding();
|
||||
let doc_id = doc.identifier();
|
||||
let doc_uri = doc
|
||||
.uri()
|
||||
.expect("docs with active language servers must be backed by paths");
|
||||
|
||||
async move {
|
||||
let json = request.await?;
|
||||
|
@ -361,6 +334,7 @@ pub fn symbol_picker(cx: &mut Context) {
|
|||
lsp::DocumentSymbolResponse::Flat(symbols) => symbols
|
||||
.into_iter()
|
||||
.map(|symbol| SymbolInformationItem {
|
||||
uri: doc_uri.clone(),
|
||||
symbol,
|
||||
offset_encoding,
|
||||
})
|
||||
|
@ -368,7 +342,13 @@ pub fn symbol_picker(cx: &mut Context) {
|
|||
lsp::DocumentSymbolResponse::Nested(symbols) => {
|
||||
let mut flat_symbols = Vec::new();
|
||||
for symbol in symbols {
|
||||
nested_to_flat(&mut flat_symbols, &doc_id, symbol, offset_encoding)
|
||||
nested_to_flat(
|
||||
&mut flat_symbols,
|
||||
&doc_id,
|
||||
&doc_uri,
|
||||
symbol,
|
||||
offset_encoding,
|
||||
)
|
||||
}
|
||||
flat_symbols
|
||||
}
|
||||
|
@ -377,7 +357,6 @@ pub fn symbol_picker(cx: &mut Context) {
|
|||
}
|
||||
})
|
||||
.collect();
|
||||
let current_url = doc.url();
|
||||
|
||||
if futures.is_empty() {
|
||||
cx.editor
|
||||
|
@ -392,7 +371,37 @@ pub fn symbol_picker(cx: &mut Context) {
|
|||
symbols.append(&mut lsp_items);
|
||||
}
|
||||
let call = move |_editor: &mut Editor, compositor: &mut Compositor| {
|
||||
let picker = sym_picker(symbols, current_url);
|
||||
let columns = [
|
||||
ui::PickerColumn::new("kind", |item: &SymbolInformationItem, _| {
|
||||
display_symbol_kind(item.symbol.kind).into()
|
||||
}),
|
||||
// Some symbols in the document symbol picker may have a URI that isn't
|
||||
// the current file. It should be rare though, so we concatenate that
|
||||
// URI in with the symbol name in this picker.
|
||||
ui::PickerColumn::new("name", |item: &SymbolInformationItem, _| {
|
||||
item.symbol.name.as_str().into()
|
||||
}),
|
||||
];
|
||||
|
||||
let picker = Picker::new(
|
||||
columns,
|
||||
1, // name column
|
||||
symbols,
|
||||
(),
|
||||
move |cx, item, action| {
|
||||
jump_to_location(
|
||||
cx.editor,
|
||||
&item.symbol.location,
|
||||
item.offset_encoding,
|
||||
action,
|
||||
);
|
||||
},
|
||||
)
|
||||
.with_preview(move |_editor, item| {
|
||||
uri_to_file_location(&item.uri, &item.symbol.location.range)
|
||||
})
|
||||
.truncate_start(false);
|
||||
|
||||
compositor.push(Box::new(overlaid(picker)))
|
||||
};
|
||||
|
||||
|
@ -401,6 +410,8 @@ pub fn symbol_picker(cx: &mut Context) {
|
|||
}
|
||||
|
||||
pub fn workspace_symbol_picker(cx: &mut Context) {
|
||||
use crate::ui::picker::Injector;
|
||||
|
||||
let doc = doc!(cx.editor);
|
||||
if doc
|
||||
.language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols)
|
||||
|
@ -412,25 +423,37 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
|
|||
return;
|
||||
}
|
||||
|
||||
let get_symbols = move |pattern: String, editor: &mut Editor| {
|
||||
let get_symbols = |pattern: &str, editor: &mut Editor, _data, injector: &Injector<_, _>| {
|
||||
let doc = doc!(editor);
|
||||
let mut seen_language_servers = HashSet::new();
|
||||
let mut futures: FuturesOrdered<_> = doc
|
||||
.language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols)
|
||||
.filter(|ls| seen_language_servers.insert(ls.id()))
|
||||
.map(|language_server| {
|
||||
let request = language_server.workspace_symbols(pattern.clone()).unwrap();
|
||||
let request = language_server
|
||||
.workspace_symbols(pattern.to_string())
|
||||
.unwrap();
|
||||
let offset_encoding = language_server.offset_encoding();
|
||||
async move {
|
||||
let json = request.await?;
|
||||
|
||||
let response =
|
||||
let response: Vec<_> =
|
||||
serde_json::from_value::<Option<Vec<lsp::SymbolInformation>>>(json)?
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|symbol| SymbolInformationItem {
|
||||
symbol,
|
||||
offset_encoding,
|
||||
.filter_map(|symbol| {
|
||||
let uri = match Uri::try_from(&symbol.location.uri) {
|
||||
Ok(uri) => uri,
|
||||
Err(err) => {
|
||||
log::warn!("discarding symbol with invalid URI: {err}");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
Some(SymbolInformationItem {
|
||||
symbol,
|
||||
uri,
|
||||
offset_encoding,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
@ -443,44 +466,66 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
|
|||
editor.set_error("No configured language server supports workspace symbols");
|
||||
}
|
||||
|
||||
let injector = injector.clone();
|
||||
async move {
|
||||
let mut symbols = Vec::new();
|
||||
// TODO if one symbol request errors, all other requests are discarded (even if they're valid)
|
||||
while let Some(mut lsp_items) = futures.try_next().await? {
|
||||
symbols.append(&mut lsp_items);
|
||||
while let Some(lsp_items) = futures.try_next().await? {
|
||||
for item in lsp_items {
|
||||
injector.push(item)?;
|
||||
}
|
||||
}
|
||||
anyhow::Ok(symbols)
|
||||
Ok(())
|
||||
}
|
||||
.boxed()
|
||||
};
|
||||
let columns = [
|
||||
ui::PickerColumn::new("kind", |item: &SymbolInformationItem, _| {
|
||||
display_symbol_kind(item.symbol.kind).into()
|
||||
}),
|
||||
ui::PickerColumn::new("name", |item: &SymbolInformationItem, _| {
|
||||
item.symbol.name.as_str().into()
|
||||
})
|
||||
.without_filtering(),
|
||||
ui::PickerColumn::new("path", |item: &SymbolInformationItem, _| {
|
||||
if let Some(path) = item.uri.as_path() {
|
||||
path::get_relative_path(path)
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
.into()
|
||||
} else {
|
||||
item.symbol.location.uri.to_string().into()
|
||||
}
|
||||
}),
|
||||
];
|
||||
|
||||
let current_url = doc.url();
|
||||
let initial_symbols = get_symbols("".to_owned(), cx.editor);
|
||||
let picker = Picker::new(
|
||||
columns,
|
||||
1, // name column
|
||||
[],
|
||||
(),
|
||||
move |cx, item, action| {
|
||||
jump_to_location(
|
||||
cx.editor,
|
||||
&item.symbol.location,
|
||||
item.offset_encoding,
|
||||
action,
|
||||
);
|
||||
},
|
||||
)
|
||||
.with_preview(|_editor, item| uri_to_file_location(&item.uri, &item.symbol.location.range))
|
||||
.with_dynamic_query(get_symbols, None)
|
||||
.truncate_start(false);
|
||||
|
||||
cx.jobs.callback(async move {
|
||||
let symbols = initial_symbols.await?;
|
||||
let call = move |_editor: &mut Editor, compositor: &mut Compositor| {
|
||||
let picker = sym_picker(symbols, current_url);
|
||||
let dyn_picker = DynamicPicker::new(picker, Box::new(get_symbols));
|
||||
compositor.push(Box::new(overlaid(dyn_picker)))
|
||||
};
|
||||
|
||||
Ok(Callback::EditorCompositor(Box::new(call)))
|
||||
});
|
||||
cx.push_layer(Box::new(overlaid(picker)));
|
||||
}
|
||||
|
||||
pub fn diagnostics_picker(cx: &mut Context) {
|
||||
let doc = doc!(cx.editor);
|
||||
if let Some(current_path) = doc.path() {
|
||||
let diagnostics = cx
|
||||
.editor
|
||||
.diagnostics
|
||||
.get(current_path)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
if let Some(uri) = doc.uri() {
|
||||
let diagnostics = cx.editor.diagnostics.get(&uri).cloned().unwrap_or_default();
|
||||
let picker = diag_picker(
|
||||
cx,
|
||||
[(current_path.clone(), diagnostics)].into(),
|
||||
[(uri, diagnostics)].into(),
|
||||
DiagnosticsFormat::HideSourcePath,
|
||||
);
|
||||
cx.push_layer(Box::new(overlaid(picker)));
|
||||
|
@ -741,13 +786,6 @@ pub fn code_action(cx: &mut Context) {
|
|||
});
|
||||
}
|
||||
|
||||
impl ui::menu::Item for lsp::Command {
|
||||
type Data = ();
|
||||
fn format(&self, _data: &Self::Data) -> Row {
|
||||
self.title.as_str().into()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn execute_lsp_command(
|
||||
editor: &mut Editor,
|
||||
language_server_id: LanguageServerId,
|
||||
|
@ -817,10 +855,67 @@ fn goto_impl(
|
|||
}
|
||||
[] => unreachable!("`locations` should be non-empty for `goto_impl`"),
|
||||
_locations => {
|
||||
let picker = Picker::new(locations, cwdir, move |cx, location, action| {
|
||||
let columns = [ui::PickerColumn::new(
|
||||
"location",
|
||||
|item: &lsp::Location, cwdir: &std::path::PathBuf| {
|
||||
// The preallocation here will overallocate a few characters since it will account for the
|
||||
// URL's scheme, which is not used most of the time since that scheme will be "file://".
|
||||
// Those extra chars will be used to avoid allocating when writing the line number (in the
|
||||
// common case where it has 5 digits or less, which should be enough for a cast majority
|
||||
// of usages).
|
||||
let mut res = String::with_capacity(item.uri.as_str().len());
|
||||
|
||||
if item.uri.scheme() == "file" {
|
||||
// With the preallocation above and UTF-8 paths already, this closure will do one (1)
|
||||
// allocation, for `to_file_path`, else there will be two (2), with `to_string_lossy`.
|
||||
if let Ok(path) = item.uri.to_file_path() {
|
||||
// We don't convert to a `helix_core::Uri` here because we've already checked the scheme.
|
||||
// This path won't be normalized but it's only used for display.
|
||||
res.push_str(
|
||||
&path.strip_prefix(cwdir).unwrap_or(&path).to_string_lossy(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Never allocates since we declared the string with this capacity already.
|
||||
res.push_str(item.uri.as_str());
|
||||
}
|
||||
|
||||
// Most commonly, this will not allocate, especially on Unix systems where the root prefix
|
||||
// is a simple `/` and not `C:\` (with whatever drive letter)
|
||||
write!(&mut res, ":{}", item.range.start.line + 1)
|
||||
.expect("Will only failed if allocating fail");
|
||||
res.into()
|
||||
},
|
||||
)];
|
||||
|
||||
let picker = Picker::new(columns, 0, locations, cwdir, move |cx, location, action| {
|
||||
jump_to_location(cx.editor, location, offset_encoding, action)
|
||||
})
|
||||
.with_preview(move |_editor, location| Some(location_to_file_location(location)));
|
||||
.with_preview(move |_editor, location| {
|
||||
use crate::ui::picker::PathOrId;
|
||||
|
||||
let lines = Some((
|
||||
location.range.start.line as usize,
|
||||
location.range.end.line as usize,
|
||||
));
|
||||
|
||||
// TODO: we should avoid allocating by doing the Uri conversion ahead of time.
|
||||
//
|
||||
// To do this, introduce a `Location` type in `helix-core` that reuses the core
|
||||
// `Uri` type instead of the LSP `Url` type and replaces the LSP `Range` type.
|
||||
// Refactor the callers of `goto_impl` to pass iterators that translate the
|
||||
// LSP location type to the custom one in core, or have them collect and pass
|
||||
// `Vec<Location>`s. Replace the `uri_to_file_location` function with
|
||||
// `location_to_file_location` that takes only `&helix_core::Location` as
|
||||
// parameters.
|
||||
//
|
||||
// By doing this we can also eliminate the duplicated URI info in the
|
||||
// `SymbolInformationItem` type and introduce a custom Symbol type in `helix-core`
|
||||
// which will be reused in the future for tree-sitter based symbol pickers.
|
||||
let path = Uri::try_from(&location.uri).ok()?.as_path_buf()?;
|
||||
#[allow(deprecated)]
|
||||
Some((PathOrId::from_path_buf(path), lines))
|
||||
});
|
||||
compositor.push(Box::new(overlaid(picker)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ use super::*;
|
|||
use helix_core::fuzzy::fuzzy_match;
|
||||
use helix_core::indent::MAX_INDENT;
|
||||
use helix_core::{line_ending, shellwords::Shellwords};
|
||||
use helix_lsp::LanguageServerId;
|
||||
use helix_view::document::{read_to_string, DEFAULT_LANGUAGE_NAME};
|
||||
use helix_view::editor::{CloseError, ConfigEvent};
|
||||
use serde_json::Value;
|
||||
|
@ -1378,16 +1377,6 @@ fn lsp_workspace_command(
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
struct LsIdCommand(LanguageServerId, helix_lsp::lsp::Command);
|
||||
|
||||
impl ui::menu::Item for LsIdCommand {
|
||||
type Data = ();
|
||||
|
||||
fn format(&self, _data: &Self::Data) -> Row {
|
||||
self.1.title.as_str().into()
|
||||
}
|
||||
}
|
||||
|
||||
let doc = doc!(cx.editor);
|
||||
let ls_id_commands = doc
|
||||
.language_servers_with_feature(LanguageServerFeature::WorkspaceCommand)
|
||||
|
@ -1402,7 +1391,7 @@ fn lsp_workspace_command(
|
|||
if args.is_empty() {
|
||||
let commands = ls_id_commands
|
||||
.map(|(ls_id, command)| {
|
||||
LsIdCommand(
|
||||
(
|
||||
ls_id,
|
||||
helix_lsp::lsp::Command {
|
||||
title: command.clone(),
|
||||
|
@ -1415,10 +1404,18 @@ fn lsp_workspace_command(
|
|||
let callback = async move {
|
||||
let call: job::Callback = Callback::EditorCompositor(Box::new(
|
||||
move |_editor: &mut Editor, compositor: &mut Compositor| {
|
||||
let columns = [ui::PickerColumn::new(
|
||||
"title",
|
||||
|(_ls_id, command): &(_, helix_lsp::lsp::Command), _| {
|
||||
command.title.as_str().into()
|
||||
},
|
||||
)];
|
||||
let picker = ui::Picker::new(
|
||||
columns,
|
||||
0,
|
||||
commands,
|
||||
(),
|
||||
move |cx, LsIdCommand(ls_id, command), _action| {
|
||||
move |cx, (ls_id, command), _action| {
|
||||
execute_lsp_command(cx.editor, *ls_id, command.clone());
|
||||
},
|
||||
);
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
use std::{borrow::Cow, cmp::Reverse, path::PathBuf};
|
||||
use std::{borrow::Cow, cmp::Reverse};
|
||||
|
||||
use crate::{
|
||||
compositor::{Callback, Component, Compositor, Context, Event, EventResult},
|
||||
ctrl, key, shift,
|
||||
};
|
||||
use helix_core::fuzzy::MATCHER;
|
||||
use nucleo::pattern::{Atom, AtomKind, CaseMatching};
|
||||
use nucleo::pattern::{Atom, AtomKind, CaseMatching, Normalization};
|
||||
use nucleo::{Config, Utf32Str};
|
||||
use tui::{buffer::Buffer as Surface, widgets::Table};
|
||||
|
||||
|
@ -31,18 +31,6 @@ pub trait Item: Sync + Send + 'static {
|
|||
}
|
||||
}
|
||||
|
||||
impl Item for PathBuf {
|
||||
/// Root prefix to strip.
|
||||
type Data = PathBuf;
|
||||
|
||||
fn format(&self, root_path: &Self::Data) -> Row {
|
||||
self.strip_prefix(root_path)
|
||||
.unwrap_or(self)
|
||||
.to_string_lossy()
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
pub type MenuCallback<T> = Box<dyn Fn(&mut Editor, Option<&T>, MenuEvent)>;
|
||||
|
||||
pub struct Menu<T: Item> {
|
||||
|
@ -92,7 +80,13 @@ impl<T: Item> Menu<T> {
|
|||
pub fn score(&mut self, pattern: &str, incremental: bool) {
|
||||
let mut matcher = MATCHER.lock();
|
||||
matcher.config = Config::DEFAULT;
|
||||
let pattern = Atom::new(pattern, CaseMatching::Ignore, AtomKind::Fuzzy, false);
|
||||
let pattern = Atom::new(
|
||||
pattern,
|
||||
CaseMatching::Ignore,
|
||||
Normalization::Smart,
|
||||
AtomKind::Fuzzy,
|
||||
false,
|
||||
);
|
||||
let mut buf = Vec::new();
|
||||
if incremental {
|
||||
self.matches.retain_mut(|(index, score)| {
|
||||
|
|
|
@ -21,7 +21,7 @@ pub use editor::EditorView;
|
|||
use helix_stdx::rope;
|
||||
pub use markdown::Markdown;
|
||||
pub use menu::Menu;
|
||||
pub use picker::{DynamicPicker, FileLocation, Picker};
|
||||
pub use picker::{Column as PickerColumn, FileLocation, Picker};
|
||||
pub use popup::Popup;
|
||||
pub use prompt::{Prompt, PromptEvent};
|
||||
pub use spinner::{ProgressSpinners, Spinner};
|
||||
|
@ -170,7 +170,9 @@ pub fn raw_regex_prompt(
|
|||
cx.push_layer(Box::new(prompt));
|
||||
}
|
||||
|
||||
pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker<PathBuf> {
|
||||
type FilePicker = Picker<PathBuf, PathBuf>;
|
||||
|
||||
pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePicker {
|
||||
use ignore::{types::TypesBuilder, WalkBuilder};
|
||||
use std::time::Instant;
|
||||
|
||||
|
@ -217,7 +219,16 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker
|
|||
});
|
||||
log::debug!("file_picker init {:?}", Instant::now().duration_since(now));
|
||||
|
||||
let picker = Picker::new(Vec::new(), root, move |cx, path: &PathBuf, action| {
|
||||
let columns = [PickerColumn::new(
|
||||
"path",
|
||||
|item: &PathBuf, root: &PathBuf| {
|
||||
item.strip_prefix(root)
|
||||
.unwrap_or(item)
|
||||
.to_string_lossy()
|
||||
.into()
|
||||
},
|
||||
)];
|
||||
let picker = Picker::new(columns, 0, [], root, move |cx, path: &PathBuf, action| {
|
||||
if let Err(e) = cx.editor.open(path, action) {
|
||||
let err = if let Some(err) = e.source() {
|
||||
format!("{}", err)
|
||||
|
@ -227,7 +238,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker
|
|||
cx.editor.set_error(err);
|
||||
}
|
||||
})
|
||||
.with_preview(|_editor, path| Some((path.clone().into(), None)));
|
||||
.with_preview(|_editor, path| Some((path.as_path().into(), None)));
|
||||
let injector = picker.injector();
|
||||
let timeout = std::time::Instant::now() + std::time::Duration::from_millis(30);
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
182
helix-term/src/ui/picker/handlers.rs
Normal file
182
helix-term/src/ui/picker/handlers.rs
Normal file
|
@ -0,0 +1,182 @@
|
|||
use std::{
|
||||
path::Path,
|
||||
sync::{atomic, Arc},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use helix_event::AsyncHook;
|
||||
use tokio::time::Instant;
|
||||
|
||||
use crate::{job, ui::overlay::Overlay};
|
||||
|
||||
use super::{CachedPreview, DynQueryCallback, Picker};
|
||||
|
||||
pub(super) struct PreviewHighlightHandler<T: 'static + Send + Sync, D: 'static + Send + Sync> {
|
||||
trigger: Option<Arc<Path>>,
|
||||
phantom_data: std::marker::PhantomData<(T, D)>,
|
||||
}
|
||||
|
||||
impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Default for PreviewHighlightHandler<T, D> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
trigger: None,
|
||||
phantom_data: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static + Send + Sync, D: 'static + Send + Sync> AsyncHook
|
||||
for PreviewHighlightHandler<T, D>
|
||||
{
|
||||
type Event = Arc<Path>;
|
||||
|
||||
fn handle_event(
|
||||
&mut self,
|
||||
path: Self::Event,
|
||||
timeout: Option<tokio::time::Instant>,
|
||||
) -> Option<tokio::time::Instant> {
|
||||
if self
|
||||
.trigger
|
||||
.as_ref()
|
||||
.is_some_and(|trigger| trigger == &path)
|
||||
{
|
||||
// If the path hasn't changed, don't reset the debounce
|
||||
timeout
|
||||
} else {
|
||||
self.trigger = Some(path);
|
||||
Some(Instant::now() + Duration::from_millis(150))
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_debounce(&mut self) {
|
||||
let Some(path) = self.trigger.take() else {
|
||||
return;
|
||||
};
|
||||
|
||||
job::dispatch_blocking(move |editor, compositor| {
|
||||
let Some(Overlay {
|
||||
content: picker, ..
|
||||
}) = compositor.find::<Overlay<Picker<T, D>>>()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(CachedPreview::Document(ref mut doc)) = picker.preview_cache.get_mut(&path)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if doc.language_config().is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(language_config) = doc.detect_language_config(&editor.syn_loader.load())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
doc.language = Some(language_config.clone());
|
||||
let text = doc.text().clone();
|
||||
let loader = editor.syn_loader.clone();
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let Some(syntax) = language_config
|
||||
.highlight_config(&loader.load().scopes())
|
||||
.and_then(|highlight_config| {
|
||||
helix_core::Syntax::new(text.slice(..), highlight_config, loader)
|
||||
})
|
||||
else {
|
||||
log::info!("highlighting picker item failed");
|
||||
return;
|
||||
};
|
||||
|
||||
job::dispatch_blocking(move |editor, compositor| {
|
||||
let Some(Overlay {
|
||||
content: picker, ..
|
||||
}) = compositor.find::<Overlay<Picker<T, D>>>()
|
||||
else {
|
||||
log::info!("picker closed before syntax highlighting finished");
|
||||
return;
|
||||
};
|
||||
let Some(CachedPreview::Document(ref mut doc)) =
|
||||
picker.preview_cache.get_mut(&path)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let diagnostics = helix_view::Editor::doc_diagnostics(
|
||||
&editor.language_servers,
|
||||
&editor.diagnostics,
|
||||
doc,
|
||||
);
|
||||
doc.replace_diagnostics(diagnostics, &[], None);
|
||||
doc.syntax = Some(syntax);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct DynamicQueryHandler<T: 'static + Send + Sync, D: 'static + Send + Sync> {
|
||||
callback: Arc<DynQueryCallback<T, D>>,
|
||||
// Duration used as a debounce.
|
||||
// Defaults to 100ms if not provided via `Picker::with_dynamic_query`. Callers may want to set
|
||||
// this higher if the dynamic query is expensive - for example global search.
|
||||
debounce: Duration,
|
||||
last_query: Arc<str>,
|
||||
query: Option<Arc<str>>,
|
||||
}
|
||||
|
||||
impl<T: 'static + Send + Sync, D: 'static + Send + Sync> DynamicQueryHandler<T, D> {
|
||||
pub(super) fn new(callback: DynQueryCallback<T, D>, duration_ms: Option<u64>) -> Self {
|
||||
Self {
|
||||
callback: Arc::new(callback),
|
||||
debounce: Duration::from_millis(duration_ms.unwrap_or(100)),
|
||||
last_query: "".into(),
|
||||
query: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: 'static + Send + Sync, D: 'static + Send + Sync> AsyncHook for DynamicQueryHandler<T, D> {
|
||||
type Event = Arc<str>;
|
||||
|
||||
fn handle_event(&mut self, query: Self::Event, _timeout: Option<Instant>) -> Option<Instant> {
|
||||
if query == self.last_query {
|
||||
// If the search query reverts to the last one we requested, no need to
|
||||
// make a new request.
|
||||
self.query = None;
|
||||
None
|
||||
} else {
|
||||
self.query = Some(query);
|
||||
Some(Instant::now() + self.debounce)
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_debounce(&mut self) {
|
||||
let Some(query) = self.query.take() else {
|
||||
return;
|
||||
};
|
||||
self.last_query = query.clone();
|
||||
let callback = self.callback.clone();
|
||||
|
||||
job::dispatch_blocking(move |editor, compositor| {
|
||||
let Some(Overlay {
|
||||
content: picker, ..
|
||||
}) = compositor.find::<Overlay<Picker<T, D>>>()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
// Increment the version number to cancel any ongoing requests.
|
||||
picker.version.fetch_add(1, atomic::Ordering::Relaxed);
|
||||
picker.matcher.restart(false);
|
||||
let injector = picker.injector();
|
||||
let get_options = (callback)(&query, editor, picker.editor_data.clone(), &injector);
|
||||
tokio::spawn(async move {
|
||||
if let Err(err) = get_options.await {
|
||||
log::info!("Dynamic request failed: {err}");
|
||||
}
|
||||
// NOTE: the Drop implementation of Injector will request a redraw when the
|
||||
// injector falls out of scope here, clearing the "running" indicator.
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
368
helix-term/src/ui/picker/query.rs
Normal file
368
helix-term/src/ui/picker/query.rs
Normal file
|
@ -0,0 +1,368 @@
|
|||
use std::{collections::HashMap, mem, ops::Range, sync::Arc};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct PickerQuery {
|
||||
/// The column names of the picker.
|
||||
column_names: Box<[Arc<str>]>,
|
||||
/// The index of the primary column in `column_names`.
|
||||
/// The primary column is selected by default unless another
|
||||
/// field is specified explicitly with `%fieldname`.
|
||||
primary_column: usize,
|
||||
/// The mapping between column names and input in the query
|
||||
/// for those columns.
|
||||
inner: HashMap<Arc<str>, Arc<str>>,
|
||||
/// The byte ranges of the input text which are used as input for each column.
|
||||
/// This is calculated at parsing time for use in [Self::active_column].
|
||||
/// This Vec is naturally sorted in ascending order and ranges do not overlap.
|
||||
column_ranges: Vec<(Range<usize>, Option<Arc<str>>)>,
|
||||
}
|
||||
|
||||
impl PartialEq<HashMap<Arc<str>, Arc<str>>> for PickerQuery {
|
||||
fn eq(&self, other: &HashMap<Arc<str>, Arc<str>>) -> bool {
|
||||
self.inner.eq(other)
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerQuery {
|
||||
pub(super) fn new<I: Iterator<Item = Arc<str>>>(
|
||||
column_names: I,
|
||||
primary_column: usize,
|
||||
) -> Self {
|
||||
let column_names: Box<[_]> = column_names.collect();
|
||||
let inner = HashMap::with_capacity(column_names.len());
|
||||
let column_ranges = vec![(0..usize::MAX, Some(column_names[primary_column].clone()))];
|
||||
Self {
|
||||
column_names,
|
||||
primary_column,
|
||||
inner,
|
||||
column_ranges,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn get(&self, column: &str) -> Option<&Arc<str>> {
|
||||
self.inner.get(column)
|
||||
}
|
||||
|
||||
pub(super) fn parse(&mut self, input: &str) -> HashMap<Arc<str>, Arc<str>> {
|
||||
let mut fields: HashMap<Arc<str>, String> = HashMap::new();
|
||||
let primary_field = &self.column_names[self.primary_column];
|
||||
let mut escaped = false;
|
||||
let mut in_field = false;
|
||||
let mut field = None;
|
||||
let mut text = String::new();
|
||||
self.column_ranges.clear();
|
||||
self.column_ranges
|
||||
.push((0..usize::MAX, Some(primary_field.clone())));
|
||||
|
||||
macro_rules! finish_field {
|
||||
() => {
|
||||
let key = field.take().unwrap_or(primary_field);
|
||||
|
||||
if let Some(pattern) = fields.get_mut(key) {
|
||||
pattern.push(' ');
|
||||
pattern.push_str(text.trim());
|
||||
} else {
|
||||
fields.insert(key.clone(), text.trim().to_string());
|
||||
}
|
||||
text.clear();
|
||||
};
|
||||
}
|
||||
|
||||
for (idx, ch) in input.char_indices() {
|
||||
match ch {
|
||||
// Backslash escaping
|
||||
_ if escaped => {
|
||||
// '%' is the only character that is special cased.
|
||||
// You can escape it to prevent parsing the text that
|
||||
// follows it as a field name.
|
||||
if ch != '%' {
|
||||
text.push('\\');
|
||||
}
|
||||
text.push(ch);
|
||||
escaped = false;
|
||||
}
|
||||
'\\' => escaped = !escaped,
|
||||
'%' => {
|
||||
if !text.is_empty() {
|
||||
finish_field!();
|
||||
}
|
||||
let (range, _field) = self
|
||||
.column_ranges
|
||||
.last_mut()
|
||||
.expect("column_ranges is non-empty");
|
||||
range.end = idx;
|
||||
in_field = true;
|
||||
}
|
||||
' ' if in_field => {
|
||||
text.clear();
|
||||
in_field = false;
|
||||
}
|
||||
_ if in_field => {
|
||||
text.push(ch);
|
||||
// Go over all columns and their indices, find all that starts with field key,
|
||||
// select a column that fits key the most.
|
||||
field = self
|
||||
.column_names
|
||||
.iter()
|
||||
.filter(|col| col.starts_with(&text))
|
||||
// select "fittest" column
|
||||
.min_by_key(|col| col.len());
|
||||
|
||||
// Update the column range for this column.
|
||||
if let Some((_range, current_field)) = self
|
||||
.column_ranges
|
||||
.last_mut()
|
||||
.filter(|(range, _)| range.end == usize::MAX)
|
||||
{
|
||||
*current_field = field.cloned();
|
||||
} else {
|
||||
self.column_ranges.push((idx..usize::MAX, field.cloned()));
|
||||
}
|
||||
}
|
||||
_ => text.push(ch),
|
||||
}
|
||||
}
|
||||
|
||||
if !in_field && !text.is_empty() {
|
||||
finish_field!();
|
||||
}
|
||||
|
||||
let new_inner: HashMap<_, _> = fields
|
||||
.into_iter()
|
||||
.map(|(field, query)| (field, query.as_str().into()))
|
||||
.collect();
|
||||
|
||||
mem::replace(&mut self.inner, new_inner)
|
||||
}
|
||||
|
||||
/// Finds the column which the cursor is 'within' in the last parse.
|
||||
///
|
||||
/// The cursor is considered to be within a column when it is placed within any
|
||||
/// of a column's text. See the `active_column_test` unit test below for examples.
|
||||
///
|
||||
/// `cursor` is a byte index that represents the location of the prompt's cursor.
|
||||
pub fn active_column(&self, cursor: usize) -> Option<&Arc<str>> {
|
||||
let point = self
|
||||
.column_ranges
|
||||
.partition_point(|(range, _field)| cursor > range.end);
|
||||
|
||||
self.column_ranges
|
||||
.get(point)
|
||||
.filter(|(range, _field)| cursor >= range.start && cursor <= range.end)
|
||||
.and_then(|(_range, field)| field.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use helix_core::hashmap;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_query_test() {
|
||||
let mut query = PickerQuery::new(
|
||||
[
|
||||
"primary".into(),
|
||||
"field1".into(),
|
||||
"field2".into(),
|
||||
"another".into(),
|
||||
"anode".into(),
|
||||
]
|
||||
.into_iter(),
|
||||
0,
|
||||
);
|
||||
|
||||
// Basic field splitting
|
||||
query.parse("hello world");
|
||||
assert_eq!(
|
||||
query,
|
||||
hashmap!(
|
||||
"primary".into() => "hello world".into(),
|
||||
)
|
||||
);
|
||||
query.parse("hello %field1 world %field2 !");
|
||||
assert_eq!(
|
||||
query,
|
||||
hashmap!(
|
||||
"primary".into() => "hello".into(),
|
||||
"field1".into() => "world".into(),
|
||||
"field2".into() => "!".into(),
|
||||
)
|
||||
);
|
||||
query.parse("%field1 abc %field2 def xyz");
|
||||
assert_eq!(
|
||||
query,
|
||||
hashmap!(
|
||||
"field1".into() => "abc".into(),
|
||||
"field2".into() => "def xyz".into(),
|
||||
)
|
||||
);
|
||||
|
||||
// Trailing space is trimmed
|
||||
query.parse("hello ");
|
||||
assert_eq!(
|
||||
query,
|
||||
hashmap!(
|
||||
"primary".into() => "hello".into(),
|
||||
)
|
||||
);
|
||||
|
||||
// Unknown fields are trimmed.
|
||||
query.parse("hello %foo");
|
||||
assert_eq!(
|
||||
query,
|
||||
hashmap!(
|
||||
"primary".into() => "hello".into(),
|
||||
)
|
||||
);
|
||||
|
||||
// Multiple words in a field
|
||||
query.parse("hello %field1 a b c");
|
||||
assert_eq!(
|
||||
query,
|
||||
hashmap!(
|
||||
"primary".into() => "hello".into(),
|
||||
"field1".into() => "a b c".into(),
|
||||
)
|
||||
);
|
||||
|
||||
// Escaping
|
||||
query.parse(r#"hello\ world"#);
|
||||
assert_eq!(
|
||||
query,
|
||||
hashmap!(
|
||||
"primary".into() => r#"hello\ world"#.into(),
|
||||
)
|
||||
);
|
||||
query.parse(r#"hello \%field1 world"#);
|
||||
assert_eq!(
|
||||
query,
|
||||
hashmap!(
|
||||
"primary".into() => "hello %field1 world".into(),
|
||||
)
|
||||
);
|
||||
query.parse(r#"%field1 hello\ world"#);
|
||||
assert_eq!(
|
||||
query,
|
||||
hashmap!(
|
||||
"field1".into() => r#"hello\ world"#.into(),
|
||||
)
|
||||
);
|
||||
query.parse(r#"hello %field1 a\"b"#);
|
||||
assert_eq!(
|
||||
query,
|
||||
hashmap!(
|
||||
"primary".into() => "hello".into(),
|
||||
"field1".into() => r#"a\"b"#.into(),
|
||||
)
|
||||
);
|
||||
query.parse(r#"%field1 hello\ world"#);
|
||||
assert_eq!(
|
||||
query,
|
||||
hashmap!(
|
||||
"field1".into() => r#"hello\ world"#.into(),
|
||||
)
|
||||
);
|
||||
query.parse(r#"\bfoo\b"#);
|
||||
assert_eq!(
|
||||
query,
|
||||
hashmap!(
|
||||
"primary".into() => r#"\bfoo\b"#.into(),
|
||||
)
|
||||
);
|
||||
query.parse(r#"\\n"#);
|
||||
assert_eq!(
|
||||
query,
|
||||
hashmap!(
|
||||
"primary".into() => r#"\\n"#.into(),
|
||||
)
|
||||
);
|
||||
|
||||
// Only the prefix of a field is required.
|
||||
query.parse("hello %anot abc");
|
||||
assert_eq!(
|
||||
query,
|
||||
hashmap!(
|
||||
"primary".into() => "hello".into(),
|
||||
"another".into() => "abc".into(),
|
||||
)
|
||||
);
|
||||
// The shortest matching the prefix is selected.
|
||||
query.parse("hello %ano abc");
|
||||
assert_eq!(
|
||||
query,
|
||||
hashmap!(
|
||||
"primary".into() => "hello".into(),
|
||||
"anode".into() => "abc".into()
|
||||
)
|
||||
);
|
||||
// Multiple uses of a column are concatenated with space separators.
|
||||
query.parse("hello %field1 xyz %fie abc");
|
||||
assert_eq!(
|
||||
query,
|
||||
hashmap!(
|
||||
"primary".into() => "hello".into(),
|
||||
"field1".into() => "xyz abc".into()
|
||||
)
|
||||
);
|
||||
query.parse("hello %fie abc");
|
||||
assert_eq!(
|
||||
query,
|
||||
hashmap!(
|
||||
"primary".into() => "hello".into(),
|
||||
"field1".into() => "abc".into()
|
||||
)
|
||||
);
|
||||
// The primary column can be explicitly qualified.
|
||||
query.parse("hello %fie abc %prim world");
|
||||
assert_eq!(
|
||||
query,
|
||||
hashmap!(
|
||||
"primary".into() => "hello world".into(),
|
||||
"field1".into() => "abc".into()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn active_column_test() {
|
||||
fn active_column<'a>(query: &'a mut PickerQuery, input: &str) -> Option<&'a str> {
|
||||
let cursor = input.find('|').expect("cursor must be indicated with '|'");
|
||||
let input = input.replace('|', "");
|
||||
query.parse(&input);
|
||||
query.active_column(cursor).map(AsRef::as_ref)
|
||||
}
|
||||
|
||||
let mut query = PickerQuery::new(
|
||||
["primary".into(), "foo".into(), "bar".into()].into_iter(),
|
||||
0,
|
||||
);
|
||||
|
||||
assert_eq!(active_column(&mut query, "|"), Some("primary"));
|
||||
assert_eq!(active_column(&mut query, "hello| world"), Some("primary"));
|
||||
assert_eq!(active_column(&mut query, "|%foo hello"), Some("primary"));
|
||||
assert_eq!(active_column(&mut query, "%foo|"), Some("foo"));
|
||||
assert_eq!(active_column(&mut query, "%|"), None);
|
||||
assert_eq!(active_column(&mut query, "%baz|"), None);
|
||||
assert_eq!(active_column(&mut query, "%quiz%|"), None);
|
||||
assert_eq!(active_column(&mut query, "%foo hello| world"), Some("foo"));
|
||||
assert_eq!(active_column(&mut query, "%foo hello world|"), Some("foo"));
|
||||
assert_eq!(active_column(&mut query, "%foo| hello world"), Some("foo"));
|
||||
assert_eq!(active_column(&mut query, "%|foo hello world"), Some("foo"));
|
||||
assert_eq!(active_column(&mut query, "%f|oo hello world"), Some("foo"));
|
||||
assert_eq!(active_column(&mut query, "hello %f|oo world"), Some("foo"));
|
||||
assert_eq!(
|
||||
active_column(&mut query, "hello %f|oo world %bar !"),
|
||||
Some("foo")
|
||||
);
|
||||
assert_eq!(
|
||||
active_column(&mut query, "hello %foo wo|rld %bar !"),
|
||||
Some("foo")
|
||||
);
|
||||
assert_eq!(
|
||||
active_column(&mut query, "hello %foo world %bar !|"),
|
||||
Some("bar")
|
||||
);
|
||||
}
|
||||
}
|
|
@ -92,12 +92,22 @@ impl Prompt {
|
|||
}
|
||||
}
|
||||
|
||||
/// Gets the byte index in the input representing the current cursor location.
|
||||
#[inline]
|
||||
pub(crate) fn position(&self) -> usize {
|
||||
self.cursor
|
||||
}
|
||||
|
||||
pub fn with_line(mut self, line: String, editor: &Editor) -> Self {
|
||||
self.set_line(line, editor);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_line(&mut self, line: String, editor: &Editor) {
|
||||
let cursor = line.len();
|
||||
self.line = line;
|
||||
self.cursor = cursor;
|
||||
self.recalculate_completion(editor);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_language(
|
||||
|
@ -113,6 +123,19 @@ impl Prompt {
|
|||
&self.line
|
||||
}
|
||||
|
||||
pub fn with_history_register(&mut self, history_register: Option<char>) -> &mut Self {
|
||||
self.history_register = history_register;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn first_history_completion<'a>(
|
||||
&'a self,
|
||||
editor: &'a Editor,
|
||||
) -> Option<Cow<'a, str>> {
|
||||
self.history_register
|
||||
.and_then(|reg| editor.registers.first(reg, editor))
|
||||
}
|
||||
|
||||
pub fn recalculate_completion(&mut self, editor: &Editor) {
|
||||
self.exit_selection();
|
||||
self.completion = (self.completion_fn)(editor, &self.line);
|
||||
|
@ -476,10 +499,7 @@ impl Prompt {
|
|||
let line_area = area.clip_left(self.prompt.len() as u16).clip_top(line);
|
||||
if self.line.is_empty() {
|
||||
// Show the most recently entered value as a suggestion.
|
||||
if let Some(suggestion) = self
|
||||
.history_register
|
||||
.and_then(|reg| cx.editor.registers.first(reg, cx.editor))
|
||||
{
|
||||
if let Some(suggestion) = self.first_history_completion(cx.editor) {
|
||||
surface.set_string(line_area.x, line_area.y, suggestion, suggestion_color);
|
||||
}
|
||||
} else if let Some((language, loader)) = self.language.as_ref() {
|
||||
|
@ -574,8 +594,7 @@ impl Component for Prompt {
|
|||
self.recalculate_completion(cx.editor);
|
||||
} else {
|
||||
let last_item = self
|
||||
.history_register
|
||||
.and_then(|reg| cx.editor.registers.first(reg, cx.editor))
|
||||
.first_history_completion(cx.editor)
|
||||
.map(|entry| entry.to_string())
|
||||
.unwrap_or_else(|| String::from(""));
|
||||
|
||||
|
|
|
@ -1741,6 +1741,10 @@ impl Document {
|
|||
Url::from_file_path(self.path()?).ok()
|
||||
}
|
||||
|
||||
pub fn uri(&self) -> Option<helix_core::Uri> {
|
||||
Some(self.path()?.clone().into())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn text(&self) -> &Rope {
|
||||
&self.text
|
||||
|
|
|
@ -44,7 +44,7 @@ pub use helix_core::diagnostic::Severity;
|
|||
use helix_core::{
|
||||
auto_pairs::AutoPairs,
|
||||
syntax::{self, AutoPairConfig, IndentationHeuristic, LanguageServerFeature, SoftWrap},
|
||||
Change, LineEnding, Position, Range, Selection, NATIVE_LINE_ENDING,
|
||||
Change, LineEnding, Position, Range, Selection, Uri, NATIVE_LINE_ENDING,
|
||||
};
|
||||
use helix_dap as dap;
|
||||
use helix_lsp::lsp;
|
||||
|
@ -1022,7 +1022,7 @@ pub struct Editor {
|
|||
pub macro_recording: Option<(char, Vec<KeyEvent>)>,
|
||||
pub macro_replaying: Vec<char>,
|
||||
pub language_servers: helix_lsp::Registry,
|
||||
pub diagnostics: BTreeMap<PathBuf, Vec<(lsp::Diagnostic, LanguageServerId)>>,
|
||||
pub diagnostics: BTreeMap<Uri, Vec<(lsp::Diagnostic, LanguageServerId)>>,
|
||||
pub diff_providers: DiffProviderRegistry,
|
||||
|
||||
pub debugger: Option<dap::Client>,
|
||||
|
@ -1931,7 +1931,7 @@ impl Editor {
|
|||
/// Returns all supported diagnostics for the document
|
||||
pub fn doc_diagnostics<'a>(
|
||||
language_servers: &'a helix_lsp::Registry,
|
||||
diagnostics: &'a BTreeMap<PathBuf, Vec<(lsp::Diagnostic, LanguageServerId)>>,
|
||||
diagnostics: &'a BTreeMap<Uri, Vec<(lsp::Diagnostic, LanguageServerId)>>,
|
||||
document: &Document,
|
||||
) -> impl Iterator<Item = helix_core::Diagnostic> + 'a {
|
||||
Editor::doc_diagnostics_with_filter(language_servers, diagnostics, document, |_, _| true)
|
||||
|
@ -1941,15 +1941,15 @@ impl Editor {
|
|||
/// filtered by `filter` which is invocated with the raw `lsp::Diagnostic` and the language server id it came from
|
||||
pub fn doc_diagnostics_with_filter<'a>(
|
||||
language_servers: &'a helix_lsp::Registry,
|
||||
diagnostics: &'a BTreeMap<PathBuf, Vec<(lsp::Diagnostic, LanguageServerId)>>,
|
||||
diagnostics: &'a BTreeMap<Uri, Vec<(lsp::Diagnostic, LanguageServerId)>>,
|
||||
document: &Document,
|
||||
filter: impl Fn(&lsp::Diagnostic, LanguageServerId) -> bool + 'a,
|
||||
) -> impl Iterator<Item = helix_core::Diagnostic> + 'a {
|
||||
let text = document.text().clone();
|
||||
let language_config = document.language.clone();
|
||||
document
|
||||
.path()
|
||||
.and_then(|path| diagnostics.get(path))
|
||||
.uri()
|
||||
.and_then(|uri| diagnostics.get(&uri))
|
||||
.map(|diags| {
|
||||
diags.iter().filter_map(move |(diagnostic, lsp_id)| {
|
||||
let ls = language_servers.get_by_id(*lsp_id)?;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crate::editor::Action;
|
||||
use crate::Editor;
|
||||
use crate::{DocumentId, ViewId};
|
||||
use helix_core::Uri;
|
||||
use helix_lsp::util::generate_transaction_from_edits;
|
||||
use helix_lsp::{lsp, OffsetEncoding};
|
||||
|
||||
|
@ -54,18 +55,30 @@ pub struct ApplyEditError {
|
|||
pub enum ApplyEditErrorKind {
|
||||
DocumentChanged,
|
||||
FileNotFound,
|
||||
UnknownURISchema,
|
||||
InvalidUrl(helix_core::uri::UrlConversionError),
|
||||
IoError(std::io::Error),
|
||||
// TODO: check edits before applying and propagate failure
|
||||
// InvalidEdit,
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for ApplyEditErrorKind {
|
||||
fn from(err: std::io::Error) -> Self {
|
||||
ApplyEditErrorKind::IoError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<helix_core::uri::UrlConversionError> for ApplyEditErrorKind {
|
||||
fn from(err: helix_core::uri::UrlConversionError) -> Self {
|
||||
ApplyEditErrorKind::InvalidUrl(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for ApplyEditErrorKind {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
ApplyEditErrorKind::DocumentChanged => "document has changed".to_string(),
|
||||
ApplyEditErrorKind::FileNotFound => "file not found".to_string(),
|
||||
ApplyEditErrorKind::UnknownURISchema => "URI schema not supported".to_string(),
|
||||
ApplyEditErrorKind::InvalidUrl(err) => err.to_string(),
|
||||
ApplyEditErrorKind::IoError(err) => err.to_string(),
|
||||
}
|
||||
}
|
||||
|
@ -74,25 +87,28 @@ impl ToString for ApplyEditErrorKind {
|
|||
impl Editor {
|
||||
fn apply_text_edits(
|
||||
&mut self,
|
||||
uri: &helix_lsp::Url,
|
||||
url: &helix_lsp::Url,
|
||||
version: Option<i32>,
|
||||
text_edits: Vec<lsp::TextEdit>,
|
||||
offset_encoding: OffsetEncoding,
|
||||
) -> Result<(), ApplyEditErrorKind> {
|
||||
let path = match uri.to_file_path() {
|
||||
Ok(path) => path,
|
||||
Err(_) => {
|
||||
let err = format!("unable to convert URI to filepath: {}", uri);
|
||||
log::error!("{}", err);
|
||||
self.set_error(err);
|
||||
return Err(ApplyEditErrorKind::UnknownURISchema);
|
||||
let uri = match Uri::try_from(url) {
|
||||
Ok(uri) => uri,
|
||||
Err(err) => {
|
||||
log::error!("{err}");
|
||||
return Err(err.into());
|
||||
}
|
||||
};
|
||||
let path = uri.as_path().expect("URIs are valid paths");
|
||||
|
||||
let doc_id = match self.open(&path, Action::Load) {
|
||||
let doc_id = match self.open(path, Action::Load) {
|
||||
Ok(doc_id) => doc_id,
|
||||
Err(err) => {
|
||||
let err = format!("failed to open document: {}: {}", uri, err);
|
||||
let err = format!(
|
||||
"failed to open document: {}: {}",
|
||||
path.to_string_lossy(),
|
||||
err
|
||||
);
|
||||
log::error!("{}", err);
|
||||
self.set_error(err);
|
||||
return Err(ApplyEditErrorKind::FileNotFound);
|
||||
|
@ -158,9 +174,9 @@ impl Editor {
|
|||
for (i, operation) in operations.iter().enumerate() {
|
||||
match operation {
|
||||
lsp::DocumentChangeOperation::Op(op) => {
|
||||
self.apply_document_resource_op(op).map_err(|io| {
|
||||
self.apply_document_resource_op(op).map_err(|err| {
|
||||
ApplyEditError {
|
||||
kind: ApplyEditErrorKind::IoError(io),
|
||||
kind: err,
|
||||
failed_change_idx: i,
|
||||
}
|
||||
})?;
|
||||
|
@ -214,12 +230,18 @@ impl Editor {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_document_resource_op(&mut self, op: &lsp::ResourceOp) -> std::io::Result<()> {
|
||||
fn apply_document_resource_op(
|
||||
&mut self,
|
||||
op: &lsp::ResourceOp,
|
||||
) -> Result<(), ApplyEditErrorKind> {
|
||||
use lsp::ResourceOp;
|
||||
use std::fs;
|
||||
// NOTE: If `Uri` gets another variant than `Path`, the below `expect`s
|
||||
// may no longer be valid.
|
||||
match op {
|
||||
ResourceOp::Create(op) => {
|
||||
let path = op.uri.to_file_path().unwrap();
|
||||
let uri = Uri::try_from(&op.uri)?;
|
||||
let path = uri.as_path_buf().expect("URIs are valid paths");
|
||||
let ignore_if_exists = op.options.as_ref().map_or(false, |options| {
|
||||
!options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
|
||||
});
|
||||
|
@ -236,7 +258,8 @@ impl Editor {
|
|||
}
|
||||
}
|
||||
ResourceOp::Delete(op) => {
|
||||
let path = op.uri.to_file_path().unwrap();
|
||||
let uri = Uri::try_from(&op.uri)?;
|
||||
let path = uri.as_path_buf().expect("URIs are valid paths");
|
||||
if path.is_dir() {
|
||||
let recursive = op
|
||||
.options
|
||||
|
@ -251,17 +274,19 @@ impl Editor {
|
|||
}
|
||||
self.language_servers.file_event_handler.file_changed(path);
|
||||
} else if path.is_file() {
|
||||
fs::remove_file(&path)?;
|
||||
fs::remove_file(path)?;
|
||||
}
|
||||
}
|
||||
ResourceOp::Rename(op) => {
|
||||
let from = op.old_uri.to_file_path().unwrap();
|
||||
let to = op.new_uri.to_file_path().unwrap();
|
||||
let from_uri = Uri::try_from(&op.old_uri)?;
|
||||
let from = from_uri.as_path().expect("URIs are valid paths");
|
||||
let to_uri = Uri::try_from(&op.new_uri)?;
|
||||
let to = to_uri.as_path().expect("URIs are valid paths");
|
||||
let ignore_if_exists = op.options.as_ref().map_or(false, |options| {
|
||||
!options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
|
||||
});
|
||||
if !ignore_if_exists || !to.exists() {
|
||||
self.move_path(&from, &to)?;
|
||||
self.move_path(from, to)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue