chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use directories::{BaseDirs, ProjectDirs};
|
||||
|
||||
use crate::meta::NAME;
|
||||
|
||||
pub struct Directory {}
|
||||
|
||||
impl Directory {
|
||||
pub fn home_dir() -> Option<PathBuf> {
|
||||
BaseDirs::new().map(|d| PathBuf::from(d.home_dir()))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "portable"))]
|
||||
fn project_dirs() -> Option<ProjectDirs> {
|
||||
ProjectDirs::from("dev", "lapce", NAME)
|
||||
}
|
||||
|
||||
/// Return path adjacent to lapce executable when built as portable
|
||||
#[cfg(feature = "portable")]
|
||||
fn project_dirs() -> Option<ProjectDirs> {
|
||||
if let Ok(current_exe) = std::env::current_exe() {
|
||||
if let Some(parent) = current_exe.parent() {
|
||||
return ProjectDirs::from_path(parent.join("lapce-data"));
|
||||
}
|
||||
unreachable!("Couldn't obtain current process parent path");
|
||||
}
|
||||
unreachable!("Couldn't obtain current process path");
|
||||
}
|
||||
|
||||
// Get path of local data directory
|
||||
// Local data directory differs from data directory
|
||||
// on some platforms and is not transferred across
|
||||
// machines
|
||||
pub fn data_local_directory() -> Option<PathBuf> {
|
||||
match Self::project_dirs() {
|
||||
Some(dir) => {
|
||||
let dir = dir.data_local_dir();
|
||||
if !dir.exists() {
|
||||
if let Err(err) = std::fs::create_dir_all(dir) {
|
||||
tracing::error!("{:?}", err);
|
||||
}
|
||||
}
|
||||
Some(dir.to_path_buf())
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the path to logs directory
|
||||
/// Each log file is for individual application startup
|
||||
pub fn logs_directory() -> Option<PathBuf> {
|
||||
if let Some(dir) = Self::data_local_directory() {
|
||||
let dir = dir.join("logs");
|
||||
if !dir.exists() {
|
||||
if let Err(err) = std::fs::create_dir(&dir) {
|
||||
tracing::error!("{:?}", err);
|
||||
}
|
||||
}
|
||||
Some(dir)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the path to cache directory
|
||||
pub fn cache_directory() -> Option<PathBuf> {
|
||||
if let Some(dir) = Self::data_local_directory() {
|
||||
let dir = dir.join("cache");
|
||||
if !dir.exists() {
|
||||
if let Err(err) = std::fs::create_dir(&dir) {
|
||||
tracing::error!("{:?}", err);
|
||||
}
|
||||
}
|
||||
Some(dir)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Directory to store proxy executables used on local
|
||||
/// host as well, as ones uploaded to remote host when
|
||||
/// connecting
|
||||
pub fn proxy_directory() -> Option<PathBuf> {
|
||||
if let Some(dir) = Self::data_local_directory() {
|
||||
let dir = dir.join("proxy");
|
||||
if !dir.exists() {
|
||||
if let Err(err) = std::fs::create_dir(&dir) {
|
||||
tracing::error!("{:?}", err);
|
||||
}
|
||||
}
|
||||
Some(dir)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
/// Get the path to the themes folder
|
||||
/// Themes are stored within as individual toml files
|
||||
pub fn themes_directory() -> Option<PathBuf> {
|
||||
if let Some(dir) = Self::data_local_directory() {
|
||||
let dir = dir.join("themes");
|
||||
if !dir.exists() {
|
||||
if let Err(err) = std::fs::create_dir(&dir) {
|
||||
tracing::error!("{:?}", err);
|
||||
}
|
||||
}
|
||||
Some(dir)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
// Get the path to plugins directory
|
||||
// Each plugin has own directory that contains
|
||||
// metadata file and plugin wasm
|
||||
pub fn plugins_directory() -> Option<PathBuf> {
|
||||
if let Some(dir) = Self::data_local_directory() {
|
||||
let dir = dir.join("plugins");
|
||||
if !dir.exists() {
|
||||
if let Err(err) = std::fs::create_dir(&dir) {
|
||||
tracing::error!("{:?}", err);
|
||||
}
|
||||
}
|
||||
Some(dir)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// Config directory contain only configuration files
|
||||
pub fn config_directory() -> Option<PathBuf> {
|
||||
match Self::project_dirs() {
|
||||
Some(dir) => {
|
||||
let dir = dir.config_dir();
|
||||
if !dir.exists() {
|
||||
if let Err(err) = std::fs::create_dir_all(dir) {
|
||||
tracing::error!("{:?}", err);
|
||||
}
|
||||
}
|
||||
Some(dir.to_path_buf())
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn local_socket() -> Option<PathBuf> {
|
||||
Self::data_local_directory().map(|dir| dir.join("local.sock"))
|
||||
}
|
||||
|
||||
pub fn updates_directory() -> Option<PathBuf> {
|
||||
if let Some(dir) = Self::data_local_directory() {
|
||||
let dir = dir.join("updates");
|
||||
if !dir.exists() {
|
||||
if let Err(err) = std::fs::create_dir(&dir) {
|
||||
tracing::error!("{:?}", err);
|
||||
}
|
||||
}
|
||||
Some(dir)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn queries_directory() -> Option<PathBuf> {
|
||||
if let Some(dir) = Self::config_directory() {
|
||||
let dir = dir.join("queries");
|
||||
if !dir.exists() {
|
||||
if let Err(err) = std::fs::create_dir(&dir) {
|
||||
tracing::error!("{:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
Some(dir)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn grammars_directory() -> Option<PathBuf> {
|
||||
if let Some(dir) = Self::data_local_directory() {
|
||||
let dir = dir.join("grammars");
|
||||
if !dir.exists() {
|
||||
if let Err(err) = std::fs::create_dir(&dir) {
|
||||
tracing::error!("{:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
Some(dir)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/// Convert a utf8 offset into a utf16 offset, if possible
|
||||
/// `text` is what the offsets are into
|
||||
pub fn offset_utf8_to_utf16(
|
||||
char_indices: impl Iterator<Item = (usize, char)>,
|
||||
offset: usize,
|
||||
) -> usize {
|
||||
if offset == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let mut utf16_offset = 0;
|
||||
let mut last_ich = None;
|
||||
for (utf8_offset, ch) in char_indices {
|
||||
last_ich = Some((utf8_offset, ch));
|
||||
|
||||
match utf8_offset.cmp(&offset) {
|
||||
std::cmp::Ordering::Less => {}
|
||||
// We found the right offset
|
||||
std::cmp::Ordering::Equal => {
|
||||
return utf16_offset;
|
||||
}
|
||||
// Implies that the offset was inside of a character
|
||||
std::cmp::Ordering::Greater => return utf16_offset,
|
||||
}
|
||||
|
||||
utf16_offset += ch.len_utf16();
|
||||
}
|
||||
|
||||
// TODO: We could use TrustedLen when that is stabilized and it is impl'd on
|
||||
// the iterators we use
|
||||
|
||||
// We did not find the offset. This means that it is either at the end
|
||||
// or past the end.
|
||||
let text_len = last_ich.map(|(i, c)| i + c.len_utf8());
|
||||
if text_len == Some(offset) {
|
||||
// Since the utf16 offset was being incremented each time, by now it is equivalent to the length
|
||||
// but in utf16 characters
|
||||
return utf16_offset;
|
||||
}
|
||||
|
||||
utf16_offset
|
||||
}
|
||||
|
||||
pub fn offset_utf8_to_utf16_str(text: &str, offset: usize) -> usize {
|
||||
offset_utf8_to_utf16(text.char_indices(), offset)
|
||||
}
|
||||
|
||||
/// Convert a utf16 offset into a utf8 offset, if possible
|
||||
/// `char_indices` is an iterator over utf8 offsets and the characters
|
||||
/// It is cloneable so that it can be iterated multiple times. Though it should be cheaply cloneable.
|
||||
pub fn offset_utf16_to_utf8(
|
||||
char_indices: impl Iterator<Item = (usize, char)>,
|
||||
offset: usize,
|
||||
) -> usize {
|
||||
if offset == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// We accumulate the utf16 char lens until we find the utf8 offset that matches it
|
||||
// or, we find out that it went into the middle of sometext
|
||||
// We also keep track of the last offset and char in order to calculate the length of the text
|
||||
// if we the index was at the end of the string
|
||||
let mut utf16_offset = 0;
|
||||
let mut last_ich = None;
|
||||
for (utf8_offset, ch) in char_indices {
|
||||
last_ich = Some((utf8_offset, ch));
|
||||
|
||||
let ch_utf16_len = ch.len_utf16();
|
||||
|
||||
match utf16_offset.cmp(&offset) {
|
||||
std::cmp::Ordering::Less => {}
|
||||
// We found the right offset
|
||||
std::cmp::Ordering::Equal => {
|
||||
return utf8_offset;
|
||||
}
|
||||
// This implies that the offset was in the middle of a character as we skipped over it
|
||||
std::cmp::Ordering::Greater => return utf8_offset,
|
||||
}
|
||||
|
||||
utf16_offset += ch_utf16_len;
|
||||
}
|
||||
|
||||
// We did not find the offset, this means that it was either at the end
|
||||
// or past the end
|
||||
// Since we've iterated over all the char indices, the utf16_offset is now the
|
||||
// utf16 length
|
||||
if let Some((last_utf8_offset, last_ch)) = last_ich {
|
||||
last_utf8_offset + last_ch.len_utf8()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn offset_utf16_to_utf8_str(text: &str, offset: usize) -> usize {
|
||||
offset_utf16_to_utf8(text.char_indices(), offset)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// TODO: more tests with unicode characters
|
||||
|
||||
use crate::encoding::{offset_utf8_to_utf16_str, offset_utf16_to_utf8_str};
|
||||
|
||||
#[test]
|
||||
fn utf8_to_utf16() {
|
||||
let text = "hello world";
|
||||
|
||||
assert_eq!(offset_utf8_to_utf16_str(text, 0), 0);
|
||||
assert_eq!(offset_utf8_to_utf16_str("", 0), 0);
|
||||
|
||||
assert_eq!(offset_utf8_to_utf16_str("", 1), 0);
|
||||
|
||||
assert_eq!(offset_utf8_to_utf16_str("h", 0), 0);
|
||||
assert_eq!(offset_utf8_to_utf16_str("h", 1), 1);
|
||||
|
||||
assert_eq!(offset_utf8_to_utf16_str(text, text.len()), text.len());
|
||||
|
||||
assert_eq!(
|
||||
offset_utf8_to_utf16_str(text, text.len() - 1),
|
||||
text.len() - 1
|
||||
);
|
||||
|
||||
assert_eq!(offset_utf8_to_utf16_str(text, text.len() + 1), text.len());
|
||||
|
||||
assert_eq!(offset_utf8_to_utf16_str("×", 0), 0);
|
||||
assert_eq!(offset_utf8_to_utf16_str("×", 1), 1);
|
||||
assert_eq!(offset_utf8_to_utf16_str("×", 2), 1);
|
||||
assert_eq!(offset_utf8_to_utf16_str("a×", 0), 0);
|
||||
assert_eq!(offset_utf8_to_utf16_str("a×", 1), 1);
|
||||
assert_eq!(offset_utf8_to_utf16_str("a×", 2), 2);
|
||||
assert_eq!(offset_utf8_to_utf16_str("a×", 3), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn utf16_to_utf8() {
|
||||
let text = "hello world";
|
||||
|
||||
assert_eq!(offset_utf16_to_utf8_str(text, 0), 0);
|
||||
assert_eq!(offset_utf16_to_utf8_str("", 0), 0);
|
||||
|
||||
assert_eq!(offset_utf16_to_utf8_str("", 1), 0);
|
||||
|
||||
assert_eq!(offset_utf16_to_utf8_str("h", 0), 0);
|
||||
assert_eq!(offset_utf16_to_utf8_str("h", 1), 1);
|
||||
|
||||
assert_eq!(offset_utf16_to_utf8_str(text, text.len()), text.len());
|
||||
|
||||
assert_eq!(
|
||||
offset_utf16_to_utf8_str(text, text.len() - 1),
|
||||
text.len() - 1
|
||||
);
|
||||
|
||||
assert_eq!(offset_utf16_to_utf8_str(text, text.len() + 1), text.len());
|
||||
|
||||
assert_eq!(offset_utf16_to_utf8_str("×", 0), 0);
|
||||
assert_eq!(offset_utf16_to_utf8_str("×", 1), 2);
|
||||
assert_eq!(offset_utf16_to_utf8_str("a×", 0), 0);
|
||||
assert_eq!(offset_utf16_to_utf8_str("a×", 1), 1);
|
||||
assert_eq!(offset_utf16_to_utf8_str("a×", 2), 3);
|
||||
assert_eq!(offset_utf16_to_utf8_str("×a", 1), 2);
|
||||
assert_eq!(offset_utf16_to_utf8_str("×a", 2), 3);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,388 @@
|
||||
use std::mem;
|
||||
|
||||
use lapce_xi_rope::{
|
||||
Cursor, Delta, Interval, Metric,
|
||||
interval::IntervalBounds,
|
||||
tree::{DefaultMetric, Leaf, Node, NodeInfo, TreeBuilder},
|
||||
};
|
||||
|
||||
const MIN_LEAF: usize = 5;
|
||||
const MAX_LEAF: usize = 10;
|
||||
|
||||
pub type LensNode = Node<LensInfo>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Lens(LensNode);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LensInfo(usize);
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct LensData {
|
||||
len: usize,
|
||||
line_height: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub struct LensLeaf {
|
||||
len: usize,
|
||||
data: Vec<LensData>,
|
||||
total_height: usize,
|
||||
}
|
||||
|
||||
pub struct LensIter<'a> {
|
||||
cursor: Cursor<'a, LensInfo>,
|
||||
end: usize,
|
||||
}
|
||||
|
||||
impl Lens {
|
||||
pub fn len(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.len() == 0
|
||||
}
|
||||
|
||||
pub fn line_of_height(&self, height: usize) -> usize {
|
||||
let max_height = self.0.count::<LensMetric>(self.0.len());
|
||||
if height >= max_height {
|
||||
return self.0.len();
|
||||
}
|
||||
self.0.count_base_units::<LensMetric>(height)
|
||||
}
|
||||
|
||||
pub fn height_of_line(&self, line: usize) -> usize {
|
||||
let line = self.0.len().min(line);
|
||||
self.0.count::<LensMetric>(line)
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> LensIter<'_> {
|
||||
LensIter {
|
||||
cursor: Cursor::new(&self.0, 0),
|
||||
end: self.len(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter_chunks<I: IntervalBounds>(&self, range: I) -> LensIter<'_> {
|
||||
let Interval { start, end } = range.into_interval(self.len());
|
||||
|
||||
LensIter {
|
||||
cursor: Cursor::new(&self.0, start),
|
||||
end,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_delta<M: NodeInfo>(&mut self, _delta: &Delta<M>) {}
|
||||
}
|
||||
|
||||
impl NodeInfo for LensInfo {
|
||||
type L = LensLeaf;
|
||||
|
||||
fn accumulate(&mut self, other: &Self) {
|
||||
self.0 += other.0;
|
||||
}
|
||||
|
||||
fn compute_info(l: &LensLeaf) -> LensInfo {
|
||||
LensInfo(l.total_height)
|
||||
}
|
||||
}
|
||||
|
||||
impl Leaf for LensLeaf {
|
||||
fn len(&self) -> usize {
|
||||
self.len
|
||||
}
|
||||
|
||||
fn is_ok_child(&self) -> bool {
|
||||
self.data.len() >= MIN_LEAF
|
||||
}
|
||||
|
||||
fn push_maybe_split(
|
||||
&mut self,
|
||||
other: &LensLeaf,
|
||||
iv: Interval,
|
||||
) -> Option<LensLeaf> {
|
||||
let (iv_start, iv_end) = iv.start_end();
|
||||
let mut accum = 0;
|
||||
let mut added_len = 0;
|
||||
let mut added_height = 0;
|
||||
for sec in &other.data {
|
||||
if accum + sec.len < iv_start {
|
||||
accum += sec.len;
|
||||
continue;
|
||||
}
|
||||
|
||||
if accum + sec.len <= iv_end {
|
||||
accum += sec.len;
|
||||
self.data.push(LensData {
|
||||
len: sec.len,
|
||||
line_height: sec.line_height,
|
||||
});
|
||||
added_len += sec.len;
|
||||
added_height += sec.len * sec.line_height;
|
||||
continue;
|
||||
}
|
||||
|
||||
let len = iv_end - (accum + sec.len);
|
||||
self.data.push(LensData {
|
||||
len,
|
||||
line_height: sec.line_height,
|
||||
});
|
||||
added_len += len;
|
||||
added_height += sec.len * sec.line_height;
|
||||
break;
|
||||
}
|
||||
self.len += added_len;
|
||||
self.total_height += added_height;
|
||||
|
||||
if self.data.len() <= MAX_LEAF {
|
||||
None
|
||||
} else {
|
||||
let splitpoint = self.data.len() / 2; // number of spans
|
||||
let new = self.data.split_off(splitpoint);
|
||||
let new_len = new.iter().map(|d| d.len).sum();
|
||||
let new_height = new.iter().map(|d| d.len * d.line_height).sum();
|
||||
self.len -= new_len;
|
||||
self.total_height -= new_height;
|
||||
Some(LensLeaf {
|
||||
len: new_len,
|
||||
data: new,
|
||||
total_height: new_height,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct LensMetric(());
|
||||
|
||||
impl Metric<LensInfo> for LensMetric {
|
||||
fn measure(info: &LensInfo, _len: usize) -> usize {
|
||||
info.0
|
||||
}
|
||||
|
||||
fn to_base_units(l: &LensLeaf, in_measured_units: usize) -> usize {
|
||||
if in_measured_units > l.total_height {
|
||||
l.len
|
||||
} else if in_measured_units == 0 {
|
||||
0
|
||||
} else {
|
||||
let mut line = 0;
|
||||
let mut accum = 0;
|
||||
for data in l.data.iter() {
|
||||
let leaf_height = data.line_height * data.len;
|
||||
let accum_height = accum + leaf_height;
|
||||
if accum_height > in_measured_units {
|
||||
return line + (in_measured_units - accum) / data.line_height;
|
||||
}
|
||||
accum = accum_height;
|
||||
line += data.len;
|
||||
}
|
||||
line
|
||||
}
|
||||
}
|
||||
|
||||
fn from_base_units(l: &LensLeaf, in_base_units: usize) -> usize {
|
||||
let mut line = 0;
|
||||
let mut accum = 0;
|
||||
for data in l.data.iter() {
|
||||
if in_base_units < line + data.len {
|
||||
return accum + (in_base_units - line) * data.line_height;
|
||||
}
|
||||
accum += data.len * data.line_height;
|
||||
line += data.len;
|
||||
}
|
||||
accum
|
||||
}
|
||||
|
||||
fn is_boundary(_l: &LensLeaf, _offset: usize) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn prev(_l: &LensLeaf, offset: usize) -> Option<usize> {
|
||||
if offset == 0 { None } else { Some(offset - 1) }
|
||||
}
|
||||
|
||||
fn next(l: &LensLeaf, offset: usize) -> Option<usize> {
|
||||
if offset < l.len {
|
||||
Some(offset + 1)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn can_fragment() -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl DefaultMetric for LensInfo {
|
||||
type DefaultMetric = LensBaseMetric;
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct LensBaseMetric(());
|
||||
|
||||
impl Metric<LensInfo> for LensBaseMetric {
|
||||
fn measure(_: &LensInfo, len: usize) -> usize {
|
||||
len
|
||||
}
|
||||
|
||||
fn to_base_units(_: &LensLeaf, in_measured_units: usize) -> usize {
|
||||
in_measured_units
|
||||
}
|
||||
|
||||
fn from_base_units(_: &LensLeaf, in_base_units: usize) -> usize {
|
||||
in_base_units
|
||||
}
|
||||
|
||||
fn is_boundary(l: &LensLeaf, offset: usize) -> bool {
|
||||
LensMetric::is_boundary(l, offset)
|
||||
}
|
||||
|
||||
fn prev(l: &LensLeaf, offset: usize) -> Option<usize> {
|
||||
LensMetric::prev(l, offset)
|
||||
}
|
||||
|
||||
fn next(l: &LensLeaf, offset: usize) -> Option<usize> {
|
||||
LensMetric::next(l, offset)
|
||||
}
|
||||
|
||||
fn can_fragment() -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LensBuilder {
|
||||
b: TreeBuilder<LensInfo>,
|
||||
leaf: LensLeaf,
|
||||
}
|
||||
|
||||
impl Default for LensBuilder {
|
||||
fn default() -> LensBuilder {
|
||||
LensBuilder {
|
||||
b: TreeBuilder::new(),
|
||||
leaf: LensLeaf::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LensBuilder {
|
||||
pub fn new() -> LensBuilder {
|
||||
LensBuilder::default()
|
||||
}
|
||||
|
||||
pub fn add_section(&mut self, len: usize, line_height: usize) {
|
||||
if self.leaf.data.len() == MAX_LEAF {
|
||||
let leaf = mem::take(&mut self.leaf);
|
||||
self.b.push(Node::from_leaf(leaf));
|
||||
}
|
||||
self.leaf.len += len;
|
||||
self.leaf.total_height += len * line_height;
|
||||
self.leaf.data.push(LensData { len, line_height });
|
||||
}
|
||||
|
||||
pub fn build(mut self) -> Lens {
|
||||
self.b.push(Node::from_leaf(self.leaf));
|
||||
Lens(self.b.build())
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for LensIter<'_> {
|
||||
type Item = (usize, usize);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.cursor.pos() >= self.end {
|
||||
return None;
|
||||
}
|
||||
if let Some((leaf, leaf_pos)) = self.cursor.get_leaf() {
|
||||
if leaf.data.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let line = self.cursor.pos();
|
||||
self.cursor.next::<LensMetric>();
|
||||
|
||||
let mut lines = 0;
|
||||
for data in leaf.data.iter() {
|
||||
if leaf_pos < data.len + lines {
|
||||
return Some((line, data.line_height));
|
||||
}
|
||||
lines += data.len;
|
||||
}
|
||||
return None;
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_lens_metric() {
|
||||
let mut builder = LensBuilder::new();
|
||||
builder.add_section(10, 2);
|
||||
builder.add_section(1, 25);
|
||||
builder.add_section(20, 3);
|
||||
let lens = builder.build();
|
||||
|
||||
assert_eq!(31, lens.len());
|
||||
assert_eq!(0, lens.height_of_line(0));
|
||||
assert_eq!(2, lens.height_of_line(1));
|
||||
assert_eq!(20, lens.height_of_line(10));
|
||||
assert_eq!(45, lens.height_of_line(11));
|
||||
assert_eq!(48, lens.height_of_line(12));
|
||||
assert_eq!(105, lens.height_of_line(31));
|
||||
assert_eq!(105, lens.height_of_line(32));
|
||||
assert_eq!(105, lens.height_of_line(62));
|
||||
|
||||
assert_eq!(0, lens.line_of_height(0));
|
||||
assert_eq!(0, lens.line_of_height(1));
|
||||
assert_eq!(1, lens.line_of_height(2));
|
||||
assert_eq!(1, lens.line_of_height(3));
|
||||
assert_eq!(2, lens.line_of_height(4));
|
||||
assert_eq!(2, lens.line_of_height(5));
|
||||
assert_eq!(3, lens.line_of_height(6));
|
||||
assert_eq!(10, lens.line_of_height(20));
|
||||
assert_eq!(10, lens.line_of_height(44));
|
||||
assert_eq!(11, lens.line_of_height(45));
|
||||
assert_eq!(11, lens.line_of_height(46));
|
||||
assert_eq!(31, lens.line_of_height(105));
|
||||
assert_eq!(31, lens.line_of_height(106));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lens_iter() {
|
||||
let mut builder = LensBuilder::new();
|
||||
builder.add_section(10, 2);
|
||||
builder.add_section(1, 25);
|
||||
builder.add_section(2, 3);
|
||||
let lens = builder.build();
|
||||
|
||||
let mut iter = lens.iter();
|
||||
assert_eq!(Some((0, 2)), iter.next());
|
||||
assert_eq!(Some((1, 2)), iter.next());
|
||||
assert_eq!(Some((2, 2)), iter.next());
|
||||
for _ in 0..7 {
|
||||
iter.next();
|
||||
}
|
||||
assert_eq!(Some((10, 25)), iter.next());
|
||||
assert_eq!(Some((11, 3)), iter.next());
|
||||
assert_eq!(Some((12, 3)), iter.next());
|
||||
assert_eq!(None, iter.next());
|
||||
|
||||
let mut iter = lens.iter_chunks(9..12);
|
||||
assert_eq!(Some((9, 2)), iter.next());
|
||||
assert_eq!(Some((10, 25)), iter.next());
|
||||
assert_eq!(Some((11, 3)), iter.next());
|
||||
assert_eq!(None, iter.next());
|
||||
|
||||
let mut iter = lens.iter_chunks(9..15);
|
||||
assert_eq!(Some((9, 2)), iter.next());
|
||||
assert_eq!(Some((10, 25)), iter.next());
|
||||
assert_eq!(Some((11, 3)), iter.next());
|
||||
assert_eq!(Some((12, 3)), iter.next());
|
||||
assert_eq!(None, iter.next());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#![allow(clippy::manual_clamp)]
|
||||
|
||||
pub mod directory;
|
||||
pub mod encoding;
|
||||
pub mod language;
|
||||
pub mod lens;
|
||||
pub mod meta;
|
||||
pub mod rope_text_pos;
|
||||
pub mod style;
|
||||
pub mod syntax;
|
||||
// This is primarily being re-exported to avoid changing every single usage
|
||||
// in lapce-app. We should probably remove this at some point.
|
||||
pub use floem_editor_core::*;
|
||||
@@ -0,0 +1,8 @@
|
||||
#[derive(strum_macros::AsRefStr, PartialEq, Eq)]
|
||||
pub enum ReleaseType {
|
||||
Debug,
|
||||
Stable,
|
||||
Nightly,
|
||||
}
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/meta.rs"));
|
||||
@@ -0,0 +1,40 @@
|
||||
use floem_editor_core::buffer::rope_text::RopeText;
|
||||
use lsp_types::Position;
|
||||
|
||||
use crate::encoding::{offset_utf8_to_utf16, offset_utf16_to_utf8};
|
||||
|
||||
pub trait RopeTextPosition: RopeText {
|
||||
/// Converts a UTF8 offset to a UTF16 LSP position
|
||||
/// Returns None if it is not a valid UTF16 offset
|
||||
fn offset_to_position(&self, offset: usize) -> Position {
|
||||
let (line, col) = self.offset_to_line_col(offset);
|
||||
let line_offset = self.offset_of_line(line);
|
||||
|
||||
let utf16_col =
|
||||
offset_utf8_to_utf16(self.char_indices_iter(line_offset..), col);
|
||||
|
||||
Position {
|
||||
line: line as u32,
|
||||
character: utf16_col as u32,
|
||||
}
|
||||
}
|
||||
|
||||
fn offset_of_position(&self, pos: &Position) -> usize {
|
||||
let (line, column) = self.position_to_line_col(pos);
|
||||
|
||||
self.offset_of_line_col(line, column)
|
||||
}
|
||||
|
||||
fn position_to_line_col(&self, pos: &Position) -> (usize, usize) {
|
||||
let line = pos.line as usize;
|
||||
let line_offset = self.offset_of_line(line);
|
||||
|
||||
let column = offset_utf16_to_utf8(
|
||||
self.char_indices_iter(line_offset..),
|
||||
pos.character as usize,
|
||||
);
|
||||
|
||||
(line, column)
|
||||
}
|
||||
}
|
||||
impl<T: RopeText> RopeTextPosition for T {}
|
||||
@@ -0,0 +1,75 @@
|
||||
use std::str;
|
||||
|
||||
use lapce_rpc::style::{LineStyle, Style};
|
||||
use lapce_xi_rope::{LinesMetric, Rope, spans::Spans};
|
||||
|
||||
pub const SCOPES: &[&str] = &[
|
||||
"constant",
|
||||
"type",
|
||||
"type.builtin",
|
||||
"property",
|
||||
"comment",
|
||||
"constructor",
|
||||
"function",
|
||||
"label",
|
||||
"keyword",
|
||||
"string",
|
||||
"variable",
|
||||
"variable.other.member",
|
||||
"operator",
|
||||
"attribute",
|
||||
"escape",
|
||||
"embedded",
|
||||
"symbol",
|
||||
"punctuation",
|
||||
"punctuation.special",
|
||||
"punctuation.delimiter",
|
||||
"text",
|
||||
"text.literal",
|
||||
"text.title",
|
||||
"text.uri",
|
||||
"text.reference",
|
||||
"string.escape",
|
||||
"conceal",
|
||||
"none",
|
||||
"tag",
|
||||
"markup.bold",
|
||||
"markup.italic",
|
||||
"markup.list",
|
||||
"markup.quote",
|
||||
"markup.heading",
|
||||
"markup.link.url",
|
||||
"markup.link.label",
|
||||
"markup.link.text",
|
||||
];
|
||||
|
||||
pub fn line_styles(
|
||||
text: &Rope,
|
||||
line: usize,
|
||||
styles: &Spans<Style>,
|
||||
) -> Vec<LineStyle> {
|
||||
let max_line = text.measure::<LinesMetric>() + 1;
|
||||
|
||||
if line >= max_line {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let start_offset = text.offset_of_line(line);
|
||||
let end_offset = text.offset_of_line(line + 1);
|
||||
let line_styles: Vec<LineStyle> = styles
|
||||
.iter_chunks(start_offset..end_offset)
|
||||
.filter_map(|(iv, style)| {
|
||||
let start = iv.start();
|
||||
let end = iv.end();
|
||||
if start > end_offset || end < start_offset {
|
||||
None
|
||||
} else {
|
||||
let start = start.saturating_sub(start_offset);
|
||||
let end = end - start_offset;
|
||||
let style = style.clone();
|
||||
Some(LineStyle { start, end, style })
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
line_styles
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
use floem_editor_core::buffer::{
|
||||
InsertsValueIter,
|
||||
rope_text::{RopeText, RopeTextRef},
|
||||
};
|
||||
use lapce_xi_rope::{
|
||||
Rope, RopeDelta, RopeInfo,
|
||||
delta::InsertDelta,
|
||||
multiset::{CountMatcher, Subset},
|
||||
};
|
||||
use tree_sitter::Point;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SyntaxEdit(pub(crate) Vec<tree_sitter::InputEdit>);
|
||||
|
||||
impl SyntaxEdit {
|
||||
pub fn new(edits: Vec<tree_sitter::InputEdit>) -> Self {
|
||||
Self(edits)
|
||||
}
|
||||
|
||||
pub fn from_delta(text: &Rope, delta: RopeDelta) -> SyntaxEdit {
|
||||
let (ins_delta, deletes) = delta.factor();
|
||||
|
||||
Self::from_factored_delta(text, &ins_delta, &deletes)
|
||||
}
|
||||
|
||||
pub fn from_factored_delta(
|
||||
text: &Rope,
|
||||
ins_delta: &InsertDelta<RopeInfo>,
|
||||
deletes: &Subset,
|
||||
) -> SyntaxEdit {
|
||||
let deletes = deletes.transform_expand(&ins_delta.inserted_subset());
|
||||
|
||||
let mut edits = Vec::new();
|
||||
|
||||
let mut insert_edits: Vec<tree_sitter::InputEdit> =
|
||||
InsertsValueIter::new(ins_delta)
|
||||
.map(|insert| {
|
||||
let start = insert.old_offset;
|
||||
let inserted = insert.node;
|
||||
create_insert_edit(text, start, inserted)
|
||||
})
|
||||
.collect();
|
||||
insert_edits.reverse();
|
||||
edits.append(&mut insert_edits);
|
||||
|
||||
let text = ins_delta.apply(text);
|
||||
let mut delete_edits: Vec<tree_sitter::InputEdit> = deletes
|
||||
.range_iter(CountMatcher::NonZero)
|
||||
.map(|(start, end)| create_delete_edit(&text, start, end))
|
||||
.collect();
|
||||
delete_edits.reverse();
|
||||
edits.append(&mut delete_edits);
|
||||
|
||||
SyntaxEdit::new(edits)
|
||||
}
|
||||
}
|
||||
|
||||
fn point_at_offset(text: &Rope, offset: usize) -> Point {
|
||||
let text = RopeTextRef::new(text);
|
||||
let line = text.line_of_offset(offset);
|
||||
let col = text.offset_of_line(line + 1).saturating_sub(offset);
|
||||
Point::new(line, col)
|
||||
}
|
||||
|
||||
fn traverse(point: Point, text: &str) -> Point {
|
||||
let Point {
|
||||
mut row,
|
||||
mut column,
|
||||
} = point;
|
||||
|
||||
for ch in text.chars() {
|
||||
if ch == '\n' {
|
||||
row += 1;
|
||||
column = 0;
|
||||
} else {
|
||||
column += 1;
|
||||
}
|
||||
}
|
||||
Point { row, column }
|
||||
}
|
||||
|
||||
pub fn create_insert_edit(
|
||||
old_text: &Rope,
|
||||
start: usize,
|
||||
inserted: &Rope,
|
||||
) -> tree_sitter::InputEdit {
|
||||
let start_position = point_at_offset(old_text, start);
|
||||
tree_sitter::InputEdit {
|
||||
start_byte: start,
|
||||
old_end_byte: start,
|
||||
new_end_byte: start + inserted.len(),
|
||||
start_position,
|
||||
old_end_position: start_position,
|
||||
new_end_position: traverse(
|
||||
start_position,
|
||||
&inserted.slice_to_cow(0..inserted.len()),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_delete_edit(
|
||||
old_text: &Rope,
|
||||
start: usize,
|
||||
end: usize,
|
||||
) -> tree_sitter::InputEdit {
|
||||
let start_position = point_at_offset(old_text, start);
|
||||
let end_position = point_at_offset(old_text, end);
|
||||
tree_sitter::InputEdit {
|
||||
start_byte: start,
|
||||
// The old end byte position was at the end
|
||||
old_end_byte: end,
|
||||
// but since we're deleting everything up to it, it gets 'moved' to where we start
|
||||
new_end_byte: start,
|
||||
|
||||
start_position,
|
||||
old_end_position: end_position,
|
||||
new_end_position: start_position,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,831 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Much of the code in this file is modified from [helix](https://github.com/helix-editor/helix)'s implementation of their syntax highlighting, which is under the MPL.
|
||||
*/
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cell::RefCell,
|
||||
collections::HashMap,
|
||||
path::Path,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use lapce_xi_rope::Rope;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use tree_sitter::{
|
||||
Language, Point, Query, QueryCaptures, QueryCursor, QueryMatch, Tree,
|
||||
};
|
||||
|
||||
use super::{PARSER, util::RopeProvider};
|
||||
use crate::{language::LapceLanguage, style::SCOPES};
|
||||
|
||||
thread_local! {
|
||||
static HIGHLIGHT_CONFIGS: RefCell<HashMap<LapceLanguage, Result<Arc<HighlightConfiguration>, HighlightIssue>>> = Default::default();
|
||||
}
|
||||
|
||||
pub fn reset_highlight_configs() {
|
||||
HIGHLIGHT_CONFIGS.with_borrow_mut(|configs| {
|
||||
configs.clear();
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn get_highlight_config(
|
||||
lang: LapceLanguage,
|
||||
) -> Result<Arc<HighlightConfiguration>, HighlightIssue> {
|
||||
HIGHLIGHT_CONFIGS.with(|configs| {
|
||||
let mut configs = configs.borrow_mut();
|
||||
let config = configs
|
||||
.entry(lang)
|
||||
.or_insert_with(|| lang.new_highlight_config().map(Arc::new));
|
||||
config.clone()
|
||||
})
|
||||
}
|
||||
|
||||
/// Indicates which highlight should be applied to a region of source code.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Highlight(pub usize);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum HighlightIssue {
|
||||
Error(String),
|
||||
NotAvailable,
|
||||
}
|
||||
|
||||
/// Represents a single step in rendering a syntax-highlighted document.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum HighlightEvent {
|
||||
Source { start: usize, end: usize },
|
||||
HighlightStart(Highlight),
|
||||
HighlightEnd,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct LocalDef<'a> {
|
||||
name: Cow<'a, str>,
|
||||
value_range: std::ops::Range<usize>,
|
||||
highlight: Option<Highlight>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct LocalScope<'a> {
|
||||
pub(crate) inherits: bool,
|
||||
pub(crate) range: std::ops::Range<usize>,
|
||||
pub(crate) local_defs: Vec<LocalDef<'a>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum InjectionLanguageMarker<'a> {
|
||||
Name(Cow<'a, str>),
|
||||
Filename(Cow<'a, Path>),
|
||||
Shebang(String),
|
||||
}
|
||||
|
||||
const SHEBANG: &str = r"#!\s*(?:\S*[/\\](?:env\s+(?:\-\S+\s+)*)?)?([^\s\.\d]+)";
|
||||
|
||||
const CANCELLATION_CHECK_INTERVAL: usize = 100;
|
||||
|
||||
/// Contains the data needed to highlight code written in a particular language.
|
||||
///
|
||||
/// This struct is immutable and can be shared between threads.
|
||||
#[derive(Debug)]
|
||||
pub struct HighlightConfiguration {
|
||||
pub language: Language,
|
||||
pub query: Query,
|
||||
pub injections_query: Query,
|
||||
pub combined_injections_patterns: Vec<usize>,
|
||||
pub highlights_pattern_index: usize,
|
||||
pub highlight_indices: ArcSwap<Vec<Option<Highlight>>>,
|
||||
pub non_local_variable_patterns: Vec<bool>,
|
||||
pub injection_content_capture_index: Option<u32>,
|
||||
pub injection_language_capture_index: Option<u32>,
|
||||
pub injection_filename_capture_index: Option<u32>,
|
||||
pub injection_shebang_capture_index: Option<u32>,
|
||||
pub local_scope_capture_index: Option<u32>,
|
||||
pub local_def_capture_index: Option<u32>,
|
||||
pub local_def_value_capture_index: Option<u32>,
|
||||
pub local_ref_capture_index: Option<u32>,
|
||||
}
|
||||
|
||||
impl HighlightConfiguration {
|
||||
/// Creates a `HighlightConfiguration` for a given `Language` and set of highlighting
|
||||
/// queries.
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `language` - The Tree-sitter `Language` that should be used for parsing.
|
||||
/// * `highlights_query` - A string containing tree patterns for syntax highlighting. This
|
||||
/// should be non-empty, otherwise no syntax highlights will be added.
|
||||
/// * `injections_query` - A string containing tree patterns for injecting other languages
|
||||
/// into the document. This can be empty if no injections are desired.
|
||||
/// * `locals_query` - A string containing tree patterns for tracking local variable
|
||||
/// definitions and references. This can be empty if local variable tracking is not needed.
|
||||
///
|
||||
/// Returns a `HighlightConfiguration` that can then be used with the `highlight` method.
|
||||
pub fn new(
|
||||
language: Language,
|
||||
highlights_query: &str,
|
||||
injection_query: &str,
|
||||
locals_query: &str,
|
||||
) -> Result<Self, tree_sitter::QueryError> {
|
||||
// Concatenate the query strings, keeping track of the start offset of each section.
|
||||
let mut query_source = String::new();
|
||||
query_source.push_str(locals_query);
|
||||
let highlights_query_offset = query_source.len();
|
||||
query_source.push_str(highlights_query);
|
||||
|
||||
// Construct a single query by concatenating the three query strings, but record the
|
||||
// range of pattern indices that belong to each individual string.
|
||||
let query = Query::new(&language, &query_source)?;
|
||||
let mut highlights_pattern_index = 0;
|
||||
for i in 0..(query.pattern_count()) {
|
||||
let pattern_offset = query.start_byte_for_pattern(i);
|
||||
if pattern_offset < highlights_query_offset {
|
||||
highlights_pattern_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let injections_query = Query::new(&language, injection_query)?;
|
||||
let combined_injections_patterns = (0..injections_query.pattern_count())
|
||||
.filter(|&i| {
|
||||
injections_query
|
||||
.property_settings(i)
|
||||
.iter()
|
||||
.any(|s| &*s.key == "injection.combined")
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Find all of the highlighting patterns that are disabled for nodes that
|
||||
// have been identified as local variables.
|
||||
let non_local_variable_patterns = (0..query.pattern_count())
|
||||
.map(|i| {
|
||||
query.property_predicates(i).iter().any(|(prop, positive)| {
|
||||
!*positive && prop.key.as_ref() == "local"
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Store the numeric ids for all of the special captures.
|
||||
let mut injection_content_capture_index = None;
|
||||
let mut injection_language_capture_index = None;
|
||||
let mut injection_filename_capture_index = None;
|
||||
let mut injection_shebang_capture_index = None;
|
||||
let mut local_def_capture_index = None;
|
||||
let mut local_def_value_capture_index = None;
|
||||
let mut local_ref_capture_index = None;
|
||||
let mut local_scope_capture_index = None;
|
||||
for (i, name) in query.capture_names().iter().enumerate() {
|
||||
let i = Some(i as u32);
|
||||
match *name {
|
||||
"local.definition" => local_def_capture_index = i,
|
||||
"local.definition-value" => local_def_value_capture_index = i,
|
||||
"local.reference" => local_ref_capture_index = i,
|
||||
"local.scope" => local_scope_capture_index = i,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
for (i, name) in injections_query.capture_names().iter().enumerate() {
|
||||
let i = Some(i as u32);
|
||||
match *name {
|
||||
"injection.content" => injection_content_capture_index = i,
|
||||
"injection.language" => injection_language_capture_index = i,
|
||||
"injection.filename" => injection_filename_capture_index = i,
|
||||
"injection.shebang" => injection_shebang_capture_index = i,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let highlight_indices: ArcSwap<Vec<_>> =
|
||||
ArcSwap::from_pointee(vec![None; query.capture_names().len()]);
|
||||
let conf = Self {
|
||||
language,
|
||||
query,
|
||||
injections_query,
|
||||
combined_injections_patterns,
|
||||
highlights_pattern_index,
|
||||
highlight_indices,
|
||||
non_local_variable_patterns,
|
||||
injection_content_capture_index,
|
||||
injection_language_capture_index,
|
||||
injection_shebang_capture_index,
|
||||
injection_filename_capture_index,
|
||||
local_scope_capture_index,
|
||||
local_def_capture_index,
|
||||
local_def_value_capture_index,
|
||||
local_ref_capture_index,
|
||||
};
|
||||
conf.configure(SCOPES);
|
||||
Ok(conf)
|
||||
}
|
||||
|
||||
/// Get a slice containing all of the highlight names used in the configuration.
|
||||
pub fn names(&self) -> &[&str] {
|
||||
self.query.capture_names()
|
||||
}
|
||||
|
||||
/// Set the list of recognized highlight names.
|
||||
///
|
||||
/// Tree-sitter syntax-highlighting queries specify highlights in the form of dot-separated
|
||||
/// highlight names like `punctuation.bracket` and `function.method.builtin`. Consumers of
|
||||
/// these queries can choose to recognize highlights with different levels of specificity.
|
||||
/// For example, the string `function.builtin` will match against `function.builtin.constructor`
|
||||
/// but will not match `function.method.builtin` and `function.method`.
|
||||
///
|
||||
/// When highlighting, results are returned as `Highlight` values, which contain the index
|
||||
/// of the matched highlight this list of highlight names.
|
||||
pub fn configure(&self, recognized_names: &[&str]) {
|
||||
let mut capture_parts = Vec::new();
|
||||
let indices: Vec<_> = self
|
||||
.query
|
||||
.capture_names()
|
||||
.iter()
|
||||
.map(move |capture_name| {
|
||||
capture_parts.clear();
|
||||
capture_parts.extend(capture_name.split('.'));
|
||||
|
||||
let mut best_index = None;
|
||||
let mut best_match_len = 0;
|
||||
for (i, recognized_name) in recognized_names.iter().enumerate() {
|
||||
let mut len = 0;
|
||||
let mut matches = true;
|
||||
for (i, part) in recognized_name.split('.').enumerate() {
|
||||
match capture_parts.get(i) {
|
||||
Some(capture_part) if *capture_part == part => len += 1,
|
||||
_ => {
|
||||
matches = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if matches && len > best_match_len {
|
||||
best_index = Some(i);
|
||||
best_match_len = len;
|
||||
}
|
||||
}
|
||||
best_index.map(Highlight)
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.highlight_indices.store(Arc::new(indices));
|
||||
}
|
||||
|
||||
pub fn injection_pair<'a>(
|
||||
&self,
|
||||
query_match: &QueryMatch<'a, 'a>,
|
||||
source: &'a Rope,
|
||||
) -> (
|
||||
Option<InjectionLanguageMarker<'a>>,
|
||||
Option<tree_sitter::Node<'a>>,
|
||||
) {
|
||||
let mut injection_capture = None;
|
||||
let mut content_node = None;
|
||||
|
||||
for capture in query_match.captures {
|
||||
let index = Some(capture.index);
|
||||
if index == self.injection_language_capture_index {
|
||||
let name = source.slice_to_cow(capture.node.byte_range());
|
||||
injection_capture = Some(InjectionLanguageMarker::Name(name));
|
||||
} else if index == self.injection_filename_capture_index {
|
||||
let name = source.slice_to_cow(capture.node.byte_range());
|
||||
let path = Path::new(name.as_ref()).to_path_buf();
|
||||
injection_capture =
|
||||
Some(InjectionLanguageMarker::Filename(path.into()));
|
||||
} else if index == self.injection_shebang_capture_index {
|
||||
let node_slice = source.slice(capture.node.byte_range());
|
||||
let max_line = node_slice.line_of_offset(node_slice.len());
|
||||
let node_slice = if max_line > 0 {
|
||||
node_slice.slice(..node_slice.offset_of_line(1))
|
||||
} else {
|
||||
node_slice
|
||||
};
|
||||
|
||||
static SHEBANG_REGEX: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(SHEBANG).unwrap());
|
||||
|
||||
injection_capture = SHEBANG_REGEX
|
||||
.captures_iter(&node_slice.slice_to_cow(..))
|
||||
.map(|cap| InjectionLanguageMarker::Shebang(cap[1].to_string()))
|
||||
.next()
|
||||
} else if index == self.injection_content_capture_index {
|
||||
content_node = Some(capture.node);
|
||||
}
|
||||
}
|
||||
(injection_capture, content_node)
|
||||
}
|
||||
|
||||
pub(crate) fn injection_for_match<'a>(
|
||||
&self,
|
||||
query: &'a Query,
|
||||
query_match: &QueryMatch<'a, 'a>,
|
||||
source: &'a Rope,
|
||||
) -> (
|
||||
Option<InjectionLanguageMarker<'a>>,
|
||||
Option<tree_sitter::Node<'a>>,
|
||||
IncludedChildren,
|
||||
) {
|
||||
let (mut injection_capture, content_node) =
|
||||
self.injection_pair(query_match, source);
|
||||
|
||||
let mut included_children = IncludedChildren::default();
|
||||
for prop in query.property_settings(query_match.pattern_index) {
|
||||
match prop.key.as_ref() {
|
||||
// In addition to specifying the language name via the text of a
|
||||
// captured node, it can also be hard-coded via a `#set!` predicate
|
||||
// that sets the injection.language key.
|
||||
"injection.language" if injection_capture.is_none() => {
|
||||
injection_capture = prop
|
||||
.value
|
||||
.as_ref()
|
||||
.map(|s| InjectionLanguageMarker::Name(s.as_ref().into()));
|
||||
}
|
||||
|
||||
// By default, injections do not include the *children* of an
|
||||
// `injection.content` node - only the ranges that belong to the
|
||||
// node itself. This can be changed using a `#set!` predicate that
|
||||
// sets the `injection.include-children` key.
|
||||
"injection.include-children" => {
|
||||
included_children = IncludedChildren::All
|
||||
}
|
||||
|
||||
// Some queries might only exclude named children but include unnamed
|
||||
// children in their `injection.content` node. This can be enabled using
|
||||
// a `#set!` predicate that sets the `injection.include-unnamed-children` key.
|
||||
"injection.include-unnamed-children" => {
|
||||
included_children = IncludedChildren::Unnamed
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
(injection_capture, content_node, included_children)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct HighlightIter<'a> {
|
||||
pub(crate) source: &'a Rope,
|
||||
pub(crate) byte_offset: usize,
|
||||
pub(crate) cancellation_flag: Option<&'a AtomicUsize>,
|
||||
pub(crate) layers: Vec<HighlightIterLayer<'a>>,
|
||||
pub(crate) iter_count: usize,
|
||||
pub(crate) next_event: Option<HighlightEvent>,
|
||||
pub(crate) last_highlight_range: Option<(usize, usize, usize)>,
|
||||
}
|
||||
|
||||
pub(crate) struct HighlightIterLayer<'a> {
|
||||
pub(crate) _tree: Option<Tree>,
|
||||
pub(crate) cursor: QueryCursor,
|
||||
pub(crate) captures: RefCell<
|
||||
std::iter::Peekable<QueryCaptures<'a, 'a, RopeProvider<'a>, &'a [u8]>>,
|
||||
>,
|
||||
pub(crate) config: &'a HighlightConfiguration,
|
||||
pub(crate) highlight_end_stack: Vec<usize>,
|
||||
pub(crate) scope_stack: Vec<LocalScope<'a>>,
|
||||
pub(crate) depth: usize,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for HighlightIterLayer<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("HighlightIterLayer").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl HighlightIterLayer<'_> {
|
||||
// First, sort scope boundaries by their byte offset in the document. At a
|
||||
// given position, emit scope endings before scope beginnings. Finally, emit
|
||||
// scope boundaries from deeper layers first.
|
||||
pub fn sort_key(&self) -> Option<(usize, bool, isize)> {
|
||||
let depth = -(self.depth as isize);
|
||||
let next_start = self
|
||||
.captures
|
||||
.borrow_mut()
|
||||
.peek()
|
||||
.map(|(m, i)| m.captures[*i].node.start_byte());
|
||||
let next_end = self.highlight_end_stack.last().cloned();
|
||||
match (next_start, next_end) {
|
||||
(Some(start), Some(end)) => {
|
||||
if start < end {
|
||||
Some((start, true, depth))
|
||||
} else {
|
||||
Some((end, false, depth))
|
||||
}
|
||||
}
|
||||
(Some(i), None) => Some((i, true, depth)),
|
||||
(None, Some(j)) => Some((j, false, depth)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HighlightIter<'_> {
|
||||
fn emit_event(
|
||||
&mut self,
|
||||
offset: usize,
|
||||
event: Option<HighlightEvent>,
|
||||
) -> Option<Result<HighlightEvent, super::Error>> {
|
||||
let result;
|
||||
if self.byte_offset < offset {
|
||||
result = Some(Ok(HighlightEvent::Source {
|
||||
start: self.byte_offset,
|
||||
end: offset,
|
||||
}));
|
||||
self.byte_offset = offset;
|
||||
self.next_event = event;
|
||||
} else {
|
||||
result = event.map(Ok);
|
||||
}
|
||||
self.sort_layers();
|
||||
result
|
||||
}
|
||||
|
||||
pub(crate) fn sort_layers(&mut self) {
|
||||
while !self.layers.is_empty() {
|
||||
if let Some(sort_key) = self.layers[0].sort_key() {
|
||||
let mut i = 0;
|
||||
while i + 1 < self.layers.len() {
|
||||
if let Some(next_offset) = self.layers[i + 1].sort_key() {
|
||||
if next_offset < sort_key {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
let layer = self.layers.remove(i + 1);
|
||||
PARSER.with(|ts_parser| {
|
||||
let highlighter = &mut ts_parser.borrow_mut();
|
||||
highlighter.cursors.push(layer.cursor);
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
if i > 0 {
|
||||
self.layers[0..(i + 1)].rotate_left(1);
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
let layer = self.layers.remove(0);
|
||||
PARSER.with(|ts_parser| {
|
||||
let highlighter = &mut ts_parser.borrow_mut();
|
||||
highlighter.cursors.push(layer.cursor);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for HighlightIter<'_> {
|
||||
type Item = Result<HighlightEvent, super::Error>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
'main: loop {
|
||||
// If we've already determined the next highlight boundary, just return it.
|
||||
if let Some(e) = self.next_event.take() {
|
||||
return Some(Ok(e));
|
||||
}
|
||||
|
||||
// Periodically check for cancellation, returning `Cancelled` error if the
|
||||
// cancellation flag was flipped.
|
||||
if let Some(cancellation_flag) = self.cancellation_flag {
|
||||
self.iter_count += 1;
|
||||
if self.iter_count >= CANCELLATION_CHECK_INTERVAL {
|
||||
self.iter_count = 0;
|
||||
if cancellation_flag.load(Ordering::Relaxed) != 0 {
|
||||
return Some(Err(super::Error::Cancelled));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If none of the layers have any more highlight boundaries, terminate.
|
||||
if self.layers.is_empty() {
|
||||
let len = self.source.len();
|
||||
return if self.byte_offset < len {
|
||||
let result = Some(Ok(HighlightEvent::Source {
|
||||
start: self.byte_offset,
|
||||
end: len,
|
||||
}));
|
||||
self.byte_offset = len;
|
||||
result
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
|
||||
// Get the next capture from whichever layer has the earliest highlight boundary.
|
||||
let range;
|
||||
let layer = &mut self.layers[0];
|
||||
let captures = layer.captures.get_mut();
|
||||
if let Some((next_match, capture_index)) = captures.peek() {
|
||||
let next_capture = next_match.captures[*capture_index];
|
||||
range = next_capture.node.byte_range();
|
||||
|
||||
// If any previous highlight ends before this node starts, then before
|
||||
// processing this capture, emit the source code up until the end of the
|
||||
// previous highlight, and an end event for that highlight.
|
||||
if let Some(end_byte) = layer.highlight_end_stack.last().cloned() {
|
||||
if end_byte <= range.start {
|
||||
layer.highlight_end_stack.pop();
|
||||
return self.emit_event(
|
||||
end_byte,
|
||||
Some(HighlightEvent::HighlightEnd),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
// If there are no more captures, then emit any remaining highlight end events.
|
||||
// And if there are none of those, then just advance to the end of the document.
|
||||
else if let Some(end_byte) = layer.highlight_end_stack.last().cloned()
|
||||
{
|
||||
layer.highlight_end_stack.pop();
|
||||
return self
|
||||
.emit_event(end_byte, Some(HighlightEvent::HighlightEnd));
|
||||
} else {
|
||||
return self.emit_event(self.source.len(), None);
|
||||
};
|
||||
|
||||
let (mut match_, capture_index) = captures.next().unwrap();
|
||||
let mut capture = match_.captures[capture_index];
|
||||
|
||||
// Remove from the local scope stack any local scopes that have already ended.
|
||||
while range.start > layer.scope_stack.last().unwrap().range.end {
|
||||
layer.scope_stack.pop();
|
||||
}
|
||||
|
||||
// If this capture is for tracking local variables, then process the
|
||||
// local variable info.
|
||||
let mut reference_highlight = None;
|
||||
let mut definition_highlight = None;
|
||||
while match_.pattern_index < layer.config.highlights_pattern_index {
|
||||
// If the node represents a local scope, push a new local scope onto
|
||||
// the scope stack.
|
||||
if Some(capture.index) == layer.config.local_scope_capture_index {
|
||||
definition_highlight = None;
|
||||
let mut scope = LocalScope {
|
||||
inherits: true,
|
||||
range: range.clone(),
|
||||
local_defs: Vec::new(),
|
||||
};
|
||||
for prop in
|
||||
layer.config.query.property_settings(match_.pattern_index)
|
||||
{
|
||||
if let "local.scope-inherits" = prop.key.as_ref() {
|
||||
scope.inherits = prop
|
||||
.value
|
||||
.as_ref()
|
||||
.is_none_or(|r| r.as_ref() == "true");
|
||||
}
|
||||
}
|
||||
layer.scope_stack.push(scope);
|
||||
}
|
||||
// If the node represents a definition, add a new definition to the
|
||||
// local scope at the top of the scope stack.
|
||||
else if Some(capture.index) == layer.config.local_def_capture_index
|
||||
{
|
||||
reference_highlight = None;
|
||||
let scope = layer.scope_stack.last_mut().unwrap();
|
||||
|
||||
let mut value_range = 0..0;
|
||||
for capture in match_.captures {
|
||||
if Some(capture.index)
|
||||
== layer.config.local_def_value_capture_index
|
||||
{
|
||||
value_range = capture.node.byte_range();
|
||||
}
|
||||
}
|
||||
|
||||
let name = self.source.slice_to_cow(range.clone());
|
||||
scope.local_defs.push(LocalDef {
|
||||
name,
|
||||
value_range,
|
||||
highlight: None,
|
||||
});
|
||||
definition_highlight =
|
||||
scope.local_defs.last_mut().map(|s| &mut s.highlight);
|
||||
}
|
||||
// If the node represents a reference, then try to find the corresponding
|
||||
// definition in the scope stack.
|
||||
else if Some(capture.index) == layer.config.local_ref_capture_index
|
||||
&& definition_highlight.is_none()
|
||||
{
|
||||
definition_highlight = None;
|
||||
let name = self.source.slice_to_cow(range.clone());
|
||||
for scope in layer.scope_stack.iter().rev() {
|
||||
if let Some(highlight) =
|
||||
scope.local_defs.iter().rev().find_map(|def| {
|
||||
if def.name == name
|
||||
&& range.start >= def.value_range.end
|
||||
{
|
||||
Some(def.highlight)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
{
|
||||
reference_highlight = highlight;
|
||||
break;
|
||||
}
|
||||
if !scope.inherits {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Continue processing any additional matches for the same node.
|
||||
if let Some((next_match, next_capture_index)) = captures.peek() {
|
||||
let next_capture = next_match.captures[*next_capture_index];
|
||||
if next_capture.node == capture.node {
|
||||
capture = next_capture;
|
||||
match_ = captures.next().unwrap().0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
self.sort_layers();
|
||||
continue 'main;
|
||||
}
|
||||
|
||||
// Otherwise, this capture must represent a highlight.
|
||||
// If this exact range has already been highlighted by an earlier pattern, or by
|
||||
// a different layer, then skip over this one.
|
||||
if let Some((last_start, last_end, last_depth)) =
|
||||
self.last_highlight_range
|
||||
{
|
||||
if range.start == last_start
|
||||
&& range.end == last_end
|
||||
&& layer.depth < last_depth
|
||||
{
|
||||
self.sort_layers();
|
||||
continue 'main;
|
||||
}
|
||||
}
|
||||
|
||||
// If the current node was found to be a local variable, then skip over any
|
||||
// highlighting patterns that are disabled for local variables.
|
||||
if definition_highlight.is_some() || reference_highlight.is_some() {
|
||||
while layer.config.non_local_variable_patterns[match_.pattern_index]
|
||||
{
|
||||
if let Some((next_match, next_capture_index)) = captures.peek() {
|
||||
let next_capture = next_match.captures[*next_capture_index];
|
||||
if next_capture.node == capture.node {
|
||||
capture = next_capture;
|
||||
match_ = captures.next().unwrap().0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
self.sort_layers();
|
||||
continue 'main;
|
||||
}
|
||||
}
|
||||
|
||||
// Once a highlighting pattern is found for the current node, skip over
|
||||
// any later highlighting patterns that also match this node. Captures
|
||||
// for a given node are ordered by pattern index, so these subsequent
|
||||
// captures are guaranteed to be for highlighting, not injections or
|
||||
// local variables.
|
||||
while let Some((next_match, next_capture_index)) = captures.peek() {
|
||||
let next_capture = next_match.captures[*next_capture_index];
|
||||
if next_capture.node == capture.node {
|
||||
captures.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let current_highlight =
|
||||
layer.config.highlight_indices.load()[capture.index as usize];
|
||||
|
||||
// If this node represents a local definition, then store the current
|
||||
// highlight value on the local scope entry representing this node.
|
||||
if let Some(definition_highlight) = definition_highlight {
|
||||
*definition_highlight = current_highlight;
|
||||
}
|
||||
|
||||
// Emit a scope start event and push the node's end position to the stack.
|
||||
if let Some(highlight) = reference_highlight.or(current_highlight) {
|
||||
self.last_highlight_range =
|
||||
Some((range.start, range.end, layer.depth));
|
||||
layer.highlight_end_stack.push(range.end);
|
||||
return self.emit_event(
|
||||
range.start,
|
||||
Some(HighlightEvent::HighlightStart(highlight)),
|
||||
);
|
||||
}
|
||||
|
||||
self.sort_layers();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) enum IncludedChildren {
|
||||
#[default]
|
||||
None,
|
||||
All,
|
||||
Unnamed,
|
||||
}
|
||||
|
||||
// Compute the ranges that should be included when parsing an injection.
|
||||
// This takes into account three things:
|
||||
// * `parent_ranges` - The ranges must all fall within the *current* layer's ranges.
|
||||
// * `nodes` - Every injection takes place within a set of nodes. The injection ranges
|
||||
// are the ranges of those nodes.
|
||||
// * `includes_children` - For some injections, the content nodes' children should be
|
||||
// excluded from the nested document, so that only the content nodes' *own* content
|
||||
// is reparsed. For other injections, the content nodes' entire ranges should be
|
||||
// reparsed, including the ranges of their children.
|
||||
pub(crate) fn intersect_ranges(
|
||||
parent_ranges: &[tree_sitter::Range],
|
||||
nodes: &[tree_sitter::Node],
|
||||
included_children: IncludedChildren,
|
||||
) -> Vec<tree_sitter::Range> {
|
||||
let mut cursor = nodes[0].walk();
|
||||
let mut result = Vec::new();
|
||||
let mut parent_range_iter = parent_ranges.iter();
|
||||
let mut parent_range = parent_range_iter
|
||||
.next()
|
||||
.expect("Layers should only be constructed with non-empty ranges vectors");
|
||||
for node in nodes.iter() {
|
||||
let mut preceding_range = tree_sitter::Range {
|
||||
start_byte: 0,
|
||||
start_point: Point::new(0, 0),
|
||||
end_byte: node.start_byte(),
|
||||
end_point: node.start_position(),
|
||||
};
|
||||
let following_range = tree_sitter::Range {
|
||||
start_byte: node.end_byte(),
|
||||
start_point: node.end_position(),
|
||||
end_byte: usize::MAX,
|
||||
end_point: Point::new(usize::MAX, usize::MAX),
|
||||
};
|
||||
|
||||
for excluded_range in node
|
||||
.children(&mut cursor)
|
||||
.filter_map(|child| match included_children {
|
||||
IncludedChildren::None => Some(child.range()),
|
||||
IncludedChildren::All => None,
|
||||
IncludedChildren::Unnamed => {
|
||||
if child.is_named() {
|
||||
Some(child.range())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
.chain([following_range].iter().cloned())
|
||||
{
|
||||
let mut range = tree_sitter::Range {
|
||||
start_byte: preceding_range.end_byte,
|
||||
start_point: preceding_range.end_point,
|
||||
end_byte: excluded_range.start_byte,
|
||||
end_point: excluded_range.start_point,
|
||||
};
|
||||
preceding_range = excluded_range;
|
||||
|
||||
if range.end_byte < parent_range.start_byte {
|
||||
continue;
|
||||
}
|
||||
|
||||
while parent_range.start_byte <= range.end_byte {
|
||||
if parent_range.end_byte > range.start_byte {
|
||||
if range.start_byte < parent_range.start_byte {
|
||||
range.start_byte = parent_range.start_byte;
|
||||
range.start_point = parent_range.start_point;
|
||||
}
|
||||
|
||||
if parent_range.end_byte < range.end_byte {
|
||||
if range.start_byte < parent_range.end_byte {
|
||||
result.push(tree_sitter::Range {
|
||||
start_byte: range.start_byte,
|
||||
start_point: range.start_point,
|
||||
end_byte: parent_range.end_byte,
|
||||
end_point: parent_range.end_point,
|
||||
});
|
||||
}
|
||||
range.start_byte = parent_range.end_byte;
|
||||
range.start_point = parent_range.end_point;
|
||||
} else {
|
||||
if range.start_byte < range.end_byte {
|
||||
result.push(range);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(next_range) = parent_range_iter.next() {
|
||||
parent_range = next_range;
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
use lapce_xi_rope::{Rope, rope::ChunkIter};
|
||||
use tree_sitter::TextProvider;
|
||||
|
||||
pub struct RopeChunksIterBytes<'a> {
|
||||
chunks: ChunkIter<'a>,
|
||||
}
|
||||
impl<'a> Iterator for RopeChunksIterBytes<'a> {
|
||||
type Item = &'a [u8];
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.chunks.next().map(str::as_bytes)
|
||||
}
|
||||
}
|
||||
|
||||
/// This allows tree-sitter to iterate over our Rope without us having to convert it into
|
||||
/// a contiguous byte-list.
|
||||
pub struct RopeProvider<'a>(pub &'a Rope);
|
||||
impl<'a> TextProvider<&'a [u8]> for RopeProvider<'a> {
|
||||
type I = RopeChunksIterBytes<'a>;
|
||||
|
||||
fn text(&mut self, node: tree_sitter::Node) -> Self::I {
|
||||
let start = node.start_byte();
|
||||
let end = node.end_byte().min(self.0.len());
|
||||
let chunks = self.0.iter_chunks(start..end);
|
||||
RopeChunksIterBytes { chunks }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user