mirror of
https://github.com/helix-editor/helix.git
synced 2024-11-24 18:36:18 +04:00
commit
08ee8b9443
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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
|
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cov-mark"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9ffa3d3e0138386cd4361f63537765cac7ee40698028844635a54495a92f67f3"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crc32fast"
|
name = "crc32fast"
|
||||||
version = "1.3.2"
|
version = "1.3.2"
|
||||||
@ -1337,6 +1331,7 @@ dependencies = [
|
|||||||
"unicode-general-category",
|
"unicode-general-category",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"unicode-width",
|
"unicode-width",
|
||||||
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1467,6 +1462,7 @@ dependencies = [
|
|||||||
"smallvec",
|
"smallvec",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"termini",
|
"termini",
|
||||||
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"toml",
|
"toml",
|
||||||
@ -1784,9 +1780,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nucleo"
|
name = "nucleo"
|
||||||
version = "0.2.1"
|
version = "0.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ae5331f4bcce475cf28cb29c95366c3091af4b0aa7703f1a6bc858f29718fdf3"
|
checksum = "5262af4c94921c2646c5ac6ff7900c2af9cbb08dc26a797e18130a7019c039d4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nucleo-matcher",
|
"nucleo-matcher",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
@ -1795,11 +1791,10 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nucleo-matcher"
|
name = "nucleo-matcher"
|
||||||
version = "0.2.0"
|
version = "0.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1b702b402fe286162d1f00b552a046ce74365d2ac473a2607ff36ba650f9bd57"
|
checksum = "bf33f538733d1a5a3494b836ba913207f14d9d4a1d3cd67030c5061bdd2cac85"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cov-mark",
|
|
||||||
"memchr",
|
"memchr",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
@ -38,7 +38,7 @@ package.helix-term.opt-level = 2
|
|||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
tree-sitter = { version = "0.22" }
|
tree-sitter = { version = "0.22" }
|
||||||
nucleo = "0.2.0"
|
nucleo = "0.5.0"
|
||||||
slotmap = "1.0.7"
|
slotmap = "1.0.7"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
|
|
||||||
|
@ -297,6 +297,8 @@ #### Interface
|
|||||||
| `ui.bufferline.background` | Style for bufferline background |
|
| `ui.bufferline.background` | Style for bufferline background |
|
||||||
| `ui.popup` | Documentation popups (e.g. Space + k) |
|
| `ui.popup` | Documentation popups (e.g. Space + k) |
|
||||||
| `ui.popup.info` | Prompt for multiple key options |
|
| `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.window` | Borderlines separating splits |
|
||||||
| `ui.help` | Description box for commands |
|
| `ui.help` | Description box for commands |
|
||||||
| `ui.text` | Default text style, command prompts, popup text, etc. |
|
| `ui.text` | Default text style, command prompts, popup text, etc. |
|
||||||
|
@ -34,6 +34,7 @@ bitflags = "2.6"
|
|||||||
ahash = "0.8.11"
|
ahash = "0.8.11"
|
||||||
hashbrown = { version = "0.14.5", features = ["raw"] }
|
hashbrown = { version = "0.14.5", features = ["raw"] }
|
||||||
dunce = "1.0"
|
dunce = "1.0"
|
||||||
|
url = "2.5.0"
|
||||||
|
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use std::ops::DerefMut;
|
use std::ops::DerefMut;
|
||||||
|
|
||||||
use nucleo::pattern::{Atom, AtomKind, CaseMatching};
|
use nucleo::pattern::{Atom, AtomKind, CaseMatching, Normalization};
|
||||||
use nucleo::Config;
|
use nucleo::Config;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
|
||||||
@ -38,6 +38,12 @@ pub fn fuzzy_match<T: AsRef<str>>(
|
|||||||
if path {
|
if path {
|
||||||
matcher.config.set_match_paths();
|
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)
|
pattern.match_list(items, &mut matcher)
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,7 @@
|
|||||||
pub mod text_annotations;
|
pub mod text_annotations;
|
||||||
pub mod textobject;
|
pub mod textobject;
|
||||||
mod transaction;
|
mod transaction;
|
||||||
|
pub mod uri;
|
||||||
pub mod wrap;
|
pub mod wrap;
|
||||||
|
|
||||||
pub mod unicode {
|
pub mod unicode {
|
||||||
@ -66,3 +67,5 @@ pub mod unicode {
|
|||||||
|
|
||||||
pub use line_ending::{LineEnding, NATIVE_LINE_ENDING};
|
pub use line_ending::{LineEnding, NATIVE_LINE_ENDING};
|
||||||
pub use transaction::{Assoc, Change, ChangeSet, Deletion, Operation, Transaction};
|
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;
|
use anyhow::Result;
|
||||||
pub use cancel::{cancelable_future, cancelation, CancelRx, CancelTx};
|
pub use cancel::{cancelable_future, cancelation, CancelRx, CancelTx};
|
||||||
pub use debounce::{send_blocking, AsyncHook};
|
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;
|
pub use registry::Event;
|
||||||
|
|
||||||
mod cancel;
|
mod cancel;
|
||||||
|
@ -51,3 +51,12 @@ pub fn start_frame() {
|
|||||||
pub fn lock_frame() -> RenderLockGuard {
|
pub fn lock_frame() -> RenderLockGuard {
|
||||||
RENDER_LOCK.read()
|
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 }
|
pulldown-cmark = { version = "0.11", default-features = false }
|
||||||
# file type detection
|
# file type detection
|
||||||
content_inspector = "0.2.4"
|
content_inspector = "0.2.4"
|
||||||
|
thiserror = "1.0"
|
||||||
|
|
||||||
# opening URLs
|
# opening URLs
|
||||||
open = "5.2.0"
|
open = "5.2.0"
|
||||||
|
@ -735,10 +735,10 @@ macro_rules! language_server {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Notification::PublishDiagnostics(mut params) => {
|
Notification::PublishDiagnostics(mut params) => {
|
||||||
let path = match params.uri.to_file_path() {
|
let uri = match helix_core::Uri::try_from(params.uri) {
|
||||||
Ok(path) => helix_stdx::path::normalize(path),
|
Ok(uri) => uri,
|
||||||
Err(_) => {
|
Err(err) => {
|
||||||
log::error!("Unsupported file URI: {}", params.uri);
|
log::error!("{err}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -749,11 +749,11 @@ macro_rules! language_server {
|
|||||||
}
|
}
|
||||||
// have to inline the function because of borrow checking...
|
// have to inline the function because of borrow checking...
|
||||||
let doc = self.editor.documents.values_mut()
|
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| {
|
.filter(|doc| {
|
||||||
if let Some(version) = params.version {
|
if let Some(version) = params.version {
|
||||||
if version != doc.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;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -765,7 +765,7 @@ macro_rules! language_server {
|
|||||||
let lang_conf = doc.language.clone();
|
let lang_conf = doc.language.clone();
|
||||||
|
|
||||||
if let Some(lang_conf) = &lang_conf {
|
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() {
|
if !lang_conf.persistent_diagnostic_sources.is_empty() {
|
||||||
// Sort diagnostics first by severity and then by line numbers.
|
// Sort diagnostics first by severity and then by line numbers.
|
||||||
// Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order
|
// Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order
|
||||||
@ -798,7 +798,7 @@ macro_rules! language_server {
|
|||||||
// Insert the original lsp::Diagnostics here because we may have no open document
|
// 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.
|
// 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.
|
// 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) => {
|
Entry::Occupied(o) => {
|
||||||
let current_diagnostics = o.into_mut();
|
let current_diagnostics = o.into_mut();
|
||||||
// there may entries of other language servers, which is why we can't overwrite the whole entry
|
// there may entries of other language servers, which is why we can't overwrite the whole entry
|
||||||
@ -1132,20 +1132,22 @@ fn handle_show_document(
|
|||||||
..
|
..
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
let path = match uri.to_file_path() {
|
let uri = match helix_core::Uri::try_from(uri) {
|
||||||
Ok(path) => path,
|
Ok(uri) => uri,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!("unsupported file URI: {}: {:?}", uri, err);
|
log::error!("{err}");
|
||||||
return lsp::ShowDocumentResult { success: false };
|
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 {
|
let action = match take_focus {
|
||||||
Some(true) => helix_view::editor::Action::Replace,
|
Some(true) => helix_view::editor::Action::Replace,
|
||||||
_ => helix_view::editor::Action::VerticalSplit,
|
_ => 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,
|
Ok(id) => id,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::error!("failed to open path: {:?}: {:?}", uri, err);
|
log::error!("failed to open path: {:?}: {:?}", uri, err);
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
pub(crate) mod typed;
|
pub(crate) mod typed;
|
||||||
|
|
||||||
pub use dap::*;
|
pub use dap::*;
|
||||||
|
use futures_util::FutureExt;
|
||||||
use helix_event::status;
|
use helix_event::status;
|
||||||
use helix_stdx::{
|
use helix_stdx::{
|
||||||
path::expand_tilde,
|
path::expand_tilde,
|
||||||
@ -10,10 +11,7 @@
|
|||||||
};
|
};
|
||||||
use helix_vcs::{FileChange, Hunk};
|
use helix_vcs::{FileChange, Hunk};
|
||||||
pub use lsp::*;
|
pub use lsp::*;
|
||||||
use tui::{
|
use tui::text::Span;
|
||||||
text::Span,
|
|
||||||
widgets::{Cell, Row},
|
|
||||||
};
|
|
||||||
pub use typed::*;
|
pub use typed::*;
|
||||||
|
|
||||||
use helix_core::{
|
use helix_core::{
|
||||||
@ -61,8 +59,7 @@
|
|||||||
compositor::{self, Component, Compositor},
|
compositor::{self, Component, Compositor},
|
||||||
filter_picker_entry,
|
filter_picker_entry,
|
||||||
job::Callback,
|
job::Callback,
|
||||||
keymap::ReverseKeymap,
|
ui::{self, overlay::overlaid, Picker, PickerColumn, Popup, Prompt, PromptEvent},
|
||||||
ui::{self, menu::Item, overlay::overlaid, Picker, Popup, Prompt, PromptEvent},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::job::{self, Jobs};
|
use crate::job::{self, Jobs};
|
||||||
@ -2257,216 +2254,193 @@ fn new(path: &Path, line_num: usize) -> Self {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ui::menu::Item for FileResult {
|
struct GlobalSearchConfig {
|
||||||
type Data = Option<PathBuf>;
|
smart_case: bool,
|
||||||
|
file_picker_config: helix_view::editor::FilePickerConfig,
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = cx.editor.config();
|
let config = cx.editor.config();
|
||||||
let smart_case = config.search.smart_case;
|
let config = GlobalSearchConfig {
|
||||||
let file_picker_config = config.file_picker.clone();
|
smart_case: config.search.smart_case,
|
||||||
|
file_picker_config: config.file_picker.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
let reg = cx.register.unwrap_or('/');
|
let columns = [
|
||||||
let completions = search_completions(cx, Some(reg));
|
PickerColumn::new("path", |item: &FileResult, _| {
|
||||||
ui::raw_regex_prompt(
|
let path = helix_stdx::path::get_relative_path(&item.path);
|
||||||
cx,
|
format!("{}:{}", path.to_string_lossy(), item.line_num + 1).into()
|
||||||
"global-search:".into(),
|
}),
|
||||||
Some(reg),
|
PickerColumn::hidden("contents"),
|
||||||
move |_editor: &Editor, input: &str| {
|
];
|
||||||
completions
|
|
||||||
.iter()
|
let get_files = |query: &str,
|
||||||
.filter(|comp| comp.starts_with(input))
|
editor: &mut Editor,
|
||||||
.map(|comp| (0.., std::borrow::Cow::Owned(comp.clone())))
|
config: std::sync::Arc<GlobalSearchConfig>,
|
||||||
.collect()
|
injector: &ui::picker::Injector<_, _>| {
|
||||||
},
|
if query.is_empty() {
|
||||||
move |cx, _, input, event| {
|
return async { Ok(()) }.boxed();
|
||||||
if event != PromptEvent::Validate {
|
}
|
||||||
return;
|
|
||||||
|
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 dedup_symlinks = config.file_picker_config.deduplicate_links;
|
||||||
let documents: Vec<_> = cx
|
let absolute_root = search_root
|
||||||
.editor
|
.canonicalize()
|
||||||
.documents()
|
.unwrap_or_else(|_| search_root.clone());
|
||||||
.map(|doc| (doc.path().cloned(), doc.text().to_owned()))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if let Ok(matcher) = RegexMatcherBuilder::new()
|
let injector = injector.clone();
|
||||||
.case_smart(smart_case)
|
async move {
|
||||||
.build(input)
|
let searcher = SearcherBuilder::new()
|
||||||
{
|
.binary_detection(BinaryDetection::quit(b'\x00'))
|
||||||
let search_root = helix_stdx::env::current_working_dir();
|
.build();
|
||||||
if !search_root.exists() {
|
WalkBuilder::new(search_root)
|
||||||
cx.editor
|
.hidden(config.file_picker_config.hidden)
|
||||||
.set_error("Current working directory does not exist");
|
.parents(config.file_picker_config.parents)
|
||||||
return;
|
.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 mut stop = false;
|
||||||
let absolute_root = search_root
|
let sink = sinks::UTF8(|line_num, _line_content| {
|
||||||
.canonicalize()
|
stop = injector
|
||||||
.unwrap_or_else(|_| search_root.clone());
|
.push(FileResult::new(entry.path(), line_num as usize - 1))
|
||||||
let injector_ = injector.clone();
|
.is_err();
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
Ok(!stop)
|
||||||
let searcher = SearcherBuilder::new()
|
});
|
||||||
.binary_detection(BinaryDetection::quit(b'\x00'))
|
let doc = documents.iter().find(|&(doc_path, _)| {
|
||||||
.build();
|
doc_path
|
||||||
|
.as_ref()
|
||||||
let mut walk_builder = WalkBuilder::new(search_root);
|
.map_or(false, |doc_path| doc_path == entry.path())
|
||||||
|
|
||||||
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)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
walk_builder
|
let result = if let Some((_, doc)) = doc {
|
||||||
.add_custom_ignore_filename(helix_loader::config_dir().join("ignore"));
|
// there is already a buffer for this file
|
||||||
walk_builder.add_custom_ignore_filename(".helix/ignore");
|
// search the buffer instead of the file because it's faster
|
||||||
|
// and captures new edits without requiring a save
|
||||||
walk_builder.build_parallel().run(|| {
|
if searcher.multi_line_with_matcher(&matcher) {
|
||||||
let mut searcher = searcher.clone();
|
// in this case a continous buffer is required
|
||||||
let matcher = matcher.clone();
|
// convert the rope to a string
|
||||||
let injector = injector_.clone();
|
let text = doc.to_string();
|
||||||
let documents = &documents;
|
searcher.search_slice(&matcher, text.as_bytes(), sink)
|
||||||
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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
} 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 {
|
if let Err(err) = result {
|
||||||
log::error!(
|
log::error!("Global search error: {}, {}", entry.path().display(), err);
|
||||||
"Global search error: {}, {}",
|
}
|
||||||
entry.path().display(),
|
if stop {
|
||||||
err
|
WalkState::Quit
|
||||||
);
|
} else {
|
||||||
}
|
WalkState::Continue
|
||||||
if stop {
|
}
|
||||||
WalkState::Quit
|
})
|
||||||
} else {
|
|
||||||
WalkState::Continue
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
.boxed()
|
||||||
|
};
|
||||||
|
|
||||||
cx.jobs.callback(async move {
|
let reg = cx.register.unwrap_or('/');
|
||||||
let call = move |_: &mut Editor, compositor: &mut Compositor| {
|
cx.editor.registers.last_search_register = reg;
|
||||||
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 line_num = *line_num;
|
let picker = Picker::new(
|
||||||
let view = view_mut!(cx.editor);
|
columns,
|
||||||
let text = doc.text();
|
1, // contents
|
||||||
if line_num >= text.len_lines() {
|
[],
|
||||||
cx.editor.set_error(
|
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.",
|
"The line you jumped to does not exist anymore because the file has changed.",
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let start = text.line_to_char(line_num);
|
let start = text.line_to_char(line_num);
|
||||||
let end = text.line_to_char((line_num + 1).min(text.len_lines()));
|
let end = text.line_to_char((line_num + 1).min(text.len_lines()));
|
||||||
|
|
||||||
doc.set_selection(view.id, Selection::single(start, end));
|
doc.set_selection(view.id, Selection::single(start, end));
|
||||||
if action.align_view(view, doc.id()) {
|
if action.align_view(view, doc.id()) {
|
||||||
align_view(doc, view, Align::Center);
|
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")
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
)
|
||||||
|
.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 {
|
enum Extend {
|
||||||
@ -2894,31 +2868,6 @@ struct BufferMeta {
|
|||||||
focused_at: std::time::Instant,
|
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 {
|
let new_meta = |doc: &Document| BufferMeta {
|
||||||
id: doc.id(),
|
id: doc.id(),
|
||||||
path: doc.path().cloned(),
|
path: doc.path().cloned(),
|
||||||
@ -2937,7 +2886,31 @@ fn format(&self, _data: &Self::Data) -> Row {
|
|||||||
// mru
|
// mru
|
||||||
items.sort_unstable_by_key(|item| std::cmp::Reverse(item.focused_at));
|
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);
|
cx.editor.switch(meta.id, action);
|
||||||
})
|
})
|
||||||
.with_preview(|editor, meta| {
|
.with_preview(|editor, meta| {
|
||||||
@ -2961,33 +2934,6 @@ struct JumpMeta {
|
|||||||
is_current: bool,
|
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 (view, _) in cx.editor.tree.views_mut() {
|
||||||
for doc_id in view.jumps.iter().map(|e| e.0).collect::<Vec<_>>().iter() {
|
for doc_id in view.jumps.iter().map(|e| e.0).collect::<Vec<_>>().iter() {
|
||||||
let doc = doc_mut!(cx.editor, doc_id);
|
let doc = doc_mut!(cx.editor, doc_id);
|
||||||
@ -3014,17 +2960,43 @@ fn format(&self, _data: &Self::Data) -> Row {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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(
|
let picker = Picker::new(
|
||||||
cx.editor
|
columns,
|
||||||
.tree
|
1, // path
|
||||||
.views()
|
cx.editor.tree.views().flat_map(|(view, _)| {
|
||||||
.flat_map(|(view, _)| {
|
view.jumps
|
||||||
view.jumps
|
.iter()
|
||||||
.iter()
|
.rev()
|
||||||
.rev()
|
.map(|(doc_id, selection)| new_meta(view, *doc_id, selection.clone()))
|
||||||
.map(|(doc_id, selection)| new_meta(view, *doc_id, selection.clone()))
|
}),
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
(),
|
(),
|
||||||
|cx, meta, action| {
|
|cx, meta, action| {
|
||||||
cx.editor.switch(meta.id, action);
|
cx.editor.switch(meta.id, action);
|
||||||
@ -3054,33 +3026,6 @@ pub struct FileChangeData {
|
|||||||
style_renamed: Style,
|
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();
|
let cwd = helix_stdx::env::current_working_dir();
|
||||||
if !cwd.exists() {
|
if !cwd.exists() {
|
||||||
cx.editor
|
cx.editor
|
||||||
@ -3094,8 +3039,41 @@ fn format(&self, data: &Self::Data) -> Row {
|
|||||||
let deleted = cx.editor.theme.get("diff.minus");
|
let deleted = cx.editor.theme.get("diff.minus");
|
||||||
let renamed = cx.editor.theme.get("diff.delta.moved");
|
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(
|
let picker = Picker::new(
|
||||||
Vec::new(),
|
columns,
|
||||||
|
1, // path
|
||||||
|
[],
|
||||||
FileChangeData {
|
FileChangeData {
|
||||||
cwd: cwd.clone(),
|
cwd: cwd.clone(),
|
||||||
style_untracked: added,
|
style_untracked: added,
|
||||||
@ -3116,7 +3094,7 @@ fn format(&self, data: &Self::Data) -> Row {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.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();
|
let injector = picker.injector();
|
||||||
|
|
||||||
cx.editor
|
cx.editor
|
||||||
@ -3132,35 +3110,6 @@ fn format(&self, data: &Self::Data) -> Row {
|
|||||||
cx.push_layer(Box::new(overlaid(picker)));
|
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) {
|
pub fn command_palette(cx: &mut Context) {
|
||||||
let register = cx.register;
|
let register = cx.register;
|
||||||
let count = cx.count;
|
let count = cx.count;
|
||||||
@ -3171,16 +3120,45 @@ pub fn command_palette(cx: &mut Context) {
|
|||||||
[&cx.editor.mode]
|
[&cx.editor.mode]
|
||||||
.reverse_map();
|
.reverse_map();
|
||||||
|
|
||||||
let mut commands: Vec<MappableCommand> = MappableCommand::STATIC_COMMAND_LIST.into();
|
let commands = MappableCommand::STATIC_COMMAND_LIST.iter().cloned().chain(
|
||||||
commands.extend(typed::TYPABLE_COMMAND_LIST.iter().map(|cmd| {
|
typed::TYPABLE_COMMAND_LIST
|
||||||
MappableCommand::Typable {
|
.iter()
|
||||||
name: cmd.name.to_owned(),
|
.map(|cmd| MappableCommand::Typable {
|
||||||
doc: cmd.doc.to_owned(),
|
name: cmd.name.to_owned(),
|
||||||
args: Vec::new(),
|
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 {
|
let mut ctx = Context {
|
||||||
register,
|
register,
|
||||||
count,
|
count,
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
use serde_json::{to_value, Value};
|
use serde_json::{to_value, Value};
|
||||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||||
use tui::{text::Spans, widgets::Row};
|
use tui::text::Spans;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
@ -22,38 +22,6 @@
|
|||||||
|
|
||||||
use helix_view::handlers::dap::{breakpoints_changed, jump_to_stack_frame, select_thread_id};
|
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(
|
fn thread_picker(
|
||||||
cx: &mut Context,
|
cx: &mut Context,
|
||||||
callback_fn: impl Fn(&mut Editor, &dap::Thread) + Send + 'static,
|
callback_fn: impl Fn(&mut Editor, &dap::Thread) + Send + 'static,
|
||||||
@ -73,13 +41,27 @@ fn thread_picker(
|
|||||||
let debugger = debugger!(editor);
|
let debugger = debugger!(editor);
|
||||||
|
|
||||||
let thread_states = debugger.thread_states.clone();
|
let thread_states = debugger.thread_states.clone();
|
||||||
let picker = Picker::new(threads, thread_states, move |cx, thread, _action| {
|
let columns = [
|
||||||
callback_fn(cx.editor, thread)
|
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| {
|
.with_preview(move |editor, thread| {
|
||||||
let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?;
|
let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?;
|
||||||
let frame = frames.first()?;
|
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((
|
let pos = Some((
|
||||||
frame.line.saturating_sub(1),
|
frame.line.saturating_sub(1),
|
||||||
frame.end_line.unwrap_or(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 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(
|
cx.push_layer(Box::new(overlaid(Picker::new(
|
||||||
|
columns,
|
||||||
|
0,
|
||||||
templates,
|
templates,
|
||||||
(),
|
(),
|
||||||
|cx, template, _action| {
|
|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 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);
|
let debugger = debugger!(cx.editor);
|
||||||
// TODO: this should be simpler to find
|
// TODO: this should be simpler to find
|
||||||
let pos = debugger.stack_frames[&thread_id]
|
let pos = debugger.stack_frames[&thread_id]
|
||||||
@ -755,10 +747,10 @@ pub fn dap_switch_stack_frame(cx: &mut Context) {
|
|||||||
frame
|
frame
|
||||||
.source
|
.source
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|source| source.path.clone())
|
.and_then(|source| source.path.as_ref())
|
||||||
.map(|path| {
|
.map(|path| {
|
||||||
(
|
(
|
||||||
path.into(),
|
path.as_path().into(),
|
||||||
Some((
|
Some((
|
||||||
frame.line.saturating_sub(1),
|
frame.line.saturating_sub(1),
|
||||||
frame.end_line.unwrap_or(frame.line).saturating_sub(1),
|
frame.end_line.unwrap_or(frame.line).saturating_sub(1),
|
||||||
|
@ -9,14 +9,13 @@
|
|||||||
Client, LanguageServerId, OffsetEncoding,
|
Client, LanguageServerId, OffsetEncoding,
|
||||||
};
|
};
|
||||||
use tokio_stream::StreamExt;
|
use tokio_stream::StreamExt;
|
||||||
use tui::{
|
use tui::{text::Span, widgets::Row};
|
||||||
text::{Span, Spans},
|
|
||||||
widgets::Row,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{align_view, push_jump, Align, Context, Editor};
|
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_stdx::path;
|
||||||
use helix_view::{
|
use helix_view::{
|
||||||
document::{DocumentInlayHints, DocumentInlayHintsId},
|
document::{DocumentInlayHints, DocumentInlayHintsId},
|
||||||
@ -29,7 +28,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
compositor::{self, Compositor},
|
compositor::{self, Compositor},
|
||||||
job::Callback,
|
job::Callback,
|
||||||
ui::{self, overlay::overlaid, DynamicPicker, FileLocation, Picker, Popup, PromptEvent},
|
ui::{self, overlay::overlaid, FileLocation, Picker, Popup, PromptEvent},
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
@ -37,7 +36,7 @@
|
|||||||
collections::{BTreeMap, HashSet},
|
collections::{BTreeMap, HashSet},
|
||||||
fmt::Write,
|
fmt::Write,
|
||||||
future::Future,
|
future::Future,
|
||||||
path::{Path, PathBuf},
|
path::Path,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Gets the first language server that is attached to a document which supports a specific feature.
|
/// 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 {
|
struct SymbolInformationItem {
|
||||||
symbol: lsp::SymbolInformation,
|
symbol: lsp::SymbolInformation,
|
||||||
offset_encoding: OffsetEncoding,
|
offset_encoding: OffsetEncoding,
|
||||||
}
|
uri: Uri,
|
||||||
|
|
||||||
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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DiagnosticStyles {
|
struct DiagnosticStyles {
|
||||||
@ -133,60 +75,15 @@ struct DiagnosticStyles {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct PickerDiagnostic {
|
struct PickerDiagnostic {
|
||||||
path: PathBuf,
|
uri: Uri,
|
||||||
diag: lsp::Diagnostic,
|
diag: lsp::Diagnostic,
|
||||||
offset_encoding: OffsetEncoding,
|
offset_encoding: OffsetEncoding,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ui::menu::Item for PickerDiagnostic {
|
fn uri_to_file_location<'a>(uri: &'a Uri, range: &lsp::Range) -> Option<FileLocation<'a>> {
|
||||||
type Data = (DiagnosticStyles, DiagnosticsFormat);
|
let path = uri.as_path()?;
|
||||||
|
let line = Some((range.start.line as usize, range.end.line as usize));
|
||||||
fn format(&self, (styles, format): &Self::Data) -> Row {
|
Some((path.into(), line))
|
||||||
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 jump_to_location(
|
fn jump_to_location(
|
||||||
@ -241,20 +138,39 @@ fn jump_to_position(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type SymbolPicker = Picker<SymbolInformationItem>;
|
fn display_symbol_kind(kind: lsp::SymbolKind) -> &'static str {
|
||||||
|
match kind {
|
||||||
fn sym_picker(symbols: Vec<SymbolInformationItem>, current_path: Option<lsp::Url>) -> SymbolPicker {
|
lsp::SymbolKind::FILE => "file",
|
||||||
// TODO: drop current_path comparison and instead use workspace: bool flag?
|
lsp::SymbolKind::MODULE => "module",
|
||||||
Picker::new(symbols, current_path, move |cx, item, action| {
|
lsp::SymbolKind::NAMESPACE => "namespace",
|
||||||
jump_to_location(
|
lsp::SymbolKind::PACKAGE => "package",
|
||||||
cx.editor,
|
lsp::SymbolKind::CLASS => "class",
|
||||||
&item.symbol.location,
|
lsp::SymbolKind::METHOD => "method",
|
||||||
item.offset_encoding,
|
lsp::SymbolKind::PROPERTY => "property",
|
||||||
action,
|
lsp::SymbolKind::FIELD => "field",
|
||||||
);
|
lsp::SymbolKind::CONSTRUCTOR => "construct",
|
||||||
})
|
lsp::SymbolKind::ENUM => "enum",
|
||||||
.with_preview(move |_editor, item| Some(location_to_file_location(&item.symbol.location)))
|
lsp::SymbolKind::INTERFACE => "interface",
|
||||||
.truncate_start(false)
|
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)]
|
#[derive(Copy, Clone, PartialEq)]
|
||||||
@ -263,22 +179,24 @@ enum DiagnosticsFormat {
|
|||||||
HideSourcePath,
|
HideSourcePath,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type DiagnosticsPicker = Picker<PickerDiagnostic, DiagnosticStyles>;
|
||||||
|
|
||||||
fn diag_picker(
|
fn diag_picker(
|
||||||
cx: &Context,
|
cx: &Context,
|
||||||
diagnostics: BTreeMap<PathBuf, Vec<(lsp::Diagnostic, LanguageServerId)>>,
|
diagnostics: BTreeMap<Uri, Vec<(lsp::Diagnostic, LanguageServerId)>>,
|
||||||
format: DiagnosticsFormat,
|
format: DiagnosticsFormat,
|
||||||
) -> Picker<PickerDiagnostic> {
|
) -> DiagnosticsPicker {
|
||||||
// TODO: drop current_path comparison and instead use workspace: bool flag?
|
// TODO: drop current_path comparison and instead use workspace: bool flag?
|
||||||
|
|
||||||
// flatten the map to a vec of (url, diag) pairs
|
// flatten the map to a vec of (url, diag) pairs
|
||||||
let mut flat_diag = Vec::new();
|
let mut flat_diag = Vec::new();
|
||||||
for (path, diags) in diagnostics {
|
for (uri, diags) in diagnostics {
|
||||||
flat_diag.reserve(diags.len());
|
flat_diag.reserve(diags.len());
|
||||||
|
|
||||||
for (diag, ls) in diags {
|
for (diag, ls) in diags {
|
||||||
if let Some(ls) = cx.editor.language_server_by_id(ls) {
|
if let Some(ls) = cx.editor.language_server_by_id(ls) {
|
||||||
flat_diag.push(PickerDiagnostic {
|
flat_diag.push(PickerDiagnostic {
|
||||||
path: path.clone(),
|
uri: uri.clone(),
|
||||||
diag,
|
diag,
|
||||||
offset_encoding: ls.offset_encoding(),
|
offset_encoding: ls.offset_encoding(),
|
||||||
});
|
});
|
||||||
@ -293,22 +211,72 @@ fn diag_picker(
|
|||||||
error: cx.editor.theme.get("error"),
|
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(
|
Picker::new(
|
||||||
|
columns,
|
||||||
|
primary_column,
|
||||||
flat_diag,
|
flat_diag,
|
||||||
(styles, format),
|
styles,
|
||||||
move |cx,
|
move |cx,
|
||||||
PickerDiagnostic {
|
PickerDiagnostic {
|
||||||
path,
|
uri,
|
||||||
diag,
|
diag,
|
||||||
offset_encoding,
|
offset_encoding,
|
||||||
},
|
},
|
||||||
action| {
|
action| {
|
||||||
|
let Some(path) = uri.as_path() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
jump_to_position(cx.editor, path, diag.range, *offset_encoding, action)
|
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));
|
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)
|
.truncate_start(false)
|
||||||
}
|
}
|
||||||
@ -317,6 +285,7 @@ pub fn symbol_picker(cx: &mut Context) {
|
|||||||
fn nested_to_flat(
|
fn nested_to_flat(
|
||||||
list: &mut Vec<SymbolInformationItem>,
|
list: &mut Vec<SymbolInformationItem>,
|
||||||
file: &lsp::TextDocumentIdentifier,
|
file: &lsp::TextDocumentIdentifier,
|
||||||
|
uri: &Uri,
|
||||||
symbol: lsp::DocumentSymbol,
|
symbol: lsp::DocumentSymbol,
|
||||||
offset_encoding: OffsetEncoding,
|
offset_encoding: OffsetEncoding,
|
||||||
) {
|
) {
|
||||||
@ -331,9 +300,10 @@ fn nested_to_flat(
|
|||||||
container_name: None,
|
container_name: None,
|
||||||
},
|
},
|
||||||
offset_encoding,
|
offset_encoding,
|
||||||
|
uri: uri.clone(),
|
||||||
});
|
});
|
||||||
for child in symbol.children.into_iter().flatten() {
|
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);
|
let doc = doc!(cx.editor);
|
||||||
@ -347,6 +317,9 @@ fn nested_to_flat(
|
|||||||
let request = language_server.document_symbols(doc.identifier()).unwrap();
|
let request = language_server.document_symbols(doc.identifier()).unwrap();
|
||||||
let offset_encoding = language_server.offset_encoding();
|
let offset_encoding = language_server.offset_encoding();
|
||||||
let doc_id = doc.identifier();
|
let doc_id = doc.identifier();
|
||||||
|
let doc_uri = doc
|
||||||
|
.uri()
|
||||||
|
.expect("docs with active language servers must be backed by paths");
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
let json = request.await?;
|
let json = request.await?;
|
||||||
@ -361,6 +334,7 @@ fn nested_to_flat(
|
|||||||
lsp::DocumentSymbolResponse::Flat(symbols) => symbols
|
lsp::DocumentSymbolResponse::Flat(symbols) => symbols
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|symbol| SymbolInformationItem {
|
.map(|symbol| SymbolInformationItem {
|
||||||
|
uri: doc_uri.clone(),
|
||||||
symbol,
|
symbol,
|
||||||
offset_encoding,
|
offset_encoding,
|
||||||
})
|
})
|
||||||
@ -368,7 +342,13 @@ fn nested_to_flat(
|
|||||||
lsp::DocumentSymbolResponse::Nested(symbols) => {
|
lsp::DocumentSymbolResponse::Nested(symbols) => {
|
||||||
let mut flat_symbols = Vec::new();
|
let mut flat_symbols = Vec::new();
|
||||||
for symbol in symbols {
|
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
|
flat_symbols
|
||||||
}
|
}
|
||||||
@ -377,7 +357,6 @@ fn nested_to_flat(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let current_url = doc.url();
|
|
||||||
|
|
||||||
if futures.is_empty() {
|
if futures.is_empty() {
|
||||||
cx.editor
|
cx.editor
|
||||||
@ -392,7 +371,37 @@ fn nested_to_flat(
|
|||||||
symbols.append(&mut lsp_items);
|
symbols.append(&mut lsp_items);
|
||||||
}
|
}
|
||||||
let call = move |_editor: &mut Editor, compositor: &mut Compositor| {
|
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)))
|
compositor.push(Box::new(overlaid(picker)))
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -401,6 +410,8 @@ fn nested_to_flat(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn workspace_symbol_picker(cx: &mut Context) {
|
pub fn workspace_symbol_picker(cx: &mut Context) {
|
||||||
|
use crate::ui::picker::Injector;
|
||||||
|
|
||||||
let doc = doc!(cx.editor);
|
let doc = doc!(cx.editor);
|
||||||
if doc
|
if doc
|
||||||
.language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols)
|
.language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols)
|
||||||
@ -412,25 +423,37 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
|
|||||||
return;
|
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 doc = doc!(editor);
|
||||||
let mut seen_language_servers = HashSet::new();
|
let mut seen_language_servers = HashSet::new();
|
||||||
let mut futures: FuturesOrdered<_> = doc
|
let mut futures: FuturesOrdered<_> = doc
|
||||||
.language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols)
|
.language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols)
|
||||||
.filter(|ls| seen_language_servers.insert(ls.id()))
|
.filter(|ls| seen_language_servers.insert(ls.id()))
|
||||||
.map(|language_server| {
|
.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();
|
let offset_encoding = language_server.offset_encoding();
|
||||||
async move {
|
async move {
|
||||||
let json = request.await?;
|
let json = request.await?;
|
||||||
|
|
||||||
let response =
|
let response: Vec<_> =
|
||||||
serde_json::from_value::<Option<Vec<lsp::SymbolInformation>>>(json)?
|
serde_json::from_value::<Option<Vec<lsp::SymbolInformation>>>(json)?
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|symbol| SymbolInformationItem {
|
.filter_map(|symbol| {
|
||||||
symbol,
|
let uri = match Uri::try_from(&symbol.location.uri) {
|
||||||
offset_encoding,
|
Ok(uri) => uri,
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!("discarding symbol with invalid URI: {err}");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Some(SymbolInformationItem {
|
||||||
|
symbol,
|
||||||
|
uri,
|
||||||
|
offset_encoding,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@ -443,44 +466,66 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
|
|||||||
editor.set_error("No configured language server supports workspace symbols");
|
editor.set_error("No configured language server supports workspace symbols");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let injector = injector.clone();
|
||||||
async move {
|
async move {
|
||||||
let mut symbols = Vec::new();
|
|
||||||
// TODO if one symbol request errors, all other requests are discarded (even if they're valid)
|
// 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? {
|
while let Some(lsp_items) = futures.try_next().await? {
|
||||||
symbols.append(&mut lsp_items);
|
for item in lsp_items {
|
||||||
|
injector.push(item)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
anyhow::Ok(symbols)
|
Ok(())
|
||||||
}
|
}
|
||||||
.boxed()
|
.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 picker = Picker::new(
|
||||||
let initial_symbols = get_symbols("".to_owned(), cx.editor);
|
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 {
|
cx.push_layer(Box::new(overlaid(picker)));
|
||||||
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)))
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn diagnostics_picker(cx: &mut Context) {
|
pub fn diagnostics_picker(cx: &mut Context) {
|
||||||
let doc = doc!(cx.editor);
|
let doc = doc!(cx.editor);
|
||||||
if let Some(current_path) = doc.path() {
|
if let Some(uri) = doc.uri() {
|
||||||
let diagnostics = cx
|
let diagnostics = cx.editor.diagnostics.get(&uri).cloned().unwrap_or_default();
|
||||||
.editor
|
|
||||||
.diagnostics
|
|
||||||
.get(current_path)
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_default();
|
|
||||||
let picker = diag_picker(
|
let picker = diag_picker(
|
||||||
cx,
|
cx,
|
||||||
[(current_path.clone(), diagnostics)].into(),
|
[(uri, diagnostics)].into(),
|
||||||
DiagnosticsFormat::HideSourcePath,
|
DiagnosticsFormat::HideSourcePath,
|
||||||
);
|
);
|
||||||
cx.push_layer(Box::new(overlaid(picker)));
|
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(
|
pub fn execute_lsp_command(
|
||||||
editor: &mut Editor,
|
editor: &mut Editor,
|
||||||
language_server_id: LanguageServerId,
|
language_server_id: LanguageServerId,
|
||||||
@ -817,10 +855,67 @@ fn goto_impl(
|
|||||||
}
|
}
|
||||||
[] => unreachable!("`locations` should be non-empty for `goto_impl`"),
|
[] => unreachable!("`locations` should be non-empty for `goto_impl`"),
|
||||||
_locations => {
|
_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)
|
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)));
|
compositor.push(Box::new(overlaid(picker)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,6 @@
|
|||||||
use helix_core::fuzzy::fuzzy_match;
|
use helix_core::fuzzy::fuzzy_match;
|
||||||
use helix_core::indent::MAX_INDENT;
|
use helix_core::indent::MAX_INDENT;
|
||||||
use helix_core::{line_ending, shellwords::Shellwords};
|
use helix_core::{line_ending, shellwords::Shellwords};
|
||||||
use helix_lsp::LanguageServerId;
|
|
||||||
use helix_view::document::{read_to_string, DEFAULT_LANGUAGE_NAME};
|
use helix_view::document::{read_to_string, DEFAULT_LANGUAGE_NAME};
|
||||||
use helix_view::editor::{CloseError, ConfigEvent};
|
use helix_view::editor::{CloseError, ConfigEvent};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
@ -1378,16 +1377,6 @@ fn lsp_workspace_command(
|
|||||||
return Ok(());
|
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 doc = doc!(cx.editor);
|
||||||
let ls_id_commands = doc
|
let ls_id_commands = doc
|
||||||
.language_servers_with_feature(LanguageServerFeature::WorkspaceCommand)
|
.language_servers_with_feature(LanguageServerFeature::WorkspaceCommand)
|
||||||
@ -1402,7 +1391,7 @@ fn format(&self, _data: &Self::Data) -> Row {
|
|||||||
if args.is_empty() {
|
if args.is_empty() {
|
||||||
let commands = ls_id_commands
|
let commands = ls_id_commands
|
||||||
.map(|(ls_id, command)| {
|
.map(|(ls_id, command)| {
|
||||||
LsIdCommand(
|
(
|
||||||
ls_id,
|
ls_id,
|
||||||
helix_lsp::lsp::Command {
|
helix_lsp::lsp::Command {
|
||||||
title: command.clone(),
|
title: command.clone(),
|
||||||
@ -1415,10 +1404,18 @@ fn format(&self, _data: &Self::Data) -> Row {
|
|||||||
let callback = async move {
|
let callback = async move {
|
||||||
let call: job::Callback = Callback::EditorCompositor(Box::new(
|
let call: job::Callback = Callback::EditorCompositor(Box::new(
|
||||||
move |_editor: &mut Editor, compositor: &mut Compositor| {
|
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(
|
let picker = ui::Picker::new(
|
||||||
|
columns,
|
||||||
|
0,
|
||||||
commands,
|
commands,
|
||||||
(),
|
(),
|
||||||
move |cx, LsIdCommand(ls_id, command), _action| {
|
move |cx, (ls_id, command), _action| {
|
||||||
execute_lsp_command(cx.editor, *ls_id, command.clone());
|
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::{
|
use crate::{
|
||||||
compositor::{Callback, Component, Compositor, Context, Event, EventResult},
|
compositor::{Callback, Component, Compositor, Context, Event, EventResult},
|
||||||
ctrl, key, shift,
|
ctrl, key, shift,
|
||||||
};
|
};
|
||||||
use helix_core::fuzzy::MATCHER;
|
use helix_core::fuzzy::MATCHER;
|
||||||
use nucleo::pattern::{Atom, AtomKind, CaseMatching};
|
use nucleo::pattern::{Atom, AtomKind, CaseMatching, Normalization};
|
||||||
use nucleo::{Config, Utf32Str};
|
use nucleo::{Config, Utf32Str};
|
||||||
use tui::{buffer::Buffer as Surface, widgets::Table};
|
use tui::{buffer::Buffer as Surface, widgets::Table};
|
||||||
|
|
||||||
@ -31,18 +31,6 @@ fn filter_text(&self, data: &Self::Data) -> Cow<str> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 type MenuCallback<T> = Box<dyn Fn(&mut Editor, Option<&T>, MenuEvent)>;
|
||||||
|
|
||||||
pub struct Menu<T: Item> {
|
pub struct Menu<T: Item> {
|
||||||
@ -92,7 +80,13 @@ pub fn new(
|
|||||||
pub fn score(&mut self, pattern: &str, incremental: bool) {
|
pub fn score(&mut self, pattern: &str, incremental: bool) {
|
||||||
let mut matcher = MATCHER.lock();
|
let mut matcher = MATCHER.lock();
|
||||||
matcher.config = Config::DEFAULT;
|
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();
|
let mut buf = Vec::new();
|
||||||
if incremental {
|
if incremental {
|
||||||
self.matches.retain_mut(|(index, score)| {
|
self.matches.retain_mut(|(index, score)| {
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
use helix_stdx::rope;
|
use helix_stdx::rope;
|
||||||
pub use markdown::Markdown;
|
pub use markdown::Markdown;
|
||||||
pub use menu::Menu;
|
pub use menu::Menu;
|
||||||
pub use picker::{DynamicPicker, FileLocation, Picker};
|
pub use picker::{Column as PickerColumn, FileLocation, Picker};
|
||||||
pub use popup::Popup;
|
pub use popup::Popup;
|
||||||
pub use prompt::{Prompt, PromptEvent};
|
pub use prompt::{Prompt, PromptEvent};
|
||||||
pub use spinner::{ProgressSpinners, Spinner};
|
pub use spinner::{ProgressSpinners, Spinner};
|
||||||
@ -170,7 +170,9 @@ pub fn raw_regex_prompt(
|
|||||||
cx.push_layer(Box::new(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 ignore::{types::TypesBuilder, WalkBuilder};
|
||||||
use std::time::Instant;
|
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));
|
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) {
|
if let Err(e) = cx.editor.open(path, action) {
|
||||||
let err = if let Some(err) = e.source() {
|
let err = if let Some(err) = e.source() {
|
||||||
format!("{}", err)
|
format!("{}", err)
|
||||||
@ -227,7 +238,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker
|
|||||||
cx.editor.set_error(err);
|
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 injector = picker.injector();
|
||||||
let timeout = std::time::Instant::now() + std::time::Duration::from_millis(30);
|
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 @@ pub fn new(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
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();
|
let cursor = line.len();
|
||||||
self.line = line;
|
self.line = line;
|
||||||
self.cursor = cursor;
|
self.cursor = cursor;
|
||||||
self.recalculate_completion(editor);
|
self.recalculate_completion(editor);
|
||||||
self
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_language(
|
pub fn with_language(
|
||||||
@ -113,6 +123,19 @@ pub fn line(&self) -> &String {
|
|||||||
&self.line
|
&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) {
|
pub fn recalculate_completion(&mut self, editor: &Editor) {
|
||||||
self.exit_selection();
|
self.exit_selection();
|
||||||
self.completion = (self.completion_fn)(editor, &self.line);
|
self.completion = (self.completion_fn)(editor, &self.line);
|
||||||
@ -476,10 +499,7 @@ pub fn render_prompt(&self, area: Rect, surface: &mut Surface, cx: &mut Context)
|
|||||||
let line_area = area.clip_left(self.prompt.len() as u16).clip_top(line);
|
let line_area = area.clip_left(self.prompt.len() as u16).clip_top(line);
|
||||||
if self.line.is_empty() {
|
if self.line.is_empty() {
|
||||||
// Show the most recently entered value as a suggestion.
|
// Show the most recently entered value as a suggestion.
|
||||||
if let Some(suggestion) = self
|
if let Some(suggestion) = self.first_history_completion(cx.editor) {
|
||||||
.history_register
|
|
||||||
.and_then(|reg| cx.editor.registers.first(reg, cx.editor))
|
|
||||||
{
|
|
||||||
surface.set_string(line_area.x, line_area.y, suggestion, suggestion_color);
|
surface.set_string(line_area.x, line_area.y, suggestion, suggestion_color);
|
||||||
}
|
}
|
||||||
} else if let Some((language, loader)) = self.language.as_ref() {
|
} else if let Some((language, loader)) = self.language.as_ref() {
|
||||||
@ -574,8 +594,7 @@ fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
|
|||||||
self.recalculate_completion(cx.editor);
|
self.recalculate_completion(cx.editor);
|
||||||
} else {
|
} else {
|
||||||
let last_item = self
|
let last_item = self
|
||||||
.history_register
|
.first_history_completion(cx.editor)
|
||||||
.and_then(|reg| cx.editor.registers.first(reg, cx.editor))
|
|
||||||
.map(|entry| entry.to_string())
|
.map(|entry| entry.to_string())
|
||||||
.unwrap_or_else(|| String::from(""));
|
.unwrap_or_else(|| String::from(""));
|
||||||
|
|
||||||
|
@ -1741,6 +1741,10 @@ pub fn url(&self) -> Option<Url> {
|
|||||||
Url::from_file_path(self.path()?).ok()
|
Url::from_file_path(self.path()?).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn uri(&self) -> Option<helix_core::Uri> {
|
||||||
|
Some(self.path()?.clone().into())
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn text(&self) -> &Rope {
|
pub fn text(&self) -> &Rope {
|
||||||
&self.text
|
&self.text
|
||||||
|
@ -44,7 +44,7 @@
|
|||||||
use helix_core::{
|
use helix_core::{
|
||||||
auto_pairs::AutoPairs,
|
auto_pairs::AutoPairs,
|
||||||
syntax::{self, AutoPairConfig, IndentationHeuristic, LanguageServerFeature, SoftWrap},
|
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_dap as dap;
|
||||||
use helix_lsp::lsp;
|
use helix_lsp::lsp;
|
||||||
@ -1022,7 +1022,7 @@ pub struct Editor {
|
|||||||
pub macro_recording: Option<(char, Vec<KeyEvent>)>,
|
pub macro_recording: Option<(char, Vec<KeyEvent>)>,
|
||||||
pub macro_replaying: Vec<char>,
|
pub macro_replaying: Vec<char>,
|
||||||
pub language_servers: helix_lsp::Registry,
|
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 diff_providers: DiffProviderRegistry,
|
||||||
|
|
||||||
pub debugger: Option<dap::Client>,
|
pub debugger: Option<dap::Client>,
|
||||||
@ -1931,7 +1931,7 @@ pub fn document_by_path_mut<P: AsRef<Path>>(&mut self, path: P) -> Option<&mut D
|
|||||||
/// Returns all supported diagnostics for the document
|
/// Returns all supported diagnostics for the document
|
||||||
pub fn doc_diagnostics<'a>(
|
pub fn doc_diagnostics<'a>(
|
||||||
language_servers: &'a helix_lsp::Registry,
|
language_servers: &'a helix_lsp::Registry,
|
||||||
diagnostics: &'a BTreeMap<PathBuf, Vec<(lsp::Diagnostic, LanguageServerId)>>,
|
diagnostics: &'a BTreeMap<Uri, Vec<(lsp::Diagnostic, LanguageServerId)>>,
|
||||||
document: &Document,
|
document: &Document,
|
||||||
) -> impl Iterator<Item = helix_core::Diagnostic> + 'a {
|
) -> impl Iterator<Item = helix_core::Diagnostic> + 'a {
|
||||||
Editor::doc_diagnostics_with_filter(language_servers, diagnostics, document, |_, _| true)
|
Editor::doc_diagnostics_with_filter(language_servers, diagnostics, document, |_, _| true)
|
||||||
@ -1941,15 +1941,15 @@ pub fn doc_diagnostics<'a>(
|
|||||||
/// filtered by `filter` which is invocated with the raw `lsp::Diagnostic` and the language server id it came from
|
/// 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>(
|
pub fn doc_diagnostics_with_filter<'a>(
|
||||||
language_servers: &'a helix_lsp::Registry,
|
language_servers: &'a helix_lsp::Registry,
|
||||||
diagnostics: &'a BTreeMap<PathBuf, Vec<(lsp::Diagnostic, LanguageServerId)>>,
|
diagnostics: &'a BTreeMap<Uri, Vec<(lsp::Diagnostic, LanguageServerId)>>,
|
||||||
document: &Document,
|
document: &Document,
|
||||||
filter: impl Fn(&lsp::Diagnostic, LanguageServerId) -> bool + 'a,
|
filter: impl Fn(&lsp::Diagnostic, LanguageServerId) -> bool + 'a,
|
||||||
) -> impl Iterator<Item = helix_core::Diagnostic> + 'a {
|
) -> impl Iterator<Item = helix_core::Diagnostic> + 'a {
|
||||||
let text = document.text().clone();
|
let text = document.text().clone();
|
||||||
let language_config = document.language.clone();
|
let language_config = document.language.clone();
|
||||||
document
|
document
|
||||||
.path()
|
.uri()
|
||||||
.and_then(|path| diagnostics.get(path))
|
.and_then(|uri| diagnostics.get(&uri))
|
||||||
.map(|diags| {
|
.map(|diags| {
|
||||||
diags.iter().filter_map(move |(diagnostic, lsp_id)| {
|
diags.iter().filter_map(move |(diagnostic, lsp_id)| {
|
||||||
let ls = language_servers.get_by_id(*lsp_id)?;
|
let ls = language_servers.get_by_id(*lsp_id)?;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use crate::editor::Action;
|
use crate::editor::Action;
|
||||||
use crate::Editor;
|
use crate::Editor;
|
||||||
use crate::{DocumentId, ViewId};
|
use crate::{DocumentId, ViewId};
|
||||||
|
use helix_core::Uri;
|
||||||
use helix_lsp::util::generate_transaction_from_edits;
|
use helix_lsp::util::generate_transaction_from_edits;
|
||||||
use helix_lsp::{lsp, OffsetEncoding};
|
use helix_lsp::{lsp, OffsetEncoding};
|
||||||
|
|
||||||
@ -54,18 +55,30 @@ pub struct ApplyEditError {
|
|||||||
pub enum ApplyEditErrorKind {
|
pub enum ApplyEditErrorKind {
|
||||||
DocumentChanged,
|
DocumentChanged,
|
||||||
FileNotFound,
|
FileNotFound,
|
||||||
UnknownURISchema,
|
InvalidUrl(helix_core::uri::UrlConversionError),
|
||||||
IoError(std::io::Error),
|
IoError(std::io::Error),
|
||||||
// TODO: check edits before applying and propagate failure
|
// TODO: check edits before applying and propagate failure
|
||||||
// InvalidEdit,
|
// 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 {
|
impl ToString for ApplyEditErrorKind {
|
||||||
fn to_string(&self) -> String {
|
fn to_string(&self) -> String {
|
||||||
match self {
|
match self {
|
||||||
ApplyEditErrorKind::DocumentChanged => "document has changed".to_string(),
|
ApplyEditErrorKind::DocumentChanged => "document has changed".to_string(),
|
||||||
ApplyEditErrorKind::FileNotFound => "file not found".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(),
|
ApplyEditErrorKind::IoError(err) => err.to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -74,25 +87,28 @@ fn to_string(&self) -> String {
|
|||||||
impl Editor {
|
impl Editor {
|
||||||
fn apply_text_edits(
|
fn apply_text_edits(
|
||||||
&mut self,
|
&mut self,
|
||||||
uri: &helix_lsp::Url,
|
url: &helix_lsp::Url,
|
||||||
version: Option<i32>,
|
version: Option<i32>,
|
||||||
text_edits: Vec<lsp::TextEdit>,
|
text_edits: Vec<lsp::TextEdit>,
|
||||||
offset_encoding: OffsetEncoding,
|
offset_encoding: OffsetEncoding,
|
||||||
) -> Result<(), ApplyEditErrorKind> {
|
) -> Result<(), ApplyEditErrorKind> {
|
||||||
let path = match uri.to_file_path() {
|
let uri = match Uri::try_from(url) {
|
||||||
Ok(path) => path,
|
Ok(uri) => uri,
|
||||||
Err(_) => {
|
Err(err) => {
|
||||||
let err = format!("unable to convert URI to filepath: {}", uri);
|
log::error!("{err}");
|
||||||
log::error!("{}", err);
|
return Err(err.into());
|
||||||
self.set_error(err);
|
|
||||||
return Err(ApplyEditErrorKind::UnknownURISchema);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
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,
|
Ok(doc_id) => doc_id,
|
||||||
Err(err) => {
|
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);
|
log::error!("{}", err);
|
||||||
self.set_error(err);
|
self.set_error(err);
|
||||||
return Err(ApplyEditErrorKind::FileNotFound);
|
return Err(ApplyEditErrorKind::FileNotFound);
|
||||||
@ -158,9 +174,9 @@ pub fn apply_workspace_edit(
|
|||||||
for (i, operation) in operations.iter().enumerate() {
|
for (i, operation) in operations.iter().enumerate() {
|
||||||
match operation {
|
match operation {
|
||||||
lsp::DocumentChangeOperation::Op(op) => {
|
lsp::DocumentChangeOperation::Op(op) => {
|
||||||
self.apply_document_resource_op(op).map_err(|io| {
|
self.apply_document_resource_op(op).map_err(|err| {
|
||||||
ApplyEditError {
|
ApplyEditError {
|
||||||
kind: ApplyEditErrorKind::IoError(io),
|
kind: err,
|
||||||
failed_change_idx: i,
|
failed_change_idx: i,
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
@ -214,12 +230,18 @@ pub fn apply_workspace_edit(
|
|||||||
Ok(())
|
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 lsp::ResourceOp;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
// NOTE: If `Uri` gets another variant than `Path`, the below `expect`s
|
||||||
|
// may no longer be valid.
|
||||||
match op {
|
match op {
|
||||||
ResourceOp::Create(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| {
|
let ignore_if_exists = op.options.as_ref().map_or(false, |options| {
|
||||||
!options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
|
!options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
|
||||||
});
|
});
|
||||||
@ -236,7 +258,8 @@ fn apply_document_resource_op(&mut self, op: &lsp::ResourceOp) -> std::io::Resul
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ResourceOp::Delete(op) => {
|
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() {
|
if path.is_dir() {
|
||||||
let recursive = op
|
let recursive = op
|
||||||
.options
|
.options
|
||||||
@ -251,17 +274,19 @@ fn apply_document_resource_op(&mut self, op: &lsp::ResourceOp) -> std::io::Resul
|
|||||||
}
|
}
|
||||||
self.language_servers.file_event_handler.file_changed(path);
|
self.language_servers.file_event_handler.file_changed(path);
|
||||||
} else if path.is_file() {
|
} else if path.is_file() {
|
||||||
fs::remove_file(&path)?;
|
fs::remove_file(path)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ResourceOp::Rename(op) => {
|
ResourceOp::Rename(op) => {
|
||||||
let from = op.old_uri.to_file_path().unwrap();
|
let from_uri = Uri::try_from(&op.old_uri)?;
|
||||||
let to = op.new_uri.to_file_path().unwrap();
|
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| {
|
let ignore_if_exists = op.options.as_ref().map_or(false, |options| {
|
||||||
!options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
|
!options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
|
||||||
});
|
});
|
||||||
if !ignore_if_exists || !to.exists() {
|
if !ignore_if_exists || !to.exists() {
|
||||||
self.move_path(&from, &to)?;
|
self.move_path(from, to)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user