forked from Mirrors/helix
Merge branch 'master' of https://github.com/helix-editor/helix into refactor-tree-explorer
commit
70984fd148
@ -1,3 +0,0 @@
|
|||||||
[alias]
|
|
||||||
xtask = "run --package xtask --"
|
|
||||||
integration-test = "test --features integration --workspace --test integration"
|
|
@ -0,0 +1,3 @@
|
|||||||
|
[alias]
|
||||||
|
xtask = "run --package xtask --"
|
||||||
|
integration-test = "test --features integration --profile integration --workspace --test integration"
|
@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
name: Enhancement
|
||||||
|
about: Suggest an improvement
|
||||||
|
title: ''
|
||||||
|
labels: C-enhancement
|
||||||
|
assignees: ''
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Your enhancement may already be reported!
|
||||||
|
Please search on the issue tracker before creating a new issue.
|
||||||
|
If this is an idea for a feature, please open an "Idea" Discussion instead.
|
||||||
|
-->
|
@ -1,13 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest a new feature or improvement
|
|
||||||
title: ''
|
|
||||||
labels: C-enhancement
|
|
||||||
assignees: ''
|
|
||||||
---
|
|
||||||
|
|
||||||
<!-- Your feature may already be reported!
|
|
||||||
Please search on the issue tracker before creating one. -->
|
|
||||||
|
|
||||||
#### Describe your feature request
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
|||||||
[toolchain]
|
|
||||||
channel = "1.57.0"
|
|
||||||
components = ["rustfmt", "rust-src"]
|
|
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
|||||||
# Commands
|
# Commands
|
||||||
|
|
||||||
Command mode can be activated by pressing `:`, similar to vim. Built-in commands:
|
Command mode can be activated by pressing `:`, similar to Vim. Built-in commands:
|
||||||
|
|
||||||
{{#include ./generated/typable-cmd.md}}
|
{{#include ./generated/typable-cmd.md}}
|
||||||
|
@ -1 +0,0 @@
|
|||||||
# Hooks
|
|
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 |
@ -0,0 +1,31 @@
|
|||||||
|
- [x] "h" moves to parent instead of scrolling to left
|
||||||
|
- [x] "l" steps into current folder instead of scrolling to right
|
||||||
|
|
||||||
|
|
||||||
|
TODO
|
||||||
|
- [x] make focus current file works
|
||||||
|
- [x] test all explorer functionality (e.g. "/" etc)
|
||||||
|
- [x] Go to parent directory
|
||||||
|
- [x] add help
|
||||||
|
- [x] highlight ancestors of current selection
|
||||||
|
- [x] search "N" will hang
|
||||||
|
- [x] implement rename
|
||||||
|
- [x] implement close (refer how overlay works)
|
||||||
|
- [x] implement refresh
|
||||||
|
- [x] bug: delete file collapsed whole tree
|
||||||
|
- [x] update documentation
|
||||||
|
- [x] fix list view
|
||||||
|
- [x] -/+ to increase or decrease width
|
||||||
|
- [x] help page overflow
|
||||||
|
- [x] preview not showing in small screen
|
||||||
|
- [x] fix (-/+) overflow
|
||||||
|
- [x] improve filter UI/UX (should only apply to child not parent)
|
||||||
|
- [x] bug: "h" does not realign preview
|
||||||
|
- [x] bug: reveal file does not realign preview
|
||||||
|
- [] "l" goes back to previous child if any history
|
||||||
|
- [] refactor, add tree.expand_children() method
|
||||||
|
- [] search highlight matching word
|
||||||
|
- [] fix warnings
|
||||||
|
- [] Error didn't clear
|
||||||
|
- [] Remove comments
|
||||||
|
- [] Merge conflicts
|
@ -0,0 +1,87 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<component type="desktop-application">
|
||||||
|
<id>com.helix_editor.Helix</id>
|
||||||
|
<metadata_license>CC0-1.0</metadata_license>
|
||||||
|
<project_license>MPL-2.0</project_license>
|
||||||
|
<name>Helix</name>
|
||||||
|
<summary>A post-modern text editor</summary>
|
||||||
|
|
||||||
|
<description>
|
||||||
|
<p>
|
||||||
|
Helix is a terminal-based text editor inspired by Kakoune / Neovim and written in Rust.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Vim-like modal editing</li>
|
||||||
|
<li>Multiple selections</li>
|
||||||
|
<li>Built-in language server support</li>
|
||||||
|
<li>Smart, incremental syntax highlighting and code editing via tree-sitter</li>
|
||||||
|
</ul>
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<launchable type="desktop-id">Helix.desktop</launchable>
|
||||||
|
|
||||||
|
<screenshots>
|
||||||
|
<screenshot type="default">
|
||||||
|
<caption>Helix with default theme</caption>
|
||||||
|
<image>https://github.com/helix-editor/helix/raw/d4565b4404cabc522bd60822abd374755581d751/screenshot.png</image>
|
||||||
|
</screenshot>
|
||||||
|
</screenshots>
|
||||||
|
|
||||||
|
<url type="homepage">https://helix-editor.com/</url>
|
||||||
|
<url type="donation">https://opencollective.com/helix-editor</url>
|
||||||
|
<url type="help">https://docs.helix-editor.com/</url>
|
||||||
|
<url type="vcs-browser">https://github.com/helix-editor/helix</url>
|
||||||
|
<url type="bugtracker">https://github.com/helix-editor/helix/issues</url>
|
||||||
|
|
||||||
|
<content_rating type="oars-1.1" />
|
||||||
|
|
||||||
|
<releases>
|
||||||
|
<release version="22.12" date="2022-12-6">
|
||||||
|
<url>https://helix-editor.com/news/release-22-12-highlights/</url>
|
||||||
|
</release>
|
||||||
|
<release version="22.08" date="2022-8-31">
|
||||||
|
<url>https://helix-editor.com/news/release-22-08-highlights/</url>
|
||||||
|
</release>
|
||||||
|
<release version="22.05" date="2022-5-28">
|
||||||
|
<url>https://helix-editor.com/news/release-22-05-highlights/</url>
|
||||||
|
</release>
|
||||||
|
<release version="22.03" date="2022-3-28">
|
||||||
|
<url>https://helix-editor.com/news/release-22-03-highlights/</url>
|
||||||
|
</release>
|
||||||
|
</releases>
|
||||||
|
|
||||||
|
<requires>
|
||||||
|
<control>keyboard</control>
|
||||||
|
</requires>
|
||||||
|
|
||||||
|
<categories>
|
||||||
|
<category>Utility</category>
|
||||||
|
<category>TextEditor</category>
|
||||||
|
</categories>
|
||||||
|
|
||||||
|
<keywords>
|
||||||
|
<keyword>text</keyword>
|
||||||
|
<keyword>editor</keyword>
|
||||||
|
<keyword>development</keyword>
|
||||||
|
<keyword>programming</keyword>
|
||||||
|
</keywords>
|
||||||
|
|
||||||
|
<provides>
|
||||||
|
<binary>hx</binary>
|
||||||
|
<mediatype>text/english</mediatype>
|
||||||
|
<mediatype>text/plain</mediatype>
|
||||||
|
<mediatype>text/x-makefile</mediatype>
|
||||||
|
<mediatype>text/x-c++hdr</mediatype>
|
||||||
|
<mediatype>text/x-c++src</mediatype>
|
||||||
|
<mediatype>text/x-chdr</mediatype>
|
||||||
|
<mediatype>text/x-csrc</mediatype>
|
||||||
|
<mediatype>text/x-java</mediatype>
|
||||||
|
<mediatype>text/x-moc</mediatype>
|
||||||
|
<mediatype>text/x-pascal</mediatype>
|
||||||
|
<mediatype>text/x-tcl</mediatype>
|
||||||
|
<mediatype>text/x-tex</mediatype>
|
||||||
|
<mediatype>application/x-shellscript</mediatype>
|
||||||
|
<mediatype>text/x-c</mediatype>
|
||||||
|
<mediatype>text/x-c++</mediatype>
|
||||||
|
</provides>
|
||||||
|
</component>
|
Binary file not shown.
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 15 KiB |
@ -0,0 +1,384 @@
|
|||||||
|
//! The `DocumentFormatter` forms the bridge between the raw document text
|
||||||
|
//! and onscreen positioning. It yields the text graphemes as an iterator
|
||||||
|
//! and traverses (part) of the document text. During that traversal it
|
||||||
|
//! handles grapheme detection, softwrapping and annotations.
|
||||||
|
//! It yields `FormattedGrapheme`s and their corresponding visual coordinates.
|
||||||
|
//!
|
||||||
|
//! As both virtual text and softwrapping can insert additional lines into the document
|
||||||
|
//! it is generally not possible to find the start of the previous visual line.
|
||||||
|
//! Instead the `DocumentFormatter` starts at the last "checkpoint" (usually a linebreak)
|
||||||
|
//! called a "block" and the caller must advance it as needed.
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
use std::mem::{replace, take};
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test;
|
||||||
|
|
||||||
|
use unicode_segmentation::{Graphemes, UnicodeSegmentation};
|
||||||
|
|
||||||
|
use crate::graphemes::{Grapheme, GraphemeStr};
|
||||||
|
use crate::syntax::Highlight;
|
||||||
|
use crate::text_annotations::TextAnnotations;
|
||||||
|
use crate::{Position, RopeGraphemes, RopeSlice};
|
||||||
|
|
||||||
|
/// TODO make Highlight a u32 to reduce the size of this enum to a single word.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum GraphemeSource {
|
||||||
|
Document {
|
||||||
|
codepoints: u32,
|
||||||
|
},
|
||||||
|
/// Inline virtual text can not be highlighted with a `Highlight` iterator
|
||||||
|
/// because it's not part of the document. Instead the `Highlight`
|
||||||
|
/// is emitted right by the document formatter
|
||||||
|
VirtualText {
|
||||||
|
highlight: Option<Highlight>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct FormattedGrapheme<'a> {
|
||||||
|
pub grapheme: Grapheme<'a>,
|
||||||
|
pub source: GraphemeSource,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> FormattedGrapheme<'a> {
|
||||||
|
pub fn new(
|
||||||
|
g: GraphemeStr<'a>,
|
||||||
|
visual_x: usize,
|
||||||
|
tab_width: u16,
|
||||||
|
source: GraphemeSource,
|
||||||
|
) -> FormattedGrapheme<'a> {
|
||||||
|
FormattedGrapheme {
|
||||||
|
grapheme: Grapheme::new(g, visual_x, tab_width),
|
||||||
|
source,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Returns whether this grapheme is virtual inline text
|
||||||
|
pub fn is_virtual(&self) -> bool {
|
||||||
|
matches!(self.source, GraphemeSource::VirtualText { .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn placeholder() -> Self {
|
||||||
|
FormattedGrapheme {
|
||||||
|
grapheme: Grapheme::Other { g: " ".into() },
|
||||||
|
source: GraphemeSource::Document { codepoints: 0 },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn doc_chars(&self) -> usize {
|
||||||
|
match self.source {
|
||||||
|
GraphemeSource::Document { codepoints } => codepoints as usize,
|
||||||
|
GraphemeSource::VirtualText { .. } => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_whitespace(&self) -> bool {
|
||||||
|
self.grapheme.is_whitespace()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn width(&self) -> usize {
|
||||||
|
self.grapheme.width()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_word_boundary(&self) -> bool {
|
||||||
|
self.grapheme.is_word_boundary()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TextFormat {
|
||||||
|
pub soft_wrap: bool,
|
||||||
|
pub tab_width: u16,
|
||||||
|
pub max_wrap: u16,
|
||||||
|
pub max_indent_retain: u16,
|
||||||
|
pub wrap_indicator: Box<str>,
|
||||||
|
pub wrap_indicator_highlight: Option<Highlight>,
|
||||||
|
pub viewport_width: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
// test implementation is basically only used for testing or when softwrap is always disabled
|
||||||
|
impl Default for TextFormat {
|
||||||
|
fn default() -> Self {
|
||||||
|
TextFormat {
|
||||||
|
soft_wrap: false,
|
||||||
|
tab_width: 4,
|
||||||
|
max_wrap: 3,
|
||||||
|
max_indent_retain: 4,
|
||||||
|
wrap_indicator: Box::from(" "),
|
||||||
|
viewport_width: 17,
|
||||||
|
wrap_indicator_highlight: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DocumentFormatter<'t> {
|
||||||
|
text_fmt: &'t TextFormat,
|
||||||
|
annotations: &'t TextAnnotations,
|
||||||
|
|
||||||
|
/// The visual position at the end of the last yielded word boundary
|
||||||
|
visual_pos: Position,
|
||||||
|
graphemes: RopeGraphemes<'t>,
|
||||||
|
/// The character pos of the `graphemes` iter used for inserting annotations
|
||||||
|
char_pos: usize,
|
||||||
|
/// The line pos of the `graphemes` iter used for inserting annotations
|
||||||
|
line_pos: usize,
|
||||||
|
exhausted: bool,
|
||||||
|
|
||||||
|
/// Line breaks to be reserved for virtual text
|
||||||
|
/// at the next line break
|
||||||
|
virtual_lines: usize,
|
||||||
|
inline_anntoation_graphemes: Option<(Graphemes<'t>, Option<Highlight>)>,
|
||||||
|
|
||||||
|
// softwrap specific
|
||||||
|
/// The indentation of the current line
|
||||||
|
/// Is set to `None` if the indentation level is not yet known
|
||||||
|
/// because no non-whitespace graphemes have been encountered yet
|
||||||
|
indent_level: Option<usize>,
|
||||||
|
/// In case a long word needs to be split a single grapheme might need to be wrapped
|
||||||
|
/// while the rest of the word stays on the same line
|
||||||
|
peeked_grapheme: Option<(FormattedGrapheme<'t>, usize)>,
|
||||||
|
/// A first-in first-out (fifo) buffer for the Graphemes of any given word
|
||||||
|
word_buf: Vec<FormattedGrapheme<'t>>,
|
||||||
|
/// The index of the next grapheme that will be yielded from the `word_buf`
|
||||||
|
word_i: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'t> DocumentFormatter<'t> {
|
||||||
|
/// Creates a new formatter at the last block before `char_idx`.
|
||||||
|
/// A block is a chunk which always ends with a linebreak.
|
||||||
|
/// This is usually just a normal line break.
|
||||||
|
/// However very long lines are always wrapped at constant intervals that can be cheaply calculated
|
||||||
|
/// to avoid pathological behaviour.
|
||||||
|
pub fn new_at_prev_checkpoint(
|
||||||
|
text: RopeSlice<'t>,
|
||||||
|
text_fmt: &'t TextFormat,
|
||||||
|
annotations: &'t TextAnnotations,
|
||||||
|
char_idx: usize,
|
||||||
|
) -> (Self, usize) {
|
||||||
|
// TODO divide long lines into blocks to avoid bad performance for long lines
|
||||||
|
let block_line_idx = text.char_to_line(char_idx.min(text.len_chars()));
|
||||||
|
let block_char_idx = text.line_to_char(block_line_idx);
|
||||||
|
annotations.reset_pos(block_char_idx);
|
||||||
|
(
|
||||||
|
DocumentFormatter {
|
||||||
|
text_fmt,
|
||||||
|
annotations,
|
||||||
|
visual_pos: Position { row: 0, col: 0 },
|
||||||
|
graphemes: RopeGraphemes::new(text.slice(block_char_idx..)),
|
||||||
|
char_pos: block_char_idx,
|
||||||
|
exhausted: false,
|
||||||
|
virtual_lines: 0,
|
||||||
|
indent_level: None,
|
||||||
|
peeked_grapheme: None,
|
||||||
|
word_buf: Vec::with_capacity(64),
|
||||||
|
word_i: 0,
|
||||||
|
line_pos: block_line_idx,
|
||||||
|
inline_anntoation_graphemes: None,
|
||||||
|
},
|
||||||
|
block_char_idx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_inline_annotation_grapheme(&mut self) -> Option<(&'t str, Option<Highlight>)> {
|
||||||
|
loop {
|
||||||
|
if let Some(&mut (ref mut annotation, highlight)) =
|
||||||
|
self.inline_anntoation_graphemes.as_mut()
|
||||||
|
{
|
||||||
|
if let Some(grapheme) = annotation.next() {
|
||||||
|
return Some((grapheme, highlight));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((annotation, highlight)) =
|
||||||
|
self.annotations.next_inline_annotation_at(self.char_pos)
|
||||||
|
{
|
||||||
|
self.inline_anntoation_graphemes = Some((
|
||||||
|
UnicodeSegmentation::graphemes(&*annotation.text, true),
|
||||||
|
highlight,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn advance_grapheme(&mut self, col: usize) -> Option<FormattedGrapheme<'t>> {
|
||||||
|
let (grapheme, source) =
|
||||||
|
if let Some((grapheme, highlight)) = self.next_inline_annotation_grapheme() {
|
||||||
|
(grapheme.into(), GraphemeSource::VirtualText { highlight })
|
||||||
|
} else if let Some(grapheme) = self.graphemes.next() {
|
||||||
|
self.virtual_lines += self.annotations.annotation_lines_at(self.char_pos);
|
||||||
|
let codepoints = grapheme.len_chars() as u32;
|
||||||
|
|
||||||
|
let overlay = self.annotations.overlay_at(self.char_pos);
|
||||||
|
let grapheme = match overlay {
|
||||||
|
Some((overlay, _)) => overlay.grapheme.as_str().into(),
|
||||||
|
None => Cow::from(grapheme).into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.char_pos += codepoints as usize;
|
||||||
|
(grapheme, GraphemeSource::Document { codepoints })
|
||||||
|
} else {
|
||||||
|
if self.exhausted {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
self.exhausted = true;
|
||||||
|
// EOF grapheme is required for rendering
|
||||||
|
// and correct position computations
|
||||||
|
return Some(FormattedGrapheme {
|
||||||
|
grapheme: Grapheme::Other { g: " ".into() },
|
||||||
|
source: GraphemeSource::Document { codepoints: 0 },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
let grapheme = FormattedGrapheme::new(grapheme, col, self.text_fmt.tab_width, source);
|
||||||
|
|
||||||
|
Some(grapheme)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Move a word to the next visual line
|
||||||
|
fn wrap_word(&mut self, virtual_lines_before_word: usize) -> usize {
|
||||||
|
// softwrap this word to the next line
|
||||||
|
let indent_carry_over = if let Some(indent) = self.indent_level {
|
||||||
|
if indent as u16 <= self.text_fmt.max_indent_retain {
|
||||||
|
indent as u16
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// ensure the indent stays 0
|
||||||
|
self.indent_level = Some(0);
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
self.visual_pos.col = indent_carry_over as usize;
|
||||||
|
self.virtual_lines -= virtual_lines_before_word;
|
||||||
|
self.visual_pos.row += 1 + virtual_lines_before_word;
|
||||||
|
let mut i = 0;
|
||||||
|
let mut word_width = 0;
|
||||||
|
let wrap_indicator = UnicodeSegmentation::graphemes(&*self.text_fmt.wrap_indicator, true)
|
||||||
|
.map(|g| {
|
||||||
|
i += 1;
|
||||||
|
let grapheme = FormattedGrapheme::new(
|
||||||
|
g.into(),
|
||||||
|
self.visual_pos.col + word_width,
|
||||||
|
self.text_fmt.tab_width,
|
||||||
|
GraphemeSource::VirtualText {
|
||||||
|
highlight: self.text_fmt.wrap_indicator_highlight,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
word_width += grapheme.width();
|
||||||
|
grapheme
|
||||||
|
});
|
||||||
|
self.word_buf.splice(0..0, wrap_indicator);
|
||||||
|
|
||||||
|
for grapheme in &mut self.word_buf[i..] {
|
||||||
|
let visual_x = self.visual_pos.col + word_width;
|
||||||
|
grapheme
|
||||||
|
.grapheme
|
||||||
|
.change_position(visual_x, self.text_fmt.tab_width);
|
||||||
|
word_width += grapheme.width();
|
||||||
|
}
|
||||||
|
word_width
|
||||||
|
}
|
||||||
|
|
||||||
|
fn advance_to_next_word(&mut self) {
|
||||||
|
self.word_buf.clear();
|
||||||
|
let mut word_width = 0;
|
||||||
|
let virtual_lines_before_word = self.virtual_lines;
|
||||||
|
let mut virtual_lines_before_grapheme = self.virtual_lines;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// softwrap word if necessary
|
||||||
|
if word_width + self.visual_pos.col >= self.text_fmt.viewport_width as usize {
|
||||||
|
// wrapping this word would move too much text to the next line
|
||||||
|
// split the word at the line end instead
|
||||||
|
if word_width > self.text_fmt.max_wrap as usize {
|
||||||
|
// Usually we stop accomulating graphemes as soon as softwrapping becomes necessary.
|
||||||
|
// However if the last grapheme is multiple columns wide it might extend beyond the EOL.
|
||||||
|
// The condition below ensures that this grapheme is not cutoff and instead wrapped to the next line
|
||||||
|
if word_width + self.visual_pos.col > self.text_fmt.viewport_width as usize {
|
||||||
|
self.peeked_grapheme = self.word_buf.pop().map(|grapheme| {
|
||||||
|
(grapheme, self.virtual_lines - virtual_lines_before_grapheme)
|
||||||
|
});
|
||||||
|
self.virtual_lines = virtual_lines_before_grapheme;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
word_width = self.wrap_word(virtual_lines_before_word);
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual_lines_before_grapheme = self.virtual_lines;
|
||||||
|
|
||||||
|
let grapheme = if let Some((grapheme, virtual_lines)) = self.peeked_grapheme.take() {
|
||||||
|
self.virtual_lines += virtual_lines;
|
||||||
|
grapheme
|
||||||
|
} else if let Some(grapheme) = self.advance_grapheme(self.visual_pos.col + word_width) {
|
||||||
|
grapheme
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Track indentation
|
||||||
|
if !grapheme.is_whitespace() && self.indent_level.is_none() {
|
||||||
|
self.indent_level = Some(self.visual_pos.col);
|
||||||
|
} else if grapheme.grapheme == Grapheme::Newline {
|
||||||
|
self.indent_level = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_word_boundary = grapheme.is_word_boundary();
|
||||||
|
word_width += grapheme.width();
|
||||||
|
self.word_buf.push(grapheme);
|
||||||
|
|
||||||
|
if is_word_boundary {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns the document line pos of the **next** grapheme that will be yielded
|
||||||
|
pub fn line_pos(&self) -> usize {
|
||||||
|
self.line_pos
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns the visual pos of the **next** grapheme that will be yielded
|
||||||
|
pub fn visual_pos(&self) -> Position {
|
||||||
|
self.visual_pos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'t> Iterator for DocumentFormatter<'t> {
|
||||||
|
type Item = (FormattedGrapheme<'t>, Position);
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
let grapheme = if self.text_fmt.soft_wrap {
|
||||||
|
if self.word_i >= self.word_buf.len() {
|
||||||
|
self.advance_to_next_word();
|
||||||
|
self.word_i = 0;
|
||||||
|
}
|
||||||
|
let grapheme = replace(
|
||||||
|
self.word_buf.get_mut(self.word_i)?,
|
||||||
|
FormattedGrapheme::placeholder(),
|
||||||
|
);
|
||||||
|
self.word_i += 1;
|
||||||
|
grapheme
|
||||||
|
} else {
|
||||||
|
self.advance_grapheme(self.visual_pos.col)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let pos = self.visual_pos;
|
||||||
|
if grapheme.grapheme == Grapheme::Newline {
|
||||||
|
self.visual_pos.row += 1;
|
||||||
|
self.visual_pos.row += take(&mut self.virtual_lines);
|
||||||
|
self.visual_pos.col = 0;
|
||||||
|
self.line_pos += 1;
|
||||||
|
} else {
|
||||||
|
self.visual_pos.col += grapheme.width();
|
||||||
|
}
|
||||||
|
Some((grapheme, pos))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,222 @@
|
|||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use crate::doc_formatter::{DocumentFormatter, TextFormat};
|
||||||
|
use crate::text_annotations::{InlineAnnotation, Overlay, TextAnnotations};
|
||||||
|
|
||||||
|
impl TextFormat {
|
||||||
|
fn new_test(softwrap: bool) -> Self {
|
||||||
|
TextFormat {
|
||||||
|
soft_wrap: softwrap,
|
||||||
|
tab_width: 2,
|
||||||
|
max_wrap: 3,
|
||||||
|
max_indent_retain: 4,
|
||||||
|
wrap_indicator: ".".into(),
|
||||||
|
wrap_indicator_highlight: None,
|
||||||
|
// use a prime number to allow lining up too often with repeat
|
||||||
|
viewport_width: 17,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'t> DocumentFormatter<'t> {
|
||||||
|
fn collect_to_str(&mut self) -> String {
|
||||||
|
use std::fmt::Write;
|
||||||
|
let mut res = String::new();
|
||||||
|
let viewport_width = self.text_fmt.viewport_width;
|
||||||
|
let mut line = 0;
|
||||||
|
|
||||||
|
for (grapheme, pos) in self {
|
||||||
|
if pos.row != line {
|
||||||
|
line += 1;
|
||||||
|
assert_eq!(pos.row, line);
|
||||||
|
write!(res, "\n{}", ".".repeat(pos.col)).unwrap();
|
||||||
|
assert!(
|
||||||
|
pos.col <= viewport_width as usize,
|
||||||
|
"softwrapped failed {}<={viewport_width}",
|
||||||
|
pos.col
|
||||||
|
);
|
||||||
|
}
|
||||||
|
write!(res, "{}", grapheme.grapheme).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn softwrap_text(text: &str) -> String {
|
||||||
|
DocumentFormatter::new_at_prev_checkpoint(
|
||||||
|
text.into(),
|
||||||
|
&TextFormat::new_test(true),
|
||||||
|
&TextAnnotations::default(),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.0
|
||||||
|
.collect_to_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn basic_softwrap() {
|
||||||
|
assert_eq!(
|
||||||
|
softwrap_text(&"foo ".repeat(10)),
|
||||||
|
"foo foo foo foo \n.foo foo foo foo \n.foo foo "
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
softwrap_text(&"fooo ".repeat(10)),
|
||||||
|
"fooo fooo fooo \n.fooo fooo fooo \n.fooo fooo fooo \n.fooo "
|
||||||
|
);
|
||||||
|
|
||||||
|
// check that we don't wrap unnecessarily
|
||||||
|
assert_eq!(softwrap_text("\t\txxxx1xxxx2xx\n"), " xxxx1xxxx2xx \n ");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn softwrap_indentation() {
|
||||||
|
assert_eq!(
|
||||||
|
softwrap_text("\t\tfoo1 foo2 foo3 foo4 foo5 foo6\n"),
|
||||||
|
" foo1 foo2 \n.....foo3 foo4 \n.....foo5 foo6 \n "
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
softwrap_text("\t\t\tfoo1 foo2 foo3 foo4 foo5 foo6\n"),
|
||||||
|
" foo1 foo2 \n.foo3 foo4 foo5 \n.foo6 \n "
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn long_word_softwrap() {
|
||||||
|
assert_eq!(
|
||||||
|
softwrap_text("\t\txxxx1xxxx2xxxx3xxxx4xxxx5xxxx6xxxx7xxxx8xxxx9xxx\n"),
|
||||||
|
" xxxx1xxxx2xxx\n.....x3xxxx4xxxx5\n.....xxxx6xxxx7xx\n.....xx8xxxx9xxx \n "
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
softwrap_text("xxxxxxxx1xxxx2xxx\n"),
|
||||||
|
"xxxxxxxx1xxxx2xxx\n. \n "
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
softwrap_text("\t\txxxx1xxxx 2xxxx3xxxx4xxxx5xxxx6xxxx7xxxx8xxxx9xxx\n"),
|
||||||
|
" xxxx1xxxx \n.....2xxxx3xxxx4x\n.....xxx5xxxx6xxx\n.....x7xxxx8xxxx9\n.....xxx \n "
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
softwrap_text("\t\txxxx1xxx 2xxxx3xxxx4xxxx5xxxx6xxxx7xxxx8xxxx9xxx\n"),
|
||||||
|
" xxxx1xxx 2xxx\n.....x3xxxx4xxxx5\n.....xxxx6xxxx7xx\n.....xx8xxxx9xxx \n "
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn overlay_text(text: &str, char_pos: usize, softwrap: bool, overlays: &[Overlay]) -> String {
|
||||||
|
DocumentFormatter::new_at_prev_checkpoint(
|
||||||
|
text.into(),
|
||||||
|
&TextFormat::new_test(softwrap),
|
||||||
|
TextAnnotations::default().add_overlay(overlays.into(), None),
|
||||||
|
char_pos,
|
||||||
|
)
|
||||||
|
.0
|
||||||
|
.collect_to_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn overlay() {
|
||||||
|
assert_eq!(
|
||||||
|
overlay_text(
|
||||||
|
"foobar",
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
&[
|
||||||
|
Overlay {
|
||||||
|
char_idx: 0,
|
||||||
|
grapheme: "X".into(),
|
||||||
|
},
|
||||||
|
Overlay {
|
||||||
|
char_idx: 2,
|
||||||
|
grapheme: "\t".into(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
),
|
||||||
|
"Xo bar "
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
overlay_text(
|
||||||
|
&"foo ".repeat(10),
|
||||||
|
0,
|
||||||
|
true,
|
||||||
|
&[
|
||||||
|
Overlay {
|
||||||
|
char_idx: 2,
|
||||||
|
grapheme: "\t".into(),
|
||||||
|
},
|
||||||
|
Overlay {
|
||||||
|
char_idx: 5,
|
||||||
|
grapheme: "\t".into(),
|
||||||
|
},
|
||||||
|
Overlay {
|
||||||
|
char_idx: 16,
|
||||||
|
grapheme: "X".into(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
),
|
||||||
|
"fo f o foo \n.foo Xoo foo foo \n.foo foo foo "
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn annotate_text(text: &str, softwrap: bool, annotations: &[InlineAnnotation]) -> String {
|
||||||
|
DocumentFormatter::new_at_prev_checkpoint(
|
||||||
|
text.into(),
|
||||||
|
&TextFormat::new_test(softwrap),
|
||||||
|
TextAnnotations::default().add_inline_annotations(annotations.into(), None),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.0
|
||||||
|
.collect_to_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn annotation() {
|
||||||
|
assert_eq!(
|
||||||
|
annotate_text(
|
||||||
|
"bar",
|
||||||
|
false,
|
||||||
|
&[InlineAnnotation {
|
||||||
|
char_idx: 0,
|
||||||
|
text: "foo".into(),
|
||||||
|
}]
|
||||||
|
),
|
||||||
|
"foobar "
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
annotate_text(
|
||||||
|
&"foo ".repeat(10),
|
||||||
|
true,
|
||||||
|
&[InlineAnnotation {
|
||||||
|
char_idx: 0,
|
||||||
|
text: "foo ".into(),
|
||||||
|
}]
|
||||||
|
),
|
||||||
|
"foo foo foo foo \n.foo foo foo foo \n.foo foo foo "
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn annotation_and_overlay() {
|
||||||
|
assert_eq!(
|
||||||
|
DocumentFormatter::new_at_prev_checkpoint(
|
||||||
|
"bbar".into(),
|
||||||
|
&TextFormat::new_test(false),
|
||||||
|
TextAnnotations::default()
|
||||||
|
.add_inline_annotations(
|
||||||
|
Rc::new([InlineAnnotation {
|
||||||
|
char_idx: 0,
|
||||||
|
text: "fooo".into(),
|
||||||
|
}]),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
.add_overlay(
|
||||||
|
Rc::new([Overlay {
|
||||||
|
char_idx: 0,
|
||||||
|
grapheme: "\t".into(),
|
||||||
|
}]),
|
||||||
|
None
|
||||||
|
),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.0
|
||||||
|
.collect_to_str(),
|
||||||
|
"fooo bar "
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,235 @@
|
|||||||
|
const SEPARATOR: char = '_';
|
||||||
|
|
||||||
|
/// Increment an integer.
|
||||||
|
///
|
||||||
|
/// Supported bases:
|
||||||
|
/// 2 with prefix 0b
|
||||||
|
/// 8 with prefix 0o
|
||||||
|
/// 10 with no prefix
|
||||||
|
/// 16 with prefix 0x
|
||||||
|
///
|
||||||
|
/// An integer can contain `_` as a separator but may not start or end with a separator.
|
||||||
|
/// Base 10 integers can go negative, but bases 2, 8, and 16 cannot.
|
||||||
|
/// All addition and subtraction is saturating.
|
||||||
|
pub fn increment(selected_text: &str, amount: i64) -> Option<String> {
|
||||||
|
if selected_text.is_empty()
|
||||||
|
|| selected_text.ends_with(SEPARATOR)
|
||||||
|
|| selected_text.starts_with(SEPARATOR)
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let radix = if selected_text.starts_with("0x") {
|
||||||
|
16
|
||||||
|
} else if selected_text.starts_with("0o") {
|
||||||
|
8
|
||||||
|
} else if selected_text.starts_with("0b") {
|
||||||
|
2
|
||||||
|
} else {
|
||||||
|
10
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get separator indexes from right to left.
|
||||||
|
let separator_rtl_indexes: Vec<usize> = selected_text
|
||||||
|
.chars()
|
||||||
|
.rev()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(i, c)| if c == SEPARATOR { Some(i) } else { None })
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let word: String = selected_text.chars().filter(|&c| c != SEPARATOR).collect();
|
||||||
|
|
||||||
|
let mut new_text = if radix == 10 {
|
||||||
|
let number = &word;
|
||||||
|
let value = i128::from_str_radix(number, radix).ok()?;
|
||||||
|
let new_value = value.saturating_add(amount as i128);
|
||||||
|
|
||||||
|
let format_length = match (value.is_negative(), new_value.is_negative()) {
|
||||||
|
(true, false) => number.len() - 1,
|
||||||
|
(false, true) => number.len() + 1,
|
||||||
|
_ => number.len(),
|
||||||
|
} - separator_rtl_indexes.len();
|
||||||
|
|
||||||
|
if number.starts_with('0') || number.starts_with("-0") {
|
||||||
|
format!("{:01$}", new_value, format_length)
|
||||||
|
} else {
|
||||||
|
format!("{}", new_value)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let number = &word[2..];
|
||||||
|
let value = u128::from_str_radix(number, radix).ok()?;
|
||||||
|
let new_value = (value as i128).saturating_add(amount as i128);
|
||||||
|
let new_value = if new_value < 0 { 0 } else { new_value };
|
||||||
|
let format_length = selected_text.len() - 2 - separator_rtl_indexes.len();
|
||||||
|
|
||||||
|
match radix {
|
||||||
|
2 => format!("0b{:01$b}", new_value, format_length),
|
||||||
|
8 => format!("0o{:01$o}", new_value, format_length),
|
||||||
|
16 => {
|
||||||
|
let (lower_count, upper_count): (usize, usize) =
|
||||||
|
number.chars().fold((0, 0), |(lower, upper), c| {
|
||||||
|
(
|
||||||
|
lower + c.is_ascii_lowercase() as usize,
|
||||||
|
upper + c.is_ascii_uppercase() as usize,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
if upper_count > lower_count {
|
||||||
|
format!("0x{:01$X}", new_value, format_length)
|
||||||
|
} else {
|
||||||
|
format!("0x{:01$x}", new_value, format_length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => unimplemented!("radix not supported: {}", radix),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add separators from original number.
|
||||||
|
for &rtl_index in &separator_rtl_indexes {
|
||||||
|
if rtl_index < new_text.len() {
|
||||||
|
let new_index = new_text.len().saturating_sub(rtl_index);
|
||||||
|
if new_index > 0 {
|
||||||
|
new_text.insert(new_index, SEPARATOR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add in additional separators if necessary.
|
||||||
|
if new_text.len() > selected_text.len() && !separator_rtl_indexes.is_empty() {
|
||||||
|
let spacing = match separator_rtl_indexes.as_slice() {
|
||||||
|
[.., b, a] => a - b - 1,
|
||||||
|
_ => separator_rtl_indexes[0],
|
||||||
|
};
|
||||||
|
|
||||||
|
let prefix_length = if radix == 10 { 0 } else { 2 };
|
||||||
|
if let Some(mut index) = new_text.find(SEPARATOR) {
|
||||||
|
while index - prefix_length > spacing {
|
||||||
|
index -= spacing;
|
||||||
|
new_text.insert(index, SEPARATOR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(new_text)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_increment_basic_decimal_numbers() {
|
||||||
|
let tests = [
|
||||||
|
("100", 1, "101"),
|
||||||
|
("100", -1, "99"),
|
||||||
|
("99", 1, "100"),
|
||||||
|
("100", 1000, "1100"),
|
||||||
|
("100", -1000, "-900"),
|
||||||
|
("-1", 1, "0"),
|
||||||
|
("-1", 2, "1"),
|
||||||
|
("1", -1, "0"),
|
||||||
|
("1", -2, "-1"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (original, amount, expected) in tests {
|
||||||
|
assert_eq!(increment(original, amount).unwrap(), expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_increment_basic_hexadecimal_numbers() {
|
||||||
|
let tests = [
|
||||||
|
("0x0100", 1, "0x0101"),
|
||||||
|
("0x0100", -1, "0x00ff"),
|
||||||
|
("0x0001", -1, "0x0000"),
|
||||||
|
("0x0000", -1, "0x0000"),
|
||||||
|
("0xffffffffffffffff", 1, "0x10000000000000000"),
|
||||||
|
("0xffffffffffffffff", 2, "0x10000000000000001"),
|
||||||
|
("0xffffffffffffffff", -1, "0xfffffffffffffffe"),
|
||||||
|
("0xABCDEF1234567890", 1, "0xABCDEF1234567891"),
|
||||||
|
("0xabcdef1234567890", 1, "0xabcdef1234567891"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (original, amount, expected) in tests {
|
||||||
|
assert_eq!(increment(original, amount).unwrap(), expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_increment_basic_octal_numbers() {
|
||||||
|
let tests = [
|
||||||
|
("0o0107", 1, "0o0110"),
|
||||||
|
("0o0110", -1, "0o0107"),
|
||||||
|
("0o0001", -1, "0o0000"),
|
||||||
|
("0o7777", 1, "0o10000"),
|
||||||
|
("0o1000", -1, "0o0777"),
|
||||||
|
("0o0107", 10, "0o0121"),
|
||||||
|
("0o0000", -1, "0o0000"),
|
||||||
|
("0o1777777777777777777777", 1, "0o2000000000000000000000"),
|
||||||
|
("0o1777777777777777777777", 2, "0o2000000000000000000001"),
|
||||||
|
("0o1777777777777777777777", -1, "0o1777777777777777777776"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (original, amount, expected) in tests {
|
||||||
|
assert_eq!(increment(original, amount).unwrap(), expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_increment_basic_binary_numbers() {
|
||||||
|
let tests = [
|
||||||
|
("0b00000100", 1, "0b00000101"),
|
||||||
|
("0b00000100", -1, "0b00000011"),
|
||||||
|
("0b00000100", 2, "0b00000110"),
|
||||||
|
("0b00000100", -2, "0b00000010"),
|
||||||
|
("0b00000001", -1, "0b00000000"),
|
||||||
|
("0b00111111", 10, "0b01001001"),
|
||||||
|
("0b11111111", 1, "0b100000000"),
|
||||||
|
("0b10000000", -1, "0b01111111"),
|
||||||
|
("0b0000", -1, "0b0000"),
|
||||||
|
(
|
||||||
|
"0b1111111111111111111111111111111111111111111111111111111111111111",
|
||||||
|
1,
|
||||||
|
"0b10000000000000000000000000000000000000000000000000000000000000000",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"0b1111111111111111111111111111111111111111111111111111111111111111",
|
||||||
|
2,
|
||||||
|
"0b10000000000000000000000000000000000000000000000000000000000000001",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"0b1111111111111111111111111111111111111111111111111111111111111111",
|
||||||
|
-1,
|
||||||
|
"0b1111111111111111111111111111111111111111111111111111111111111110",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (original, amount, expected) in tests {
|
||||||
|
assert_eq!(increment(original, amount).unwrap(), expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_increment_with_separators() {
|
||||||
|
let tests = [
|
||||||
|
("999_999", 1, "1_000_000"),
|
||||||
|
("1_000_000", -1, "999_999"),
|
||||||
|
("-999_999", -1, "-1_000_000"),
|
||||||
|
("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"),
|
||||||
|
("0x0000_0000", -1, "0x0000_0000"),
|
||||||
|
("0x0000_0000_0000", -1, "0x0000_0000_0000"),
|
||||||
|
("0b01111111_11111111", 1, "0b10000000_00000000"),
|
||||||
|
("0b11111111_11111111", 1, "0b1_00000000_00000000"),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (original, amount, expected) in tests {
|
||||||
|
assert_eq!(increment(original, amount).unwrap(), expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_leading_and_trailing_separators_arent_a_match() {
|
||||||
|
assert_eq!(increment("9_", 1), None);
|
||||||
|
assert_eq!(increment("_9", 1), None);
|
||||||
|
assert_eq!(increment("_9_", 1), None);
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,10 @@
|
|||||||
pub mod date_time;
|
mod date_time;
|
||||||
pub mod number;
|
mod integer;
|
||||||
|
|
||||||
use crate::{Range, Tendril};
|
pub fn integer(selected_text: &str, amount: i64) -> Option<String> {
|
||||||
|
integer::increment(selected_text, amount)
|
||||||
|
}
|
||||||
|
|
||||||
pub trait Increment {
|
pub fn date_time(selected_text: &str, amount: i64) -> Option<String> {
|
||||||
fn increment(&self, amount: i64) -> (Range, Tendril);
|
date_time::increment(selected_text, amount)
|
||||||
}
|
}
|
||||||
|
@ -1,507 +0,0 @@
|
|||||||
use std::borrow::Cow;
|
|
||||||
|
|
||||||
use ropey::RopeSlice;
|
|
||||||
|
|
||||||
use super::Increment;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
textobject::{textobject_word, TextObject},
|
|
||||||
Range, Tendril,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
pub struct NumberIncrementor<'a> {
|
|
||||||
value: i64,
|
|
||||||
radix: u32,
|
|
||||||
range: Range,
|
|
||||||
|
|
||||||
text: RopeSlice<'a>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> NumberIncrementor<'a> {
|
|
||||||
/// Return information about number under rang if there is one.
|
|
||||||
pub fn from_range(text: RopeSlice, range: Range) -> Option<NumberIncrementor> {
|
|
||||||
// If the cursor is on the minus sign of a number we want to get the word textobject to the
|
|
||||||
// right of it.
|
|
||||||
let range = if range.to() < text.len_chars()
|
|
||||||
&& range.to() - range.from() <= 1
|
|
||||||
&& text.char(range.from()) == '-'
|
|
||||||
{
|
|
||||||
Range::new(range.from() + 1, range.to() + 1)
|
|
||||||
} else {
|
|
||||||
range
|
|
||||||
};
|
|
||||||
|
|
||||||
let range = textobject_word(text, range, TextObject::Inside, 1, false);
|
|
||||||
|
|
||||||
// If there is a minus sign to the left of the word object, we want to include it in the range.
|
|
||||||
let range = if range.from() > 0 && text.char(range.from() - 1) == '-' {
|
|
||||||
range.extend(range.from() - 1, range.from())
|
|
||||||
} else {
|
|
||||||
range
|
|
||||||
};
|
|
||||||
|
|
||||||
let word: String = text
|
|
||||||
.slice(range.from()..range.to())
|
|
||||||
.chars()
|
|
||||||
.filter(|&c| c != '_')
|
|
||||||
.collect();
|
|
||||||
let (radix, prefixed) = if word.starts_with("0x") {
|
|
||||||
(16, true)
|
|
||||||
} else if word.starts_with("0o") {
|
|
||||||
(8, true)
|
|
||||||
} else if word.starts_with("0b") {
|
|
||||||
(2, true)
|
|
||||||
} else {
|
|
||||||
(10, false)
|
|
||||||
};
|
|
||||||
|
|
||||||
let number = if prefixed { &word[2..] } else { &word };
|
|
||||||
|
|
||||||
let value = i128::from_str_radix(number, radix).ok()?;
|
|
||||||
if (value.is_positive() && value.leading_zeros() < 64)
|
|
||||||
|| (value.is_negative() && value.leading_ones() < 64)
|
|
||||||
{
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let value = value as i64;
|
|
||||||
Some(NumberIncrementor {
|
|
||||||
range,
|
|
||||||
value,
|
|
||||||
radix,
|
|
||||||
text,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Increment for NumberIncrementor<'a> {
|
|
||||||
fn increment(&self, amount: i64) -> (Range, Tendril) {
|
|
||||||
let old_text: Cow<str> = self.text.slice(self.range.from()..self.range.to()).into();
|
|
||||||
let old_length = old_text.len();
|
|
||||||
let new_value = self.value.wrapping_add(amount);
|
|
||||||
|
|
||||||
// Get separator indexes from right to left.
|
|
||||||
let separator_rtl_indexes: Vec<usize> = old_text
|
|
||||||
.chars()
|
|
||||||
.rev()
|
|
||||||
.enumerate()
|
|
||||||
.filter_map(|(i, c)| if c == '_' { Some(i) } else { None })
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let format_length = if self.radix == 10 {
|
|
||||||
match (self.value.is_negative(), new_value.is_negative()) {
|
|
||||||
(true, false) => old_length - 1,
|
|
||||||
(false, true) => old_length + 1,
|
|
||||||
_ => old_text.len(),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
old_text.len() - 2
|
|
||||||
} - separator_rtl_indexes.len();
|
|
||||||
|
|
||||||
let mut new_text = match self.radix {
|
|
||||||
2 => format!("0b{:01$b}", new_value, format_length),
|
|
||||||
8 => format!("0o{:01$o}", new_value, format_length),
|
|
||||||
10 if old_text.starts_with('0') || old_text.starts_with("-0") => {
|
|
||||||
format!("{:01$}", new_value, format_length)
|
|
||||||
}
|
|
||||||
10 => format!("{}", new_value),
|
|
||||||
16 => {
|
|
||||||
let (lower_count, upper_count): (usize, usize) =
|
|
||||||
old_text.chars().skip(2).fold((0, 0), |(lower, upper), c| {
|
|
||||||
(
|
|
||||||
lower + c.is_ascii_lowercase().then(|| 1).unwrap_or(0),
|
|
||||||
upper + c.is_ascii_uppercase().then(|| 1).unwrap_or(0),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
if upper_count > lower_count {
|
|
||||||
format!("0x{:01$X}", new_value, format_length)
|
|
||||||
} else {
|
|
||||||
format!("0x{:01$x}", new_value, format_length)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => unimplemented!("radix not supported: {}", self.radix),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add separators from original number.
|
|
||||||
for &rtl_index in &separator_rtl_indexes {
|
|
||||||
if rtl_index < new_text.len() {
|
|
||||||
let new_index = new_text.len() - rtl_index;
|
|
||||||
new_text.insert(new_index, '_');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add in additional separators if necessary.
|
|
||||||
if new_text.len() > old_length && !separator_rtl_indexes.is_empty() {
|
|
||||||
let spacing = match separator_rtl_indexes.as_slice() {
|
|
||||||
[.., b, a] => a - b - 1,
|
|
||||||
_ => separator_rtl_indexes[0],
|
|
||||||
};
|
|
||||||
|
|
||||||
let prefix_length = if self.radix == 10 { 0 } else { 2 };
|
|
||||||
if let Some(mut index) = new_text.find('_') {
|
|
||||||
while index - prefix_length > spacing {
|
|
||||||
index -= spacing;
|
|
||||||
new_text.insert(index, '_');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(self.range, new_text.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
use crate::Rope;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_decimal_at_point() {
|
|
||||||
let rope = Rope::from_str("Test text 12345 more text.");
|
|
||||||
let range = Range::point(12);
|
|
||||||
assert_eq!(
|
|
||||||
NumberIncrementor::from_range(rope.slice(..), range),
|
|
||||||
Some(NumberIncrementor {
|
|
||||||
range: Range::new(10, 15),
|
|
||||||
value: 12345,
|
|
||||||
radix: 10,
|
|
||||||
text: rope.slice(..),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_uppercase_hexadecimal_at_point() {
|
|
||||||
let rope = Rope::from_str("Test text 0x123ABCDEF more text.");
|
|
||||||
let range = Range::point(12);
|
|
||||||
assert_eq!(
|
|
||||||
NumberIncrementor::from_range(rope.slice(..), range),
|
|
||||||
Some(NumberIncrementor {
|
|
||||||
range: Range::new(10, 21),
|
|
||||||
value: 0x123ABCDEF,
|
|
||||||
radix: 16,
|
|
||||||
text: rope.slice(..),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_lowercase_hexadecimal_at_point() {
|
|
||||||
let rope = Rope::from_str("Test text 0xfa3b4e more text.");
|
|
||||||
let range = Range::point(12);
|
|
||||||
assert_eq!(
|
|
||||||
NumberIncrementor::from_range(rope.slice(..), range),
|
|
||||||
Some(NumberIncrementor {
|
|
||||||
range: Range::new(10, 18),
|
|
||||||
value: 0xfa3b4e,
|
|
||||||
radix: 16,
|
|
||||||
text: rope.slice(..),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_octal_at_point() {
|
|
||||||
let rope = Rope::from_str("Test text 0o1074312 more text.");
|
|
||||||
let range = Range::point(12);
|
|
||||||
assert_eq!(
|
|
||||||
NumberIncrementor::from_range(rope.slice(..), range),
|
|
||||||
Some(NumberIncrementor {
|
|
||||||
range: Range::new(10, 19),
|
|
||||||
value: 0o1074312,
|
|
||||||
radix: 8,
|
|
||||||
text: rope.slice(..),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_binary_at_point() {
|
|
||||||
let rope = Rope::from_str("Test text 0b10111010010101 more text.");
|
|
||||||
let range = Range::point(12);
|
|
||||||
assert_eq!(
|
|
||||||
NumberIncrementor::from_range(rope.slice(..), range),
|
|
||||||
Some(NumberIncrementor {
|
|
||||||
range: Range::new(10, 26),
|
|
||||||
value: 0b10111010010101,
|
|
||||||
radix: 2,
|
|
||||||
text: rope.slice(..),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_negative_decimal_at_point() {
|
|
||||||
let rope = Rope::from_str("Test text -54321 more text.");
|
|
||||||
let range = Range::point(12);
|
|
||||||
assert_eq!(
|
|
||||||
NumberIncrementor::from_range(rope.slice(..), range),
|
|
||||||
Some(NumberIncrementor {
|
|
||||||
range: Range::new(10, 16),
|
|
||||||
value: -54321,
|
|
||||||
radix: 10,
|
|
||||||
text: rope.slice(..),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_decimal_with_leading_zeroes_at_point() {
|
|
||||||
let rope = Rope::from_str("Test text 000045326 more text.");
|
|
||||||
let range = Range::point(12);
|
|
||||||
assert_eq!(
|
|
||||||
NumberIncrementor::from_range(rope.slice(..), range),
|
|
||||||
Some(NumberIncrementor {
|
|
||||||
range: Range::new(10, 19),
|
|
||||||
value: 45326,
|
|
||||||
radix: 10,
|
|
||||||
text: rope.slice(..),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_negative_decimal_cursor_on_minus_sign() {
|
|
||||||
let rope = Rope::from_str("Test text -54321 more text.");
|
|
||||||
let range = Range::point(10);
|
|
||||||
assert_eq!(
|
|
||||||
NumberIncrementor::from_range(rope.slice(..), range),
|
|
||||||
Some(NumberIncrementor {
|
|
||||||
range: Range::new(10, 16),
|
|
||||||
value: -54321,
|
|
||||||
radix: 10,
|
|
||||||
text: rope.slice(..),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_number_under_range_start_of_rope() {
|
|
||||||
let rope = Rope::from_str("100");
|
|
||||||
let range = Range::point(0);
|
|
||||||
assert_eq!(
|
|
||||||
NumberIncrementor::from_range(rope.slice(..), range),
|
|
||||||
Some(NumberIncrementor {
|
|
||||||
range: Range::new(0, 3),
|
|
||||||
value: 100,
|
|
||||||
radix: 10,
|
|
||||||
text: rope.slice(..),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_number_under_range_end_of_rope() {
|
|
||||||
let rope = Rope::from_str("100");
|
|
||||||
let range = Range::point(2);
|
|
||||||
assert_eq!(
|
|
||||||
NumberIncrementor::from_range(rope.slice(..), range),
|
|
||||||
Some(NumberIncrementor {
|
|
||||||
range: Range::new(0, 3),
|
|
||||||
value: 100,
|
|
||||||
radix: 10,
|
|
||||||
text: rope.slice(..),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_number_surrounded_by_punctuation() {
|
|
||||||
let rope = Rope::from_str(",100;");
|
|
||||||
let range = Range::point(1);
|
|
||||||
assert_eq!(
|
|
||||||
NumberIncrementor::from_range(rope.slice(..), range),
|
|
||||||
Some(NumberIncrementor {
|
|
||||||
range: Range::new(1, 4),
|
|
||||||
value: 100,
|
|
||||||
radix: 10,
|
|
||||||
text: rope.slice(..),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_not_a_number_point() {
|
|
||||||
let rope = Rope::from_str("Test text 45326 more text.");
|
|
||||||
let range = Range::point(6);
|
|
||||||
assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_number_too_large_at_point() {
|
|
||||||
let rope = Rope::from_str("Test text 0xFFFFFFFFFFFFFFFFF more text.");
|
|
||||||
let range = Range::point(12);
|
|
||||||
assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_number_cursor_one_right_of_number() {
|
|
||||||
let rope = Rope::from_str("100 ");
|
|
||||||
let range = Range::point(3);
|
|
||||||
assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_number_cursor_one_left_of_number() {
|
|
||||||
let rope = Rope::from_str(" 100");
|
|
||||||
let range = Range::point(0);
|
|
||||||
assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_increment_basic_decimal_numbers() {
|
|
||||||
let tests = [
|
|
||||||
("100", 1, "101"),
|
|
||||||
("100", -1, "99"),
|
|
||||||
("99", 1, "100"),
|
|
||||||
("100", 1000, "1100"),
|
|
||||||
("100", -1000, "-900"),
|
|
||||||
("-1", 1, "0"),
|
|
||||||
("-1", 2, "1"),
|
|
||||||
("1", -1, "0"),
|
|
||||||
("1", -2, "-1"),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (original, amount, expected) in tests {
|
|
||||||
let rope = Rope::from_str(original);
|
|
||||||
let range = Range::point(0);
|
|
||||||
assert_eq!(
|
|
||||||
NumberIncrementor::from_range(rope.slice(..), range)
|
|
||||||
.unwrap()
|
|
||||||
.increment(amount)
|
|
||||||
.1,
|
|
||||||
Tendril::from(expected)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_increment_basic_hexadecimal_numbers() {
|
|
||||||
let tests = [
|
|
||||||
("0x0100", 1, "0x0101"),
|
|
||||||
("0x0100", -1, "0x00ff"),
|
|
||||||
("0x0001", -1, "0x0000"),
|
|
||||||
("0x0000", -1, "0xffffffffffffffff"),
|
|
||||||
("0xffffffffffffffff", 1, "0x0000000000000000"),
|
|
||||||
("0xffffffffffffffff", 2, "0x0000000000000001"),
|
|
||||||
("0xffffffffffffffff", -1, "0xfffffffffffffffe"),
|
|
||||||
("0xABCDEF1234567890", 1, "0xABCDEF1234567891"),
|
|
||||||
("0xabcdef1234567890", 1, "0xabcdef1234567891"),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (original, amount, expected) in tests {
|
|
||||||
let rope = Rope::from_str(original);
|
|
||||||
let range = Range::point(0);
|
|
||||||
assert_eq!(
|
|
||||||
NumberIncrementor::from_range(rope.slice(..), range)
|
|
||||||
.unwrap()
|
|
||||||
.increment(amount)
|
|
||||||
.1,
|
|
||||||
Tendril::from(expected)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_increment_basic_octal_numbers() {
|
|
||||||
let tests = [
|
|
||||||
("0o0107", 1, "0o0110"),
|
|
||||||
("0o0110", -1, "0o0107"),
|
|
||||||
("0o0001", -1, "0o0000"),
|
|
||||||
("0o7777", 1, "0o10000"),
|
|
||||||
("0o1000", -1, "0o0777"),
|
|
||||||
("0o0107", 10, "0o0121"),
|
|
||||||
("0o0000", -1, "0o1777777777777777777777"),
|
|
||||||
("0o1777777777777777777777", 1, "0o0000000000000000000000"),
|
|
||||||
("0o1777777777777777777777", 2, "0o0000000000000000000001"),
|
|
||||||
("0o1777777777777777777777", -1, "0o1777777777777777777776"),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (original, amount, expected) in tests {
|
|
||||||
let rope = Rope::from_str(original);
|
|
||||||
let range = Range::point(0);
|
|
||||||
assert_eq!(
|
|
||||||
NumberIncrementor::from_range(rope.slice(..), range)
|
|
||||||
.unwrap()
|
|
||||||
.increment(amount)
|
|
||||||
.1,
|
|
||||||
Tendril::from(expected)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_increment_basic_binary_numbers() {
|
|
||||||
let tests = [
|
|
||||||
("0b00000100", 1, "0b00000101"),
|
|
||||||
("0b00000100", -1, "0b00000011"),
|
|
||||||
("0b00000100", 2, "0b00000110"),
|
|
||||||
("0b00000100", -2, "0b00000010"),
|
|
||||||
("0b00000001", -1, "0b00000000"),
|
|
||||||
("0b00111111", 10, "0b01001001"),
|
|
||||||
("0b11111111", 1, "0b100000000"),
|
|
||||||
("0b10000000", -1, "0b01111111"),
|
|
||||||
(
|
|
||||||
"0b0000",
|
|
||||||
-1,
|
|
||||||
"0b1111111111111111111111111111111111111111111111111111111111111111",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"0b1111111111111111111111111111111111111111111111111111111111111111",
|
|
||||||
1,
|
|
||||||
"0b0000000000000000000000000000000000000000000000000000000000000000",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"0b1111111111111111111111111111111111111111111111111111111111111111",
|
|
||||||
2,
|
|
||||||
"0b0000000000000000000000000000000000000000000000000000000000000001",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"0b1111111111111111111111111111111111111111111111111111111111111111",
|
|
||||||
-1,
|
|
||||||
"0b1111111111111111111111111111111111111111111111111111111111111110",
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (original, amount, expected) in tests {
|
|
||||||
let rope = Rope::from_str(original);
|
|
||||||
let range = Range::point(0);
|
|
||||||
assert_eq!(
|
|
||||||
NumberIncrementor::from_range(rope.slice(..), range)
|
|
||||||
.unwrap()
|
|
||||||
.increment(amount)
|
|
||||||
.1,
|
|
||||||
Tendril::from(expected)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_increment_with_separators() {
|
|
||||||
let tests = [
|
|
||||||
("999_999", 1, "1_000_000"),
|
|
||||||
("1_000_000", -1, "999_999"),
|
|
||||||
("-999_999", -1, "-1_000_000"),
|
|
||||||
("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"),
|
|
||||||
("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"),
|
|
||||||
("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"),
|
|
||||||
("0x0000_0000", -1, "0xffff_ffff_ffff_ffff"),
|
|
||||||
("0x0000_0000_0000", -1, "0xffff_ffff_ffff_ffff"),
|
|
||||||
("0b01111111_11111111", 1, "0b10000000_00000000"),
|
|
||||||
("0b11111111_11111111", 1, "0b1_00000000_00000000"),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (original, amount, expected) in tests {
|
|
||||||
let rope = Rope::from_str(original);
|
|
||||||
let range = Range::point(0);
|
|
||||||
assert_eq!(
|
|
||||||
NumberIncrementor::from_range(rope.slice(..), range)
|
|
||||||
.unwrap()
|
|
||||||
.increment(amount)
|
|
||||||
.1,
|
|
||||||
Tendril::from(expected)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,271 @@
|
|||||||
|
use std::cell::Cell;
|
||||||
|
use std::convert::identity;
|
||||||
|
use std::ops::Range;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use crate::syntax::Highlight;
|
||||||
|
use crate::Tendril;
|
||||||
|
|
||||||
|
/// An inline annotation is continuous text shown
|
||||||
|
/// on the screen before the grapheme that starts at
|
||||||
|
/// `char_idx`
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct InlineAnnotation {
|
||||||
|
pub text: Tendril,
|
||||||
|
pub char_idx: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a **single Grapheme** that is part of the document
|
||||||
|
/// that start at `char_idx` that will be replaced with
|
||||||
|
/// a different `grapheme`.
|
||||||
|
/// If `grapheme` contains multiple graphemes the text
|
||||||
|
/// will render incorrectly.
|
||||||
|
/// If you want to overlay multiple graphemes simply
|
||||||
|
/// use multiple `Overlays`.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// The following examples are valid overlays for the following text:
|
||||||
|
///
|
||||||
|
/// `aX͎̊͢͜͝͡bc`
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use helix_core::text_annotations::Overlay;
|
||||||
|
///
|
||||||
|
/// // replaces a
|
||||||
|
/// Overlay {
|
||||||
|
/// char_idx: 0,
|
||||||
|
/// grapheme: "X".into(),
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// // replaces X͎̊͢͜͝͡
|
||||||
|
/// Overlay{
|
||||||
|
/// char_idx: 1,
|
||||||
|
/// grapheme: "\t".into(),
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// // replaces b
|
||||||
|
/// Overlay{
|
||||||
|
/// char_idx: 6,
|
||||||
|
/// grapheme: "X̢̢̟͖̲͌̋̇͑͝".into(),
|
||||||
|
/// };
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The following examples are invalid uses
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use helix_core::text_annotations::Overlay;
|
||||||
|
///
|
||||||
|
/// // overlay is not aligned at grapheme boundary
|
||||||
|
/// Overlay{
|
||||||
|
/// char_idx: 3,
|
||||||
|
/// grapheme: "x".into(),
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// // overlay contains multiple graphemes
|
||||||
|
/// Overlay{
|
||||||
|
/// char_idx: 0,
|
||||||
|
/// grapheme: "xy".into(),
|
||||||
|
/// };
|
||||||
|
/// ```
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Overlay {
|
||||||
|
pub char_idx: usize,
|
||||||
|
pub grapheme: Tendril,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Line annotations allow for virtual text between normal
|
||||||
|
/// text lines. They cause `height` empty lines to be inserted
|
||||||
|
/// below the document line that contains `anchor_char_idx`.
|
||||||
|
///
|
||||||
|
/// These lines can be filled with text in the rendering code
|
||||||
|
/// as their contents have no effect beyond visual appearance.
|
||||||
|
///
|
||||||
|
/// To insert a line after a document line simply set
|
||||||
|
/// `anchor_char_idx` to `doc.line_to_char(line_idx)`
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct LineAnnotation {
|
||||||
|
pub anchor_char_idx: usize,
|
||||||
|
pub height: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Layer<A, M> {
|
||||||
|
annotations: Rc<[A]>,
|
||||||
|
current_index: Cell<usize>,
|
||||||
|
metadata: M,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A, M: Clone> Clone for Layer<A, M> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Layer {
|
||||||
|
annotations: self.annotations.clone(),
|
||||||
|
current_index: self.current_index.clone(),
|
||||||
|
metadata: self.metadata.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A, M> Layer<A, M> {
|
||||||
|
pub fn reset_pos(&self, char_idx: usize, get_char_idx: impl Fn(&A) -> usize) {
|
||||||
|
let new_index = self
|
||||||
|
.annotations
|
||||||
|
.binary_search_by_key(&char_idx, get_char_idx)
|
||||||
|
.unwrap_or_else(identity);
|
||||||
|
|
||||||
|
self.current_index.set(new_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn consume(&self, char_idx: usize, get_char_idx: impl Fn(&A) -> usize) -> Option<&A> {
|
||||||
|
let annot = self.annotations.get(self.current_index.get())?;
|
||||||
|
debug_assert!(get_char_idx(annot) >= char_idx);
|
||||||
|
if get_char_idx(annot) == char_idx {
|
||||||
|
self.current_index.set(self.current_index.get() + 1);
|
||||||
|
Some(annot)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A, M> From<(Rc<[A]>, M)> for Layer<A, M> {
|
||||||
|
fn from((annotations, metadata): (Rc<[A]>, M)) -> Layer<A, M> {
|
||||||
|
Layer {
|
||||||
|
annotations,
|
||||||
|
current_index: Cell::new(0),
|
||||||
|
metadata,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset_pos<A, M>(layers: &[Layer<A, M>], pos: usize, get_pos: impl Fn(&A) -> usize) {
|
||||||
|
for layer in layers {
|
||||||
|
layer.reset_pos(pos, &get_pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Annotations that change that is displayed when the document is render.
|
||||||
|
/// Also commonly called virtual text.
|
||||||
|
#[derive(Default, Debug, Clone)]
|
||||||
|
pub struct TextAnnotations {
|
||||||
|
inline_annotations: Vec<Layer<InlineAnnotation, Option<Highlight>>>,
|
||||||
|
overlays: Vec<Layer<Overlay, Option<Highlight>>>,
|
||||||
|
line_annotations: Vec<Layer<LineAnnotation, ()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextAnnotations {
|
||||||
|
/// Prepare the TextAnnotations for iteration starting at char_idx
|
||||||
|
pub fn reset_pos(&self, char_idx: usize) {
|
||||||
|
reset_pos(&self.inline_annotations, char_idx, |annot| annot.char_idx);
|
||||||
|
reset_pos(&self.overlays, char_idx, |annot| annot.char_idx);
|
||||||
|
reset_pos(&self.line_annotations, char_idx, |annot| {
|
||||||
|
annot.anchor_char_idx
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn collect_overlay_highlights(
|
||||||
|
&self,
|
||||||
|
char_range: Range<usize>,
|
||||||
|
) -> Vec<(usize, Range<usize>)> {
|
||||||
|
let mut highlights = Vec::new();
|
||||||
|
self.reset_pos(char_range.start);
|
||||||
|
for char_idx in char_range {
|
||||||
|
if let Some((_, Some(highlight))) = self.overlay_at(char_idx) {
|
||||||
|
// we don't know the number of chars the original grapheme takes
|
||||||
|
// however it doesn't matter as highlight bounderies are automatically
|
||||||
|
// aligned to grapheme boundaries in the rendering code
|
||||||
|
highlights.push((highlight.0, char_idx..char_idx + 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
highlights
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add new inline annotations.
|
||||||
|
///
|
||||||
|
/// The annotations grapheme will be rendered with `highlight`
|
||||||
|
/// patched on top of `ui.text`.
|
||||||
|
///
|
||||||
|
/// The annotations **must be sorted** by their `char_idx`.
|
||||||
|
/// Multiple annotations with the same `char_idx` are allowed,
|
||||||
|
/// they will be display in the order that they are present in the layer.
|
||||||
|
///
|
||||||
|
/// If multiple layers contain annotations at the same position
|
||||||
|
/// the annotations that belong to the layers added first will be shown first.
|
||||||
|
pub fn add_inline_annotations(
|
||||||
|
&mut self,
|
||||||
|
layer: Rc<[InlineAnnotation]>,
|
||||||
|
highlight: Option<Highlight>,
|
||||||
|
) -> &mut Self {
|
||||||
|
self.inline_annotations.push((layer, highlight).into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add new grapheme overlays.
|
||||||
|
///
|
||||||
|
/// The overlayed grapheme will be rendered with `highlight`
|
||||||
|
/// patched on top of `ui.text`.
|
||||||
|
///
|
||||||
|
/// The overlays **must be sorted** by their `char_idx`.
|
||||||
|
/// Multiple overlays with the same `char_idx` **are allowed**.
|
||||||
|
///
|
||||||
|
/// If multiple layers contain overlay at the same position
|
||||||
|
/// the overlay from the layer added last will be show.
|
||||||
|
pub fn add_overlay(&mut self, layer: Rc<[Overlay]>, highlight: Option<Highlight>) -> &mut Self {
|
||||||
|
self.overlays.push((layer, highlight).into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add new annotation lines.
|
||||||
|
///
|
||||||
|
/// The line annotations **must be sorted** by their `char_idx`.
|
||||||
|
/// Multiple line annotations with the same `char_idx` **are not allowed**.
|
||||||
|
pub fn add_line_annotation(&mut self, layer: Rc<[LineAnnotation]>) -> &mut Self {
|
||||||
|
self.line_annotations.push((layer, ()).into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes all line annotations, useful for vertical motions
|
||||||
|
/// so that virtual text lines are automatically skipped.
|
||||||
|
pub fn clear_line_annotations(&mut self) {
|
||||||
|
self.line_annotations.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn next_inline_annotation_at(
|
||||||
|
&self,
|
||||||
|
char_idx: usize,
|
||||||
|
) -> Option<(&InlineAnnotation, Option<Highlight>)> {
|
||||||
|
self.inline_annotations.iter().find_map(|layer| {
|
||||||
|
let annotation = layer.consume(char_idx, |annot| annot.char_idx)?;
|
||||||
|
Some((annotation, layer.metadata))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn overlay_at(&self, char_idx: usize) -> Option<(&Overlay, Option<Highlight>)> {
|
||||||
|
let mut overlay = None;
|
||||||
|
for layer in &self.overlays {
|
||||||
|
while let Some(new_overlay) = layer.consume(char_idx, |annot| annot.char_idx) {
|
||||||
|
overlay = Some((new_overlay, layer.metadata));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
overlay
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn annotation_lines_at(&self, char_idx: usize) -> usize {
|
||||||
|
self.line_annotations
|
||||||
|
.iter()
|
||||||
|
.map(|layer| {
|
||||||
|
let mut lines = 0;
|
||||||
|
while let Some(annot) = layer.annotations.get(layer.current_index.get()) {
|
||||||
|
if annot.anchor_char_idx == char_idx {
|
||||||
|
layer.current_index.set(layer.current_index.get() + 1);
|
||||||
|
lines += annot.height
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines
|
||||||
|
})
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,26 @@
|
|||||||
|
use std::borrow::Cow;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
const VERSION: &str = include_str!("../VERSION");
|
||||||
|
|
||||||
fn main() {
|
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!(
|
println!(
|
||||||
"cargo:rustc-env=BUILD_TARGET={}",
|
"cargo:rustc-env=BUILD_TARGET={}",
|
||||||
std::env::var("TARGET").unwrap()
|
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 helix_loader::grammar::{build_grammars, fetch_grammars};
|
||||||
use std::borrow::Cow;
|
|
||||||
use std::process::Command;
|
|
||||||
|
|
||||||
const VERSION: &str = include_str!("../VERSION");
|
|
||||||
|
|
||||||
fn main() {
|
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() {
|
if std::env::var("HELIX_DISABLE_AUTO_GRAMMAR_BUILD").is_err() {
|
||||||
fetch_grammars().expect("Failed to fetch tree-sitter grammars");
|
fetch_grammars().expect("Failed to fetch tree-sitter grammars");
|
||||||
build_grammars(Some(std::env::var("TARGET").unwrap()))
|
build_grammars(Some(std::env::var("TARGET").unwrap()))
|
||||||
.expect("Failed to compile tree-sitter grammars");
|
.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
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue