forked from Mirrors/helix
Merge branch 'tree_explore' of github.com:cossonfork/helix into tree_explore
commit
e5ed461ca7
@ -1,2 +1,3 @@
|
||||
[alias]
|
||||
xtask = "run --package xtask --"
|
||||
integration-test = "test --features integration --workspace --test integration"
|
||||
|
@ -0,0 +1,11 @@
|
||||
# Auto detect text files and perform normalization
|
||||
* text=auto
|
||||
|
||||
*.rs text diff=rust
|
||||
*.toml text diff=toml
|
||||
|
||||
*.scm text diff=scheme
|
||||
*.md text diff=markdown
|
||||
|
||||
book/theme/highlight.js linguist-vendored
|
||||
Cargo.lock text
|
@ -1,6 +1,5 @@
|
||||
target
|
||||
.direnv
|
||||
helix-term/rustfmt.toml
|
||||
helix-syntax/languages/
|
||||
result
|
||||
runtime/grammars
|
||||
|
@ -0,0 +1,5 @@
|
||||
# Things that we don't want ripgrep to search that we do want in git
|
||||
# https://github.com/BurntSushi/ripgrep/blob/master/GUIDE.md#automatic-filtering
|
||||
|
||||
# Minified JS vendored from mdbook
|
||||
book/theme/highlight.js
|
@ -0,0 +1,49 @@
|
||||
# You can move it here ~/.config/elvish/lib/hx.elv
|
||||
# Or add `eval (slurp < ~/$REPOS/helix/contrib/completion/hx.elv)`
|
||||
# Be sure to replace `$REPOS` with something that makes sense for you!
|
||||
|
||||
### Renders a pretty completion candidate
|
||||
var candidate = { | _stem _desc |
|
||||
edit:complex-candidate $_stem &display=(styled $_stem bold)(styled " "$_desc dim)
|
||||
}
|
||||
|
||||
### These commands will invalidate further input (i.e. not react to them)
|
||||
var skips = [ "--tutor" "--help" "--version" "-V" "--health" ]
|
||||
|
||||
### Grammar commands
|
||||
var grammar = [ "--grammar" "-g" ]
|
||||
|
||||
### Config commands
|
||||
var config = [ "--config" "-c" ]
|
||||
|
||||
### Set an arg-completer for the `hx` binary
|
||||
set edit:completion:arg-completer[hx] = {|@args|
|
||||
var n = (count $args)
|
||||
if (>= $n 3) {
|
||||
# Stop completions if passed arg will take presedence
|
||||
# and invalidate further input
|
||||
if (has-value $skips $args[-2]) {
|
||||
return
|
||||
}
|
||||
# If the previous arg == --grammar, then only suggest:
|
||||
if (has-value $grammar $args[-2]) {
|
||||
$candidate "fetch" "Fetch the tree-sitter grammars"
|
||||
$candidate "build" "Build the tree-sitter grammars"
|
||||
return
|
||||
}
|
||||
# When we have --config, we need a file
|
||||
if (has-values $config $args[-2]) {
|
||||
edit:complete-filename $args[-1] | each { |v| put $v[stem] }
|
||||
return
|
||||
}
|
||||
}
|
||||
edit:complete-filename $args[-1] | each { |v| put $v[stem]}
|
||||
$candidate "--help" "(Prints help information)"
|
||||
$candidate "--version" "(Prints version information)"
|
||||
$candidate "--tutor" "(Loads the tutorial)"
|
||||
$candidate "--health" "(Checks for errors in editor setup)"
|
||||
$candidate "--grammar" "(Fetch or build the tree-sitter grammars)"
|
||||
$candidate "--vsplit" "(Splits all given files vertically)"
|
||||
$candidate "--hsplit" "(Splits all given files horizontally)"
|
||||
$candidate "--config" "(Specifies a file to use for configuration)"
|
||||
}
|
@ -1 +0,0 @@
|
||||
../runtime/themes
|
@ -0,0 +1,8 @@
|
||||
# Flake's default package for non-flake-enabled nix instances
|
||||
let
|
||||
compat = builtins.fetchTarball {
|
||||
url = "https://github.com/edolstra/flake-compat/archive/b4a34015c698c7793d592d66adbab377907a2be8.tar.gz";
|
||||
sha256 = "sha256:1qc703yg0babixi6wshn5wm2kgl5y1drcswgszh4xxzbrwkk9sv7";
|
||||
};
|
||||
in
|
||||
(import compat {src = ./.;}).defaultNix.default
|
@ -0,0 +1,59 @@
|
||||
## Checklist
|
||||
|
||||
Helix releases are versioned in the Calendar Versioning scheme:
|
||||
`YY.0M(.MICRO)`, for example `22.05` for May of 2022. In these instructions
|
||||
we'll use `<tag>` as a placeholder for the tag being published.
|
||||
|
||||
* Merge the changelog PR
|
||||
* Tag and push
|
||||
* `git tag -s -m "<tag>" -a <tag> && git push`
|
||||
* Make sure to switch to master and pull first
|
||||
* Edit the `VERSION` file and change the date to the next planned release
|
||||
* Releases are planned to happen every two months, so `22.05` would change to `22.07`
|
||||
* Wait for the Release CI to finish
|
||||
* It will automatically turn the git tag into a GitHub release when it uploads artifacts
|
||||
* Edit the new release
|
||||
* Use `<tag>` as the title
|
||||
* Link to the changelog and release notes
|
||||
* Merge the release notes PR
|
||||
* Download the macos and linux binaries and update the `sha256`s in the [homebrew formula]
|
||||
* Use `sha256sum` on the downloaded `.tar.xz` files to determine the hash
|
||||
* Link to the release notes in this-week-in-rust
|
||||
* [Example PR](https://github.com/rust-lang/this-week-in-rust/pull/3300)
|
||||
* Post to reddit
|
||||
* [Example post](https://www.reddit.com/r/rust/comments/uzp5ze/helix_editor_2205_released/)
|
||||
|
||||
[homebrew formula]: https://github.com/helix-editor/homebrew-helix/blob/master/Formula/helix.rb
|
||||
|
||||
## Changelog Curation
|
||||
|
||||
The changelog is currently created manually by reading through commits in the
|
||||
log since the last release. GitHub's compare view is a nice way to approach
|
||||
this. For example when creating the 22.07 release notes, this compare link
|
||||
may be used
|
||||
|
||||
```
|
||||
https://github.com/helix-editor/helix/compare/22.05...master
|
||||
```
|
||||
|
||||
Either side of the triple-dot may be replaced with an exact revision, so if
|
||||
you wish to incrementally compile the changelog, you can tackle a weeks worth
|
||||
or so, record the revision where you stopped, and use that as a starting point
|
||||
next week:
|
||||
|
||||
```
|
||||
https://github.com/helix-editor/helix/compare/7706a4a0d8b67b943c31d0c5f7b00d357b5d838d...master
|
||||
```
|
||||
|
||||
A work-in-progress commit for a changelog might look like
|
||||
[this example](https://github.com/helix-editor/helix/commit/831adfd4c709ca16b248799bfef19698d5175e55).
|
||||
|
||||
Not every PR or commit needs a blurb in the changelog. Each release section
|
||||
tends to have a blurb that links to a GitHub comparison between release
|
||||
versions for convenience:
|
||||
|
||||
> As usual, the following is a summary of each of the changes since the last
|
||||
> release. For the full log, check out the git log.
|
||||
|
||||
Typically, small changes like dependencies or documentation updates, refactors,
|
||||
or meta changes like GitHub Actions work are left out.
|
@ -1 +0,0 @@
|
||||
../../../src/indent.rs
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,133 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use helix_core::syntax;
|
||||
use helix_view::graphics::{Margin, Rect, Style};
|
||||
use tui::buffer::Buffer;
|
||||
use tui::widgets::{BorderType, Paragraph, Widget, Wrap};
|
||||
|
||||
use crate::compositor::{Component, Compositor, Context};
|
||||
|
||||
use crate::ui::Markdown;
|
||||
|
||||
use super::Popup;
|
||||
|
||||
pub struct SignatureHelp {
|
||||
signature: String,
|
||||
signature_doc: Option<String>,
|
||||
/// Part of signature text
|
||||
active_param_range: Option<(usize, usize)>,
|
||||
|
||||
language: String,
|
||||
config_loader: Arc<syntax::Loader>,
|
||||
}
|
||||
|
||||
impl SignatureHelp {
|
||||
pub const ID: &'static str = "signature-help";
|
||||
|
||||
pub fn new(signature: String, language: String, config_loader: Arc<syntax::Loader>) -> Self {
|
||||
Self {
|
||||
signature,
|
||||
signature_doc: None,
|
||||
active_param_range: None,
|
||||
language,
|
||||
config_loader,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_signature_doc(&mut self, signature_doc: Option<String>) {
|
||||
self.signature_doc = signature_doc;
|
||||
}
|
||||
|
||||
pub fn set_active_param_range(&mut self, offset: Option<(usize, usize)>) {
|
||||
self.active_param_range = offset;
|
||||
}
|
||||
|
||||
pub fn visible_popup(compositor: &mut Compositor) -> Option<&mut Popup<Self>> {
|
||||
compositor.find_id::<Popup<Self>>(Self::ID)
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for SignatureHelp {
|
||||
fn render(&mut self, area: Rect, surface: &mut Buffer, cx: &mut Context) {
|
||||
let margin = Margin::horizontal(1);
|
||||
|
||||
let active_param_span = self.active_param_range.map(|(start, end)| {
|
||||
vec![(
|
||||
cx.editor.theme.find_scope_index("ui.selection").unwrap(),
|
||||
start..end,
|
||||
)]
|
||||
});
|
||||
|
||||
let sig_text = crate::ui::markdown::highlighted_code_block(
|
||||
self.signature.clone(),
|
||||
&self.language,
|
||||
Some(&cx.editor.theme),
|
||||
Arc::clone(&self.config_loader),
|
||||
active_param_span,
|
||||
);
|
||||
|
||||
let (_, sig_text_height) = crate::ui::text::required_size(&sig_text, area.width);
|
||||
let sig_text_area = area.clip_top(1).with_height(sig_text_height);
|
||||
let sig_text_para = Paragraph::new(sig_text).wrap(Wrap { trim: false });
|
||||
sig_text_para.render(sig_text_area.inner(&margin), surface);
|
||||
|
||||
if self.signature_doc.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
let sep_style = Style::default();
|
||||
let borders = BorderType::line_symbols(BorderType::Plain);
|
||||
for x in sig_text_area.left()..sig_text_area.right() {
|
||||
if let Some(cell) = surface.get_mut(x, sig_text_area.bottom()) {
|
||||
cell.set_symbol(borders.horizontal).set_style(sep_style);
|
||||
}
|
||||
}
|
||||
|
||||
let sig_doc = match &self.signature_doc {
|
||||
None => return,
|
||||
Some(doc) => Markdown::new(doc.clone(), Arc::clone(&self.config_loader)),
|
||||
};
|
||||
let sig_doc = sig_doc.parse(Some(&cx.editor.theme));
|
||||
let sig_doc_area = area.clip_top(sig_text_area.height + 2);
|
||||
let sig_doc_para = Paragraph::new(sig_doc)
|
||||
.wrap(Wrap { trim: false })
|
||||
.scroll((cx.scroll.unwrap_or_default() as u16, 0));
|
||||
sig_doc_para.render(sig_doc_area.inner(&margin), surface);
|
||||
}
|
||||
|
||||
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
|
||||
const PADDING: u16 = 2;
|
||||
const SEPARATOR_HEIGHT: u16 = 1;
|
||||
|
||||
if PADDING >= viewport.1 || PADDING >= viewport.0 {
|
||||
return None;
|
||||
}
|
||||
let max_text_width = (viewport.0 - PADDING).min(120);
|
||||
|
||||
let signature_text = crate::ui::markdown::highlighted_code_block(
|
||||
self.signature.clone(),
|
||||
&self.language,
|
||||
None,
|
||||
Arc::clone(&self.config_loader),
|
||||
None,
|
||||
);
|
||||
let (sig_width, sig_height) =
|
||||
crate::ui::text::required_size(&signature_text, max_text_width);
|
||||
|
||||
let (width, height) = match self.signature_doc {
|
||||
Some(ref doc) => {
|
||||
let doc_md = Markdown::new(doc.clone(), Arc::clone(&self.config_loader));
|
||||
let doc_text = doc_md.parse(None);
|
||||
let (doc_width, doc_height) =
|
||||
crate::ui::text::required_size(&doc_text, max_text_width);
|
||||
(
|
||||
sig_width.max(doc_width),
|
||||
sig_height + SEPARATOR_HEIGHT + doc_height,
|
||||
)
|
||||
}
|
||||
None => (sig_width, sig_height),
|
||||
};
|
||||
|
||||
Some((width + PADDING, height + PADDING))
|
||||
}
|
||||
}
|
@ -0,0 +1,375 @@
|
||||
use helix_core::{coords_at_pos, encoding, Position};
|
||||
use helix_view::{
|
||||
document::{Mode, SCRATCH_BUFFER_NAME},
|
||||
graphics::Rect,
|
||||
theme::Style,
|
||||
Document, Editor, View,
|
||||
};
|
||||
|
||||
use crate::ui::ProgressSpinners;
|
||||
|
||||
use helix_view::editor::StatusLineElement as StatusLineElementID;
|
||||
use tui::buffer::Buffer as Surface;
|
||||
use tui::text::{Span, Spans};
|
||||
|
||||
pub struct RenderContext<'a> {
|
||||
pub editor: &'a Editor,
|
||||
pub doc: &'a Document,
|
||||
pub view: &'a View,
|
||||
pub focused: bool,
|
||||
pub spinners: &'a ProgressSpinners,
|
||||
pub parts: RenderBuffer<'a>,
|
||||
}
|
||||
|
||||
impl<'a> RenderContext<'a> {
|
||||
pub fn new(
|
||||
editor: &'a Editor,
|
||||
doc: &'a Document,
|
||||
view: &'a View,
|
||||
focused: bool,
|
||||
spinners: &'a ProgressSpinners,
|
||||
) -> Self {
|
||||
RenderContext {
|
||||
editor,
|
||||
doc,
|
||||
view,
|
||||
focused,
|
||||
spinners,
|
||||
parts: RenderBuffer::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct RenderBuffer<'a> {
|
||||
pub left: Spans<'a>,
|
||||
pub center: Spans<'a>,
|
||||
pub right: Spans<'a>,
|
||||
}
|
||||
|
||||
pub fn render(context: &mut RenderContext, viewport: Rect, surface: &mut Surface) {
|
||||
let base_style = if context.focused {
|
||||
context.editor.theme.get("ui.statusline")
|
||||
} else {
|
||||
context.editor.theme.get("ui.statusline.inactive")
|
||||
};
|
||||
|
||||
surface.set_style(viewport.with_height(1), base_style);
|
||||
|
||||
let write_left = |context: &mut RenderContext, text, style| {
|
||||
append(&mut context.parts.left, text, &base_style, style)
|
||||
};
|
||||
let write_center = |context: &mut RenderContext, text, style| {
|
||||
append(&mut context.parts.center, text, &base_style, style)
|
||||
};
|
||||
let write_right = |context: &mut RenderContext, text, style| {
|
||||
append(&mut context.parts.right, text, &base_style, style)
|
||||
};
|
||||
|
||||
// Left side of the status line.
|
||||
|
||||
let element_ids = &context.editor.config().statusline.left;
|
||||
element_ids
|
||||
.iter()
|
||||
.map(|element_id| get_render_function(*element_id))
|
||||
.for_each(|render| render(context, write_left));
|
||||
|
||||
surface.set_spans(
|
||||
viewport.x,
|
||||
viewport.y,
|
||||
&context.parts.left,
|
||||
context.parts.left.width() as u16,
|
||||
);
|
||||
|
||||
// Right side of the status line.
|
||||
|
||||
let element_ids = &context.editor.config().statusline.right;
|
||||
element_ids
|
||||
.iter()
|
||||
.map(|element_id| get_render_function(*element_id))
|
||||
.for_each(|render| render(context, write_right));
|
||||
|
||||
surface.set_spans(
|
||||
viewport.x
|
||||
+ viewport
|
||||
.width
|
||||
.saturating_sub(context.parts.right.width() as u16),
|
||||
viewport.y,
|
||||
&context.parts.right,
|
||||
context.parts.right.width() as u16,
|
||||
);
|
||||
|
||||
// Center of the status line.
|
||||
|
||||
let element_ids = &context.editor.config().statusline.center;
|
||||
element_ids
|
||||
.iter()
|
||||
.map(|element_id| get_render_function(*element_id))
|
||||
.for_each(|render| render(context, write_center));
|
||||
|
||||
// Width of the empty space between the left and center area and between the center and right area.
|
||||
let spacing = 1u16;
|
||||
|
||||
let edge_width = context.parts.left.width().max(context.parts.right.width()) as u16;
|
||||
let center_max_width = viewport.width.saturating_sub(2 * edge_width + 2 * spacing);
|
||||
let center_width = center_max_width.min(context.parts.center.width() as u16);
|
||||
|
||||
surface.set_spans(
|
||||
viewport.x + viewport.width / 2 - center_width / 2,
|
||||
viewport.y,
|
||||
&context.parts.center,
|
||||
center_width,
|
||||
);
|
||||
}
|
||||
|
||||
fn append(buffer: &mut Spans, text: String, base_style: &Style, style: Option<Style>) {
|
||||
buffer.0.push(Span::styled(
|
||||
text,
|
||||
style.map_or(*base_style, |s| (*base_style).patch(s)),
|
||||
));
|
||||
}
|
||||
|
||||
fn get_render_function<F>(element_id: StatusLineElementID) -> impl Fn(&mut RenderContext, F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
match element_id {
|
||||
helix_view::editor::StatusLineElement::Mode => render_mode,
|
||||
helix_view::editor::StatusLineElement::Spinner => render_lsp_spinner,
|
||||
helix_view::editor::StatusLineElement::FileName => render_file_name,
|
||||
helix_view::editor::StatusLineElement::FileEncoding => render_file_encoding,
|
||||
helix_view::editor::StatusLineElement::FileLineEnding => render_file_line_ending,
|
||||
helix_view::editor::StatusLineElement::FileType => render_file_type,
|
||||
helix_view::editor::StatusLineElement::Diagnostics => render_diagnostics,
|
||||
helix_view::editor::StatusLineElement::Selections => render_selections,
|
||||
helix_view::editor::StatusLineElement::Position => render_position,
|
||||
helix_view::editor::StatusLineElement::PositionPercentage => render_position_percentage,
|
||||
helix_view::editor::StatusLineElement::Separator => render_separator,
|
||||
helix_view::editor::StatusLineElement::Spacer => render_spacer,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_mode<F>(context: &mut RenderContext, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
let visible = context.focused;
|
||||
|
||||
write(
|
||||
context,
|
||||
format!(
|
||||
" {} ",
|
||||
if visible {
|
||||
match context.doc.mode() {
|
||||
Mode::Insert => "INS",
|
||||
Mode::Select => "SEL",
|
||||
Mode::Normal => "NOR",
|
||||
}
|
||||
} else {
|
||||
// If not focused, explicitly leave an empty space instead of returning None.
|
||||
" "
|
||||
}
|
||||
),
|
||||
if visible && context.editor.config().color_modes {
|
||||
match context.doc.mode() {
|
||||
Mode::Insert => Some(context.editor.theme.get("ui.statusline.insert")),
|
||||
Mode::Select => Some(context.editor.theme.get("ui.statusline.select")),
|
||||
Mode::Normal => Some(context.editor.theme.get("ui.statusline.normal")),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn render_lsp_spinner<F>(context: &mut RenderContext, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
write(
|
||||
context,
|
||||
context
|
||||
.doc
|
||||
.language_server()
|
||||
.and_then(|srv| {
|
||||
context
|
||||
.spinners
|
||||
.get(srv.id())
|
||||
.and_then(|spinner| spinner.frame())
|
||||
})
|
||||
// Even if there's no spinner; reserve its space to avoid elements frequently shifting.
|
||||
.unwrap_or(" ")
|
||||
.to_string(),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
fn render_diagnostics<F>(context: &mut RenderContext, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
let (warnings, errors) = context
|
||||
.doc
|
||||
.diagnostics()
|
||||
.iter()
|
||||
.fold((0, 0), |mut counts, diag| {
|
||||
use helix_core::diagnostic::Severity;
|
||||
match diag.severity {
|
||||
Some(Severity::Warning) => counts.0 += 1,
|
||||
Some(Severity::Error) | None => counts.1 += 1,
|
||||
_ => {}
|
||||
}
|
||||
counts
|
||||
});
|
||||
|
||||
if warnings > 0 {
|
||||
write(
|
||||
context,
|
||||
"●".to_string(),
|
||||
Some(context.editor.theme.get("warning")),
|
||||
);
|
||||
write(context, format!(" {} ", warnings), None);
|
||||
}
|
||||
|
||||
if errors > 0 {
|
||||
write(
|
||||
context,
|
||||
"●".to_string(),
|
||||
Some(context.editor.theme.get("error")),
|
||||
);
|
||||
write(context, format!(" {} ", errors), None);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_selections<F>(context: &mut RenderContext, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
let count = context.doc.selection(context.view.id).len();
|
||||
write(
|
||||
context,
|
||||
format!(" {} sel{} ", count, if count == 1 { "" } else { "s" }),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
fn get_position(context: &RenderContext) -> Position {
|
||||
coords_at_pos(
|
||||
context.doc.text().slice(..),
|
||||
context
|
||||
.doc
|
||||
.selection(context.view.id)
|
||||
.primary()
|
||||
.cursor(context.doc.text().slice(..)),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_position<F>(context: &mut RenderContext, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
let position = get_position(context);
|
||||
write(
|
||||
context,
|
||||
format!(" {}:{} ", position.row + 1, position.col + 1),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
fn render_position_percentage<F>(context: &mut RenderContext, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
let position = get_position(context);
|
||||
let maxrows = context.doc.text().len_lines();
|
||||
write(
|
||||
context,
|
||||
format!("{}%", (position.row + 1) * 100 / maxrows),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
fn render_file_encoding<F>(context: &mut RenderContext, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
let enc = context.doc.encoding();
|
||||
|
||||
if enc != encoding::UTF_8 {
|
||||
write(context, format!(" {} ", enc.name()), None);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_file_line_ending<F>(context: &mut RenderContext, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
use helix_core::LineEnding::*;
|
||||
let line_ending = match context.doc.line_ending {
|
||||
Crlf => "CRLF",
|
||||
LF => "LF",
|
||||
#[cfg(feature = "unicode-lines")]
|
||||
VT => "VT", // U+000B -- VerticalTab
|
||||
#[cfg(feature = "unicode-lines")]
|
||||
FF => "FF", // U+000C -- FormFeed
|
||||
#[cfg(feature = "unicode-lines")]
|
||||
CR => "CR", // U+000D -- CarriageReturn
|
||||
#[cfg(feature = "unicode-lines")]
|
||||
Nel => "NEL", // U+0085 -- NextLine
|
||||
#[cfg(feature = "unicode-lines")]
|
||||
LS => "LS", // U+2028 -- Line Separator
|
||||
#[cfg(feature = "unicode-lines")]
|
||||
PS => "PS", // U+2029 -- ParagraphSeparator
|
||||
};
|
||||
|
||||
write(context, format!(" {} ", line_ending), None);
|
||||
}
|
||||
|
||||
fn render_file_type<F>(context: &mut RenderContext, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
let file_type = context.doc.language_id().unwrap_or("text");
|
||||
|
||||
write(context, format!(" {} ", file_type), None);
|
||||
}
|
||||
|
||||
fn render_file_name<F>(context: &mut RenderContext, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
let title = {
|
||||
let rel_path = context.doc.relative_path();
|
||||
let path = rel_path
|
||||
.as_ref()
|
||||
.map(|p| p.to_string_lossy())
|
||||
.unwrap_or_else(|| SCRATCH_BUFFER_NAME.into());
|
||||
format!(
|
||||
" {}{} ",
|
||||
path,
|
||||
if context.doc.is_modified() { "[+]" } else { "" }
|
||||
)
|
||||
};
|
||||
|
||||
write(context, title, None);
|
||||
}
|
||||
|
||||
fn render_separator<F>(context: &mut RenderContext, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
let sep = &context.editor.config().statusline.separator;
|
||||
|
||||
write(
|
||||
context,
|
||||
sep.to_string(),
|
||||
Some(context.editor.theme.get("ui.statusline.separator")),
|
||||
);
|
||||
}
|
||||
|
||||
fn render_spacer<F>(context: &mut RenderContext, write: F)
|
||||
where
|
||||
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
|
||||
{
|
||||
write(context, String::from(" "), None);
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
#[cfg(feature = "integration")]
|
||||
mod test {
|
||||
mod helpers;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use helix_core::{syntax::AutoPairConfig, Position, Selection};
|
||||
use helix_term::{args::Args, config::Config};
|
||||
|
||||
use indoc::indoc;
|
||||
|
||||
use self::helpers::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn hello_world() -> anyhow::Result<()> {
|
||||
test(("#[\n|]#", "ihello world<esc>", "hello world#[|\n]#")).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
mod auto_indent;
|
||||
mod auto_pairs;
|
||||
mod commands;
|
||||
mod movement;
|
||||
mod write;
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn auto_indent_c() -> anyhow::Result<()> {
|
||||
test_with_config(
|
||||
Args {
|
||||
files: vec![(PathBuf::from("foo.c"), Position::default())],
|
||||
..Default::default()
|
||||
},
|
||||
Config::default(),
|
||||
// switches to append mode?
|
||||
(
|
||||
helpers::platform_line("void foo() {#[|}]#").as_ref(),
|
||||
"i<ret><esc>",
|
||||
helpers::platform_line(indoc! {"\
|
||||
void foo() {
|
||||
#[|\n]#\
|
||||
}
|
||||
"})
|
||||
.as_ref(),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn auto_pairs_basic() -> anyhow::Result<()> {
|
||||
test(("#[\n|]#", "i(<esc>", "(#[|)]#\n")).await?;
|
||||
|
||||
test_with_config(
|
||||
Args::default(),
|
||||
Config {
|
||||
editor: helix_view::editor::Config {
|
||||
auto_pairs: AutoPairConfig::Enable(false),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
("#[\n|]#", "i(<esc>", "(#[|\n]#"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
@ -0,0 +1,133 @@
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
ops::RangeInclusive,
|
||||
};
|
||||
|
||||
use helix_core::diagnostic::Severity;
|
||||
use helix_term::application::Application;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_write_quit_fail() -> anyhow::Result<()> {
|
||||
let file = helpers::new_readonly_tempfile()?;
|
||||
|
||||
test_key_sequence(
|
||||
&mut helpers::app_with_file(file.path())?,
|
||||
Some("ihello<esc>:wq<ret>"),
|
||||
Some(&|app| {
|
||||
assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1);
|
||||
}),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_buffer_close_concurrent() -> anyhow::Result<()> {
|
||||
test_key_sequences(
|
||||
&mut Application::new(Args::default(), Config::default())?,
|
||||
vec![
|
||||
(
|
||||
None,
|
||||
Some(&|app| {
|
||||
assert_eq!(1, app.editor.documents().count());
|
||||
assert!(!app.editor.is_err());
|
||||
}),
|
||||
),
|
||||
(
|
||||
Some("ihello<esc>:new<ret>"),
|
||||
Some(&|app| {
|
||||
assert_eq!(2, app.editor.documents().count());
|
||||
assert!(!app.editor.is_err());
|
||||
}),
|
||||
),
|
||||
(
|
||||
Some(":buffer<minus>close<ret>"),
|
||||
Some(&|app| {
|
||||
assert_eq!(1, app.editor.documents().count());
|
||||
assert!(!app.editor.is_err());
|
||||
}),
|
||||
),
|
||||
],
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// verify if writes are queued up, it finishes them before closing the buffer
|
||||
let mut file = tempfile::NamedTempFile::new()?;
|
||||
let mut command = String::new();
|
||||
const RANGE: RangeInclusive<i32> = 1..=1000;
|
||||
|
||||
for i in RANGE {
|
||||
let cmd = format!("%c{}<esc>:w<ret>", i);
|
||||
command.push_str(&cmd);
|
||||
}
|
||||
|
||||
command.push_str(":buffer<minus>close<ret>");
|
||||
|
||||
test_key_sequence(
|
||||
&mut helpers::app_with_file(file.path())?,
|
||||
Some(&command),
|
||||
Some(&|app| {
|
||||
assert!(!app.editor.is_err(), "error: {:?}", app.editor.get_status());
|
||||
|
||||
let doc = app.editor.document_by_path(file.path());
|
||||
assert!(doc.is_none(), "found doc: {:?}", doc);
|
||||
}),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
file.as_file_mut().flush()?;
|
||||
file.as_file_mut().sync_all()?;
|
||||
|
||||
let mut file_content = String::new();
|
||||
file.as_file_mut().read_to_string(&mut file_content)?;
|
||||
assert_eq!(RANGE.end().to_string(), file_content);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_selection_duplication() -> anyhow::Result<()> {
|
||||
// Forward
|
||||
test((
|
||||
platform_line(indoc! {"\
|
||||
#[lo|]#rem
|
||||
ipsum
|
||||
dolor
|
||||
"})
|
||||
.as_str(),
|
||||
"CC",
|
||||
platform_line(indoc! {"\
|
||||
#(lo|)#rem
|
||||
#(ip|)#sum
|
||||
#[do|]#lor
|
||||
"})
|
||||
.as_str(),
|
||||
))
|
||||
.await?;
|
||||
|
||||
// Backward
|
||||
test((
|
||||
platform_line(indoc! {"\
|
||||
#[|lo]#rem
|
||||
ipsum
|
||||
dolor
|
||||
"})
|
||||
.as_str(),
|
||||
"CC",
|
||||
platform_line(indoc! {"\
|
||||
#(|lo)#rem
|
||||
#(|ip)#sum
|
||||
#[|do]#lor
|
||||
"})
|
||||
.as_str(),
|
||||
))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
@ -0,0 +1,213 @@
|
||||
use std::{io::Write, path::PathBuf, time::Duration};
|
||||
|
||||
use anyhow::bail;
|
||||
use crossterm::event::{Event, KeyEvent};
|
||||
use helix_core::{test, Selection, Transaction};
|
||||
use helix_term::{application::Application, args::Args, config::Config};
|
||||
use helix_view::{doc, input::parse_macro};
|
||||
use tempfile::NamedTempFile;
|
||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TestCase {
|
||||
pub in_text: String,
|
||||
pub in_selection: Selection,
|
||||
pub in_keys: String,
|
||||
pub out_text: String,
|
||||
pub out_selection: Selection,
|
||||
}
|
||||
|
||||
impl<S: Into<String>> From<(S, S, S)> for TestCase {
|
||||
fn from((input, keys, output): (S, S, S)) -> Self {
|
||||
let (in_text, in_selection) = test::print(&input.into());
|
||||
let (out_text, out_selection) = test::print(&output.into());
|
||||
|
||||
TestCase {
|
||||
in_text,
|
||||
in_selection,
|
||||
in_keys: keys.into(),
|
||||
out_text,
|
||||
out_selection,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub async fn test_key_sequence(
|
||||
app: &mut Application,
|
||||
in_keys: Option<&str>,
|
||||
test_fn: Option<&dyn Fn(&Application)>,
|
||||
should_exit: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
test_key_sequences(app, vec![(in_keys, test_fn)], should_exit).await
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub async fn test_key_sequences(
|
||||
app: &mut Application,
|
||||
inputs: Vec<(Option<&str>, Option<&dyn Fn(&Application)>)>,
|
||||
should_exit: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
const TIMEOUT: Duration = Duration::from_millis(500);
|
||||
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let mut rx_stream = UnboundedReceiverStream::new(rx);
|
||||
let num_inputs = inputs.len();
|
||||
|
||||
for (i, (in_keys, test_fn)) in inputs.into_iter().enumerate() {
|
||||
if let Some(in_keys) = in_keys {
|
||||
for key_event in parse_macro(in_keys)?.into_iter() {
|
||||
tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?;
|
||||
}
|
||||
}
|
||||
|
||||
let app_exited = !app.event_loop_until_idle(&mut rx_stream).await;
|
||||
|
||||
// the app should not exit from any test until the last one
|
||||
if i < num_inputs - 1 && app_exited {
|
||||
bail!("application exited before test function could run");
|
||||
}
|
||||
|
||||
// verify if it exited on the last iteration if it should have and
|
||||
// the inverse
|
||||
if i == num_inputs - 1 && app_exited != should_exit {
|
||||
bail!("expected app to exit: {} != {}", app_exited, should_exit);
|
||||
}
|
||||
|
||||
if let Some(test) = test_fn {
|
||||
test(app);
|
||||
};
|
||||
}
|
||||
|
||||
if !should_exit {
|
||||
for key_event in parse_macro("<esc>:q!<ret>")?.into_iter() {
|
||||
tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?;
|
||||
}
|
||||
|
||||
let event_loop = app.event_loop(&mut rx_stream);
|
||||
tokio::time::timeout(TIMEOUT, event_loop).await?;
|
||||
}
|
||||
|
||||
app.close().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn test_key_sequence_with_input_text<T: Into<TestCase>>(
|
||||
app: Option<Application>,
|
||||
test_case: T,
|
||||
test_fn: &dyn Fn(&Application),
|
||||
should_exit: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
let test_case = test_case.into();
|
||||
let mut app = match app {
|
||||
Some(app) => app,
|
||||
None => Application::new(Args::default(), Config::default())?,
|
||||
};
|
||||
|
||||
let (view, doc) = helix_view::current!(app.editor);
|
||||
let sel = doc.selection(view.id).clone();
|
||||
|
||||
// replace the initial text with the input text
|
||||
doc.apply(
|
||||
&Transaction::change_by_selection(doc.text(), &sel, |_| {
|
||||
(0, doc.text().len_chars(), Some((&test_case.in_text).into()))
|
||||
})
|
||||
.with_selection(test_case.in_selection.clone()),
|
||||
view.id,
|
||||
);
|
||||
|
||||
test_key_sequence(
|
||||
&mut app,
|
||||
Some(&test_case.in_keys),
|
||||
Some(test_fn),
|
||||
should_exit,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Use this for very simple test cases where there is one input
|
||||
/// document, selection, and sequence of key presses, and you just
|
||||
/// want to verify the resulting document and selection.
|
||||
pub async fn test_with_config<T: Into<TestCase>>(
|
||||
args: Args,
|
||||
config: Config,
|
||||
test_case: T,
|
||||
) -> anyhow::Result<()> {
|
||||
let test_case = test_case.into();
|
||||
let app = Application::new(args, config)?;
|
||||
|
||||
test_key_sequence_with_input_text(
|
||||
Some(app),
|
||||
test_case.clone(),
|
||||
&|app| {
|
||||
let doc = doc!(app.editor);
|
||||
assert_eq!(&test_case.out_text, doc.text());
|
||||
|
||||
let mut selections: Vec<_> = doc.selections().values().cloned().collect();
|
||||
assert_eq!(1, selections.len());
|
||||
|
||||
let sel = selections.pop().unwrap();
|
||||
assert_eq!(test_case.out_selection, sel);
|
||||
},
|
||||
false,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn test<T: Into<TestCase>>(test_case: T) -> anyhow::Result<()> {
|
||||
test_with_config(Args::default(), Config::default(), test_case).await
|
||||
}
|
||||
|
||||
pub fn temp_file_with_contents<S: AsRef<str>>(
|
||||
content: S,
|
||||
) -> anyhow::Result<tempfile::NamedTempFile> {
|
||||
let mut temp_file = tempfile::NamedTempFile::new()?;
|
||||
|
||||
temp_file
|
||||
.as_file_mut()
|
||||
.write_all(content.as_ref().as_bytes())?;
|
||||
|
||||
temp_file.flush()?;
|
||||
temp_file.as_file_mut().sync_all()?;
|
||||
Ok(temp_file)
|
||||
}
|
||||
|
||||
/// Replaces all LF chars with the system's appropriate line feed
|
||||
/// character, and if one doesn't exist already, appends the system's
|
||||
/// appropriate line ending to the end of a string.
|
||||
pub fn platform_line(input: &str) -> String {
|
||||
let line_end = helix_core::DEFAULT_LINE_ENDING.as_str();
|
||||
|
||||
// we can assume that the source files in this code base will always
|
||||
// be LF, so indoc strings will always insert LF
|
||||
let mut output = input.replace('\n', line_end);
|
||||
|
||||
if !output.ends_with(line_end) {
|
||||
output.push_str(line_end);
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
/// Creates a new temporary file that is set to read only. Useful for
|
||||
/// testing write failures.
|
||||
pub fn new_readonly_tempfile() -> anyhow::Result<NamedTempFile> {
|
||||
let mut file = tempfile::NamedTempFile::new()?;
|
||||
let metadata = file.as_file().metadata()?;
|
||||
let mut perms = metadata.permissions();
|
||||
perms.set_readonly(true);
|
||||
file.as_file_mut().set_permissions(perms)?;
|
||||
Ok(file)
|
||||
}
|
||||
|
||||
/// Creates a new Application with default config that opens the given file
|
||||
/// path
|
||||
pub fn app_with_file<P: Into<PathBuf>>(path: P) -> anyhow::Result<Application> {
|
||||
Application::new(
|
||||
Args {
|
||||
files: vec![(path.into(), helix_core::Position::default())],
|
||||
..Default::default()
|
||||
},
|
||||
Config::default(),
|
||||
)
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn insert_mode_cursor_position() -> anyhow::Result<()> {
|
||||
test(TestCase {
|
||||
in_text: String::new(),
|
||||
in_selection: Selection::single(0, 0),
|
||||
in_keys: "i".into(),
|
||||
out_text: String::new(),
|
||||
out_selection: Selection::single(0, 0),
|
||||
})
|
||||
.await?;
|
||||
|
||||
test(("#[\n|]#", "i", "#[|\n]#")).await?;
|
||||
test(("#[\n|]#", "i<esc>", "#[|\n]#")).await?;
|
||||
test(("#[\n|]#", "i<esc>i", "#[|\n]#")).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Range direction is preserved when escaping insert mode to normal
|
||||
#[tokio::test]
|
||||
async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> {
|
||||
test(("#[f|]#oo\n", "vll<A-;><esc>", "#[|foo]#\n")).await?;
|
||||
test((
|
||||
indoc! {"\
|
||||
#[f|]#oo
|
||||
#(b|)#ar"
|
||||
},
|
||||
"vll<A-;><esc>",
|
||||
indoc! {"\
|
||||
#[|foo]#
|
||||
#(|bar)#"
|
||||
},
|
||||
))
|
||||
.await?;
|
||||
|
||||
test((
|
||||
indoc! {"\
|
||||
#[f|]#oo
|
||||
#(b|)#ar"
|
||||
},
|
||||
"a",
|
||||
indoc! {"\
|
||||
#[fo|]#o
|
||||
#(ba|)#r"
|
||||
},
|
||||
))
|
||||
.await?;
|
||||
|
||||
test((
|
||||
indoc! {"\
|
||||
#[f|]#oo
|
||||
#(b|)#ar"
|
||||
},
|
||||
"a<esc>",
|
||||
indoc! {"\
|
||||
#[f|]#oo
|
||||
#(b|)#ar"
|
||||
},
|
||||
))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensure the very initial cursor in an opened file is the width of
|
||||
/// the first grapheme
|
||||
#[tokio::test]
|
||||
async fn cursor_position_newly_opened_file() -> anyhow::Result<()> {
|
||||
let test = |content: &str, expected_sel: Selection| -> anyhow::Result<()> {
|
||||
let file = helpers::temp_file_with_contents(content)?;
|
||||
let mut app = helpers::app_with_file(file.path())?;
|
||||
|
||||
let (view, doc) = helix_view::current!(app.editor);
|
||||
let sel = doc.selection(view.id).clone();
|
||||
assert_eq!(expected_sel, sel);
|
||||
|
||||
Ok(())
|
||||
};
|
||||
|
||||
test("foo", Selection::single(0, 1))?;
|
||||
test("👨👩👧👦 foo", Selection::single(0, 7))?;
|
||||
test("", Selection::single(0, 0))?;
|
||||
|
||||
Ok(())
|
||||
}
|
@ -0,0 +1,169 @@
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
ops::RangeInclusive,
|
||||
};
|
||||
|
||||
use helix_core::diagnostic::Severity;
|
||||
use helix_term::application::Application;
|
||||
use helix_view::doc;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_write() -> anyhow::Result<()> {
|
||||
let mut file = tempfile::NamedTempFile::new()?;
|
||||
|
||||
test_key_sequence(
|
||||
&mut helpers::app_with_file(file.path())?,
|
||||
Some("ithe gostak distims the doshes<ret><esc>:w<ret>"),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
file.as_file_mut().flush()?;
|
||||
file.as_file_mut().sync_all()?;
|
||||
|
||||
let mut file_content = String::new();
|
||||
file.as_file_mut().read_to_string(&mut file_content)?;
|
||||
|
||||
assert_eq!(
|
||||
helpers::platform_line("the gostak distims the doshes"),
|
||||
file_content
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_write_quit() -> anyhow::Result<()> {
|
||||
let mut file = tempfile::NamedTempFile::new()?;
|
||||
|
||||
test_key_sequence(
|
||||
&mut helpers::app_with_file(file.path())?,
|
||||
Some("ithe gostak distims the doshes<ret><esc>:wq<ret>"),
|
||||
None,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
file.as_file_mut().flush()?;
|
||||
file.as_file_mut().sync_all()?;
|
||||
|
||||
let mut file_content = String::new();
|
||||
file.as_file_mut().read_to_string(&mut file_content)?;
|
||||
|
||||
assert_eq!(
|
||||
helpers::platform_line("the gostak distims the doshes"),
|
||||
file_content
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_write_concurrent() -> anyhow::Result<()> {
|
||||
let mut file = tempfile::NamedTempFile::new()?;
|
||||
let mut command = String::new();
|
||||
const RANGE: RangeInclusive<i32> = 1..=5000;
|
||||
|
||||
for i in RANGE {
|
||||
let cmd = format!("%c{}<esc>:w<ret>", i);
|
||||
command.push_str(&cmd);
|
||||
}
|
||||
|
||||
test_key_sequence(
|
||||
&mut helpers::app_with_file(file.path())?,
|
||||
Some(&command),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
file.as_file_mut().flush()?;
|
||||
file.as_file_mut().sync_all()?;
|
||||
|
||||
let mut file_content = String::new();
|
||||
file.as_file_mut().read_to_string(&mut file_content)?;
|
||||
assert_eq!(RANGE.end().to_string(), file_content);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_write_fail_mod_flag() -> anyhow::Result<()> {
|
||||
let file = helpers::new_readonly_tempfile()?;
|
||||
|
||||
test_key_sequences(
|
||||
&mut helpers::app_with_file(file.path())?,
|
||||
vec![
|
||||
(
|
||||
None,
|
||||
Some(&|app| {
|
||||
let doc = doc!(app.editor);
|
||||
assert!(!doc.is_modified());
|
||||
}),
|
||||
),
|
||||
(
|
||||
Some("ihello<esc>"),
|
||||
Some(&|app| {
|
||||
let doc = doc!(app.editor);
|
||||
assert!(doc.is_modified());
|
||||
}),
|
||||
),
|
||||
(
|
||||
Some(":w<ret>"),
|
||||
Some(&|app| {
|
||||
assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1);
|
||||
|
||||
let doc = doc!(app.editor);
|
||||
assert!(doc.is_modified());
|
||||
}),
|
||||
),
|
||||
],
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_write_fail_new_path() -> anyhow::Result<()> {
|
||||
let file = helpers::new_readonly_tempfile()?;
|
||||
|
||||
test_key_sequences(
|
||||
&mut Application::new(Args::default(), Config::default())?,
|
||||
vec![
|
||||
(
|
||||
None,
|
||||
Some(&|app| {
|
||||
let doc = doc!(app.editor);
|
||||
assert_ne!(
|
||||
Some(&Severity::Error),
|
||||
app.editor.get_status().map(|status| status.1)
|
||||
);
|
||||
assert_eq!(None, doc.path());
|
||||
}),
|
||||
),
|
||||
(
|
||||
Some(&format!(":w {}<ret>", file.path().to_string_lossy())),
|
||||
Some(&|app| {
|
||||
let doc = doc!(app.editor);
|
||||
assert_eq!(
|
||||
Some(&Severity::Error),
|
||||
app.editor.get_status().map(|status| status.1)
|
||||
);
|
||||
assert_eq!(None, doc.path());
|
||||
}),
|
||||
),
|
||||
],
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue