forked from Mirrors/helix
@ -1,3 +1,3 @@
|
||||
[toolchain]
|
||||
channel = "1.59.0"
|
||||
channel = "1.61.0"
|
||||
components = ["rustfmt", "rust-src"]
|
||||
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 2.8 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 15 KiB |
@ -1,17 +0,0 @@
|
||||
use crate::{Rope, Selection};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct State {
|
||||
pub doc: Rope,
|
||||
pub selection: Selection,
|
||||
}
|
||||
|
||||
impl State {
|
||||
#[must_use]
|
||||
pub fn new(doc: Rope) -> Self {
|
||||
Self {
|
||||
doc,
|
||||
selection: Selection::point(0),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,26 @@
|
||||
use std::borrow::Cow;
|
||||
use std::process::Command;
|
||||
|
||||
const VERSION: &str = include_str!("../VERSION");
|
||||
|
||||
fn main() {
|
||||
let git_hash = Command::new("git")
|
||||
.args(["rev-parse", "HEAD"])
|
||||
.output()
|
||||
.ok()
|
||||
.filter(|output| output.status.success())
|
||||
.and_then(|x| String::from_utf8(x.stdout).ok());
|
||||
|
||||
let version: Cow<_> = match git_hash {
|
||||
Some(git_hash) => format!("{} ({})", VERSION, &git_hash[..8]).into(),
|
||||
None => VERSION.into(),
|
||||
};
|
||||
|
||||
println!(
|
||||
"cargo:rustc-env=BUILD_TARGET={}",
|
||||
std::env::var("TARGET").unwrap()
|
||||
);
|
||||
|
||||
println!("cargo:rerun-if-changed=../VERSION");
|
||||
println!("cargo:rustc-env=VERSION_AND_GIT_HASH={}", version);
|
||||
}
|
||||
|
@ -1,30 +1,9 @@
|
||||
use helix_loader::grammar::{build_grammars, fetch_grammars};
|
||||
use std::borrow::Cow;
|
||||
use std::process::Command;
|
||||
|
||||
const VERSION: &str = include_str!("../VERSION");
|
||||
|
||||
fn main() {
|
||||
let git_hash = Command::new("git")
|
||||
.args(&["rev-parse", "HEAD"])
|
||||
.output()
|
||||
.ok()
|
||||
.filter(|output| output.status.success())
|
||||
.and_then(|x| String::from_utf8(x.stdout).ok());
|
||||
|
||||
let version: Cow<_> = match git_hash {
|
||||
Some(git_hash) => format!("{} ({})", VERSION, &git_hash[..8]).into(),
|
||||
None => VERSION.into(),
|
||||
};
|
||||
|
||||
if std::env::var("HELIX_DISABLE_AUTO_GRAMMAR_BUILD").is_err() {
|
||||
fetch_grammars().expect("Failed to fetch tree-sitter grammars");
|
||||
build_grammars(Some(std::env::var("TARGET").unwrap()))
|
||||
.expect("Failed to compile tree-sitter grammars");
|
||||
}
|
||||
|
||||
println!("cargo:rerun-if-changed=../runtime/grammars/");
|
||||
println!("cargo:rerun-if-changed=../VERSION");
|
||||
|
||||
println!("cargo:rustc-env=VERSION_AND_GIT_HASH={}", version);
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,74 @@
|
||||
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
|
||||
use fuzzy_matcher::FuzzyMatcher;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
pub struct FuzzyQuery {
|
||||
queries: Vec<String>,
|
||||
}
|
||||
|
||||
impl FuzzyQuery {
|
||||
pub fn new(query: &str) -> FuzzyQuery {
|
||||
let mut saw_backslash = false;
|
||||
let queries = query
|
||||
.split(|c| {
|
||||
saw_backslash = match c {
|
||||
' ' if !saw_backslash => return true,
|
||||
'\\' => true,
|
||||
_ => false,
|
||||
};
|
||||
false
|
||||
})
|
||||
.filter_map(|query| {
|
||||
if query.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(query.replace("\\ ", " "))
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
FuzzyQuery { queries }
|
||||
}
|
||||
|
||||
pub fn fuzzy_match(&self, item: &str, matcher: &Matcher) -> Option<i64> {
|
||||
// use the rank of the first query for the rank, because merging ranks is not really possible
|
||||
// this behaviour matches fzf and skim
|
||||
let score = matcher.fuzzy_match(item, self.queries.get(0)?)?;
|
||||
if self
|
||||
.queries
|
||||
.iter()
|
||||
.any(|query| matcher.fuzzy_match(item, query).is_none())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(score)
|
||||
}
|
||||
|
||||
pub fn fuzzy_indicies(&self, item: &str, matcher: &Matcher) -> Option<(i64, Vec<usize>)> {
|
||||
if self.queries.len() == 1 {
|
||||
return matcher.fuzzy_indices(item, &self.queries[0]);
|
||||
}
|
||||
|
||||
// use the rank of the first query for the rank, because merging ranks is not really possible
|
||||
// this behaviour matches fzf and skim
|
||||
let (score, mut indicies) = matcher.fuzzy_indices(item, self.queries.get(0)?)?;
|
||||
|
||||
// fast path for the common case of not using a space
|
||||
// during matching this branch should be free thanks to branch prediction
|
||||
if self.queries.len() == 1 {
|
||||
return Some((score, indicies));
|
||||
}
|
||||
|
||||
for query in &self.queries[1..] {
|
||||
let (_, matched_indicies) = matcher.fuzzy_indices(item, query)?;
|
||||
indicies.extend_from_slice(&matched_indicies);
|
||||
}
|
||||
|
||||
// deadup and remove duplicate matches
|
||||
indicies.sort_unstable();
|
||||
indicies.dedup();
|
||||
|
||||
Some((score, indicies))
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
use crate::ui::fuzzy_match::FuzzyQuery;
|
||||
use crate::ui::fuzzy_match::Matcher;
|
||||
|
||||
fn run_test<'a>(query: &str, items: &'a [&'a str]) -> Vec<String> {
|
||||
let query = FuzzyQuery::new(query);
|
||||
let matcher = Matcher::default();
|
||||
items
|
||||
.iter()
|
||||
.filter_map(|item| {
|
||||
let (_, indicies) = query.fuzzy_indicies(item, &matcher)?;
|
||||
let matched_string = indicies
|
||||
.iter()
|
||||
.map(|&pos| item.chars().nth(pos).unwrap())
|
||||
.collect();
|
||||
Some(matched_string)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_single_value() {
|
||||
let matches = run_test("foo", &["foobar", "foo", "bar"]);
|
||||
assert_eq!(matches, &["foo", "foo"])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn match_multiple_values() {
|
||||
let matches = run_test(
|
||||
"foo bar",
|
||||
&["foo bar", "foo bar", "bar foo", "bar", "foo"],
|
||||
);
|
||||
assert_eq!(matches, &["foobar", "foobar", "barfoo"])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn space_escape() {
|
||||
let matches = run_test(r"foo\ bar", &["bar foo", "foo bar", "foobar"]);
|
||||
assert_eq!(matches, &["foo bar"])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trim() {
|
||||
let matches = run_test(r" foo bar ", &["bar foo", "foo bar", "foobar"]);
|
||||
assert_eq!(matches, &["barfoo", "foobar", "foobar"]);
|
||||
let matches = run_test(r" foo bar\ ", &["bar foo", "foo bar", "foobar"]);
|
||||
assert_eq!(matches, &["bar foo"])
|
||||
}
|
@ -0,0 +1,190 @@
|
||||
use super::*;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_split_write_quit_all() -> anyhow::Result<()> {
|
||||
let mut file1 = tempfile::NamedTempFile::new()?;
|
||||
let mut file2 = tempfile::NamedTempFile::new()?;
|
||||
let mut file3 = tempfile::NamedTempFile::new()?;
|
||||
|
||||
let mut app = helpers::AppBuilder::new()
|
||||
.with_file(file1.path(), None)
|
||||
.build()?;
|
||||
|
||||
test_key_sequences(
|
||||
&mut app,
|
||||
vec![
|
||||
(
|
||||
Some(&format!(
|
||||
"ihello1<esc>:sp<ret>:o {}<ret>ihello2<esc>:sp<ret>:o {}<ret>ihello3<esc>",
|
||||
file2.path().to_string_lossy(),
|
||||
file3.path().to_string_lossy()
|
||||
)),
|
||||
Some(&|app| {
|
||||
let docs: Vec<_> = app.editor.documents().collect();
|
||||
assert_eq!(3, docs.len());
|
||||
|
||||
let doc1 = docs
|
||||
.iter()
|
||||
.find(|doc| doc.path().unwrap() == file1.path())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!("hello1", doc1.text().to_string());
|
||||
|
||||
let doc2 = docs
|
||||
.iter()
|
||||
.find(|doc| doc.path().unwrap() == file2.path())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!("hello2", doc2.text().to_string());
|
||||
|
||||
let doc3 = docs
|
||||
.iter()
|
||||
.find(|doc| doc.path().unwrap() == file3.path())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!("hello3", doc3.text().to_string());
|
||||
|
||||
helpers::assert_status_not_error(&app.editor);
|
||||
assert_eq!(3, app.editor.tree.views().count());
|
||||
}),
|
||||
),
|
||||
(
|
||||
Some(":wqa<ret>"),
|
||||
Some(&|app| {
|
||||
helpers::assert_status_not_error(&app.editor);
|
||||
assert_eq!(0, app.editor.tree.views().count());
|
||||
}),
|
||||
),
|
||||
],
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
helpers::assert_file_has_content(file1.as_file_mut(), "hello1")?;
|
||||
helpers::assert_file_has_content(file2.as_file_mut(), "hello2")?;
|
||||
helpers::assert_file_has_content(file3.as_file_mut(), "hello3")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_split_write_quit_same_file() -> anyhow::Result<()> {
|
||||
let mut file = tempfile::NamedTempFile::new()?;
|
||||
let mut app = helpers::AppBuilder::new()
|
||||
.with_file(file.path(), None)
|
||||
.build()?;
|
||||
|
||||
test_key_sequences(
|
||||
&mut app,
|
||||
vec![
|
||||
(
|
||||
Some("O<esc>ihello<esc>:sp<ret>ogoodbye<esc>"),
|
||||
Some(&|app| {
|
||||
assert_eq!(2, app.editor.tree.views().count());
|
||||
helpers::assert_status_not_error(&app.editor);
|
||||
|
||||
let mut docs: Vec<_> = app.editor.documents().collect();
|
||||
assert_eq!(1, docs.len());
|
||||
|
||||
let doc = docs.pop().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
helpers::platform_line("hello\ngoodbye"),
|
||||
doc.text().to_string()
|
||||
);
|
||||
|
||||
assert!(doc.is_modified());
|
||||
}),
|
||||
),
|
||||
(
|
||||
Some(":wq<ret>"),
|
||||
Some(&|app| {
|
||||
helpers::assert_status_not_error(&app.editor);
|
||||
assert_eq!(1, app.editor.tree.views().count());
|
||||
|
||||
let mut docs: Vec<_> = app.editor.documents().collect();
|
||||
assert_eq!(1, docs.len());
|
||||
|
||||
let doc = docs.pop().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
helpers::platform_line("hello\ngoodbye"),
|
||||
doc.text().to_string()
|
||||
);
|
||||
|
||||
assert!(!doc.is_modified());
|
||||
}),
|
||||
),
|
||||
],
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
helpers::assert_file_has_content(
|
||||
file.as_file_mut(),
|
||||
&helpers::platform_line("hello\ngoodbye"),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_changes_in_splits_apply_to_all_views() -> anyhow::Result<()> {
|
||||
// See <https://github.com/helix-editor/helix/issues/4732>.
|
||||
// Transactions must be applied to any view that has the changed document open.
|
||||
// This sequence would panic since the jumplist entry would be modified in one
|
||||
// window but not the other. Attempting to update the changelist in the other
|
||||
// window would cause a panic since it would point outside of the document.
|
||||
|
||||
// The key sequence here:
|
||||
// * <C-w>v Create a vertical split of the current buffer.
|
||||
// Both views look at the same doc.
|
||||
// * [<space> Add a line ending to the beginning of the document.
|
||||
// The cursor is now at line 2 in window 2.
|
||||
// * <C-s> Save that selection to the jumplist in window 2.
|
||||
// * <C-w>w Switch to window 1.
|
||||
// * kd Delete line 1 in window 1.
|
||||
// * <C-w>q Close window 1, focusing window 2.
|
||||
// * d Delete line 1 in window 2.
|
||||
//
|
||||
// This panicked in the past because the jumplist entry on line 2 of window 2
|
||||
// was not updated and after the `kd` step, pointed outside of the document.
|
||||
test(("#[|]#", "<C-w>v[<space><C-s><C-w>wkd<C-w>qd", "#[|]#")).await?;
|
||||
|
||||
// Transactions are applied to the views for windows lazily when they are focused.
|
||||
// This case panics if the transactions and inversions are not applied in the
|
||||
// correct order as we switch between windows.
|
||||
test((
|
||||
"#[|]#",
|
||||
"[<space>[<space>[<space><C-w>vuuu<C-w>wUUU<C-w>quuu",
|
||||
"#[|]#",
|
||||
))
|
||||
.await?;
|
||||
|
||||
// See <https://github.com/helix-editor/helix/issues/4957>.
|
||||
// This sequence undoes part of the history and then adds new changes, creating a
|
||||
// new branch in the history tree. `View::sync_changes` applies transactions down
|
||||
// and up to the lowest common ancestor in the path between old and new revision
|
||||
// numbers. If we apply these up/down transactions in the wrong order, this case
|
||||
// panics.
|
||||
// The key sequence:
|
||||
// * 3[<space> Create three empty lines so we are at the end of the document.
|
||||
// * <C-w>v<C-s> Create a split and save that point at the end of the document
|
||||
// in the jumplist.
|
||||
// * <C-w>w Switch back to the first window.
|
||||
// * uu Undo twice (not three times which would bring us back to the
|
||||
// root of the tree).
|
||||
// * 3[<space> Create three empty lines. Now the end of the document is past
|
||||
// where it was on step 1.
|
||||
// * <C-w>q Close window 1, focusing window 2 and causing a sync. This step
|
||||
// panics if we don't apply in the right order.
|
||||
// * %d Clean up the buffer.
|
||||
test((
|
||||
"#[|]#",
|
||||
"3[<space><C-w>v<C-s><C-w>wuu3[<space><C-w>q%d",
|
||||
"#[|]#",
|
||||
))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue