mirror of
https://github.com/nushell/reedline.git
synced 2024-09-11 15:55:50 +03:00
Folderize painting code
- Move painter and styled_text into a common folder - Separate out `PromptLines` and freestanding utility functions
This commit is contained in:
parent
ad4760e476
commit
5bc9d6c747
@ -10,7 +10,7 @@ use {
|
||||
hinter::{DefaultHinter, Hinter},
|
||||
history::{FileBackedHistory, History, HistoryNavigationQuery},
|
||||
menu::{parse_selection_char, Menu, MenuEvent},
|
||||
painter::{Painter, PromptLines},
|
||||
painting::{Painter, PromptLines},
|
||||
prompt::{PromptEditMode, PromptHistorySearchStatus},
|
||||
text_manipulation, DefaultValidator, EditCommand, ExampleHighlighter, Highlighter, Prompt,
|
||||
PromptHistorySearch, Signal, ValidationResult, Validator,
|
||||
|
@ -1,7 +1,7 @@
|
||||
mod example;
|
||||
mod simple_match;
|
||||
|
||||
use crate::styled_text::StyledText;
|
||||
use crate::StyledText;
|
||||
|
||||
pub use example::ExampleHighlighter;
|
||||
pub use simple_match::SimpleMatchHighlighter;
|
||||
|
@ -185,7 +185,8 @@ mod text_manipulation;
|
||||
mod enums;
|
||||
pub use enums::{EditCommand, ReedlineEvent, Signal, UndoBehavior};
|
||||
|
||||
mod painter;
|
||||
mod painting;
|
||||
pub use painting::StyledText;
|
||||
|
||||
mod engine;
|
||||
pub use engine::Reedline;
|
||||
@ -208,9 +209,6 @@ pub use edit_mode::{
|
||||
mod highlighter;
|
||||
pub use highlighter::{ExampleHighlighter, Highlighter, SimpleMatchHighlighter};
|
||||
|
||||
mod styled_text;
|
||||
pub use styled_text::StyledText;
|
||||
|
||||
mod completion;
|
||||
pub use completion::{Completer, DefaultCompleter, Span};
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
use super::{find_common_string, Menu, MenuEvent, MenuTextStyle};
|
||||
use crate::{painter::Painter, Completer, History, LineBuffer, Span};
|
||||
use crate::{painting::Painter, Completer, History, LineBuffer, Span};
|
||||
use nu_ansi_term::{ansi::RESET, Style};
|
||||
|
||||
/// Default values used as reference for the menu. These values are set during
|
||||
|
@ -1,6 +1,6 @@
|
||||
use super::{parse_selection_char, Menu, MenuEvent, MenuTextStyle};
|
||||
use crate::{
|
||||
painter::{estimate_single_line_wraps, Painter},
|
||||
painting::{estimate_single_line_wraps, Painter},
|
||||
Completer, History, LineBuffer, Span,
|
||||
};
|
||||
use nu_ansi_term::{ansi::RESET, Style};
|
||||
|
@ -1,7 +1,7 @@
|
||||
mod completion_menu;
|
||||
mod history_menu;
|
||||
|
||||
use crate::{painter::Painter, Completer, History, LineBuffer, Span};
|
||||
use crate::{painting::Painter, Completer, History, LineBuffer, Span};
|
||||
pub use completion_menu::CompletionMenu;
|
||||
pub use history_menu::HistoryMenu;
|
||||
use nu_ansi_term::{Color, Style};
|
||||
|
9
src/painting/mod.rs
Normal file
9
src/painting/mod.rs
Normal file
@ -0,0 +1,9 @@
|
||||
mod painter;
|
||||
mod prompt_lines;
|
||||
mod styled_text;
|
||||
mod utils;
|
||||
|
||||
pub use painter::Painter;
|
||||
pub(crate) use prompt_lines::PromptLines;
|
||||
pub use styled_text::StyledText;
|
||||
pub(crate) use utils::estimate_single_line_wraps;
|
@ -1,158 +1,16 @@
|
||||
use super::utils::{coerce_crlf, line_width};
|
||||
|
||||
use {
|
||||
crate::{
|
||||
menu::Menu, prompt::PromptEditMode, styled_text::strip_ansi, Prompt, PromptHistorySearch,
|
||||
},
|
||||
crate::{menu::Menu, painting::PromptLines, Prompt},
|
||||
crossterm::{
|
||||
cursor::{self, MoveTo, RestorePosition, SavePosition},
|
||||
style::{Print, ResetColor, SetForegroundColor},
|
||||
terminal::{self, Clear, ClearType, ScrollUp},
|
||||
QueueableCommand, Result,
|
||||
},
|
||||
std::borrow::Cow,
|
||||
std::io::Write,
|
||||
unicode_width::UnicodeWidthStr,
|
||||
};
|
||||
|
||||
pub struct PromptLines<'prompt> {
|
||||
prompt_str_left: Cow<'prompt, str>,
|
||||
prompt_str_right: Cow<'prompt, str>,
|
||||
prompt_indicator: Cow<'prompt, str>,
|
||||
before_cursor: Cow<'prompt, str>,
|
||||
after_cursor: Cow<'prompt, str>,
|
||||
hint: Cow<'prompt, str>,
|
||||
}
|
||||
|
||||
impl<'prompt> PromptLines<'prompt> {
|
||||
/// Splits the strings before and after the cursor as well as the hint
|
||||
/// This vector with the str are used to calculate how many lines are
|
||||
/// required to print after the prompt
|
||||
pub fn new(
|
||||
prompt: &'prompt dyn Prompt,
|
||||
prompt_mode: PromptEditMode,
|
||||
history_indicator: Option<PromptHistorySearch>,
|
||||
before_cursor: &'prompt str,
|
||||
after_cursor: &'prompt str,
|
||||
hint: &'prompt str,
|
||||
) -> Self {
|
||||
let prompt_str_left = prompt.render_prompt_left();
|
||||
let prompt_str_right = prompt.render_prompt_right();
|
||||
|
||||
let prompt_indicator = match history_indicator {
|
||||
Some(prompt_search) => prompt.render_prompt_history_search_indicator(prompt_search),
|
||||
None => prompt.render_prompt_indicator(prompt_mode),
|
||||
};
|
||||
|
||||
let before_cursor = coerce_crlf(before_cursor);
|
||||
let after_cursor = coerce_crlf(after_cursor);
|
||||
let hint = coerce_crlf(hint);
|
||||
|
||||
Self {
|
||||
prompt_str_left,
|
||||
prompt_str_right,
|
||||
prompt_indicator,
|
||||
before_cursor,
|
||||
after_cursor,
|
||||
hint,
|
||||
}
|
||||
}
|
||||
|
||||
/// The required lines to paint the buffer are calculated by counting the
|
||||
/// number of newlines in all the strings that form the prompt and buffer.
|
||||
/// The plus 1 is to indicate that there should be at least one line.
|
||||
fn required_lines(&self, terminal_columns: u16, menu: Option<&dyn Menu>) -> u16 {
|
||||
let input = if menu.is_none() {
|
||||
self.prompt_str_left.to_string()
|
||||
+ &self.prompt_indicator
|
||||
+ &self.before_cursor
|
||||
+ &self.after_cursor
|
||||
+ &self.hint
|
||||
} else {
|
||||
self.prompt_str_left.to_string()
|
||||
+ &self.prompt_indicator
|
||||
+ &self.before_cursor
|
||||
+ &self.after_cursor
|
||||
};
|
||||
|
||||
let lines = estimate_required_lines(&input, terminal_columns);
|
||||
|
||||
if let Some(menu) = menu {
|
||||
lines as u16 + menu.menu_required_lines(terminal_columns)
|
||||
} else {
|
||||
lines as u16
|
||||
}
|
||||
}
|
||||
|
||||
/// Estimated distance of the cursor to the prompt.
|
||||
/// This considers line wrapping
|
||||
fn distance_from_prompt(&self, terminal_columns: u16) -> u16 {
|
||||
let input = self.prompt_str_left.to_string() + &self.prompt_indicator + &self.before_cursor;
|
||||
let lines = estimate_required_lines(&input, terminal_columns);
|
||||
lines.saturating_sub(1) as u16
|
||||
}
|
||||
|
||||
/// Total lines that the prompt uses considering that it may wrap the screen
|
||||
fn prompt_lines_with_wrap(&self, screen_width: u16) -> u16 {
|
||||
let complete_prompt = self.prompt_str_left.to_string() + &self.prompt_indicator;
|
||||
let lines = estimate_required_lines(&complete_prompt, screen_width);
|
||||
lines.saturating_sub(1) as u16
|
||||
}
|
||||
|
||||
/// Estimated width of the actual input
|
||||
fn estimate_first_input_line_width(&self) -> u16 {
|
||||
let last_line_left_prompt = self.prompt_str_left.lines().last();
|
||||
|
||||
let prompt_lines_total = self.before_cursor.to_string() + &self.after_cursor + &self.hint;
|
||||
let prompt_lines_first = prompt_lines_total.lines().next();
|
||||
|
||||
let mut estimate = 0; // space in front of the input
|
||||
|
||||
if let Some(last_line_left_prompt) = last_line_left_prompt {
|
||||
estimate += line_width(last_line_left_prompt);
|
||||
}
|
||||
|
||||
estimate += line_width(&self.prompt_indicator);
|
||||
|
||||
if let Some(prompt_lines_first) = prompt_lines_first {
|
||||
estimate += line_width(prompt_lines_first);
|
||||
}
|
||||
|
||||
if estimate > u16::MAX as usize {
|
||||
u16::MAX
|
||||
} else {
|
||||
estimate as u16
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn estimate_required_lines(input: &str, screen_width: u16) -> usize {
|
||||
input.lines().fold(0, |acc, line| {
|
||||
let wrap = estimate_single_line_wraps(line, screen_width);
|
||||
|
||||
acc + 1 + wrap
|
||||
})
|
||||
}
|
||||
|
||||
/// Reports the additional lines needed due to wrapping for the given line.
|
||||
///
|
||||
/// Does not account for any potential linebreaks in `line`
|
||||
///
|
||||
/// If `line` fits in `terminal_columns` returns 0
|
||||
pub(crate) fn estimate_single_line_wraps(line: &str, terminal_columns: u16) -> usize {
|
||||
let estimated_width = line_width(line);
|
||||
let terminal_columns: usize = terminal_columns.into();
|
||||
|
||||
// integer ceiling rounding division for positive divisors
|
||||
let estimated_line_count = (estimated_width + terminal_columns - 1) / terminal_columns;
|
||||
|
||||
// Any wrapping will add to our overall line count
|
||||
estimated_line_count.saturating_sub(1)
|
||||
}
|
||||
|
||||
/// Compute the line width for ANSI escaped text
|
||||
fn line_width(line: &str) -> usize {
|
||||
strip_ansi(line).width()
|
||||
}
|
||||
|
||||
// Returns a string that skips N number of lines with the next offset of lines
|
||||
// An offset of 0 would return only one line after skipping the required lines
|
||||
fn skip_buffer_lines(string: &str, skip: usize, offset: Option<usize>) -> &str {
|
||||
@ -181,33 +39,6 @@ fn skip_buffer_lines(string: &str, skip: usize, offset: Option<usize>) -> &str {
|
||||
string[index..limit].trim_end_matches('\n')
|
||||
}
|
||||
|
||||
fn coerce_crlf(input: &str) -> Cow<str> {
|
||||
let mut result = Cow::Borrowed(input);
|
||||
let mut cursor: usize = 0;
|
||||
for (idx, _) in input.match_indices('\n') {
|
||||
if !(idx > 0 && input.as_bytes()[idx - 1] == b'\r') {
|
||||
if let Cow::Borrowed(_) = result {
|
||||
// Best case 1 allocation, worst case 2 allocations
|
||||
let mut owned = String::with_capacity(input.len() + 1);
|
||||
// Optimization to avoid the `AddAssign for Cow<str>`
|
||||
// optimization for `Cow<str>.is_empty` that would replace the
|
||||
// preallocation
|
||||
owned.push_str(&input[cursor..idx]);
|
||||
result = Cow::Owned(owned);
|
||||
} else {
|
||||
result += &input[cursor..idx];
|
||||
}
|
||||
result += "\r\n";
|
||||
// Advance beyond the matched LF char (single byte)
|
||||
cursor = idx + 1;
|
||||
}
|
||||
}
|
||||
if let Cow::Owned(_) = result {
|
||||
result += &input[cursor..input.len()];
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// the type used by crossterm operations
|
||||
pub type W = std::io::BufWriter<std::io::Stderr>;
|
||||
|
||||
@ -594,7 +425,6 @@ impl Painter {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
#[test]
|
||||
fn test_skip_lines() {
|
||||
@ -646,23 +476,4 @@ mod tests {
|
||||
assert_eq!(skip_buffer_lines(string, 0, Some(0)), "sentence1",);
|
||||
assert_eq!(skip_buffer_lines(string, 1, Some(0)), "sentence2",);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case("sentence\nsentence", "sentence\r\nsentence")]
|
||||
#[case("sentence\r\nsentence", "sentence\r\nsentence")]
|
||||
#[case("sentence\nsentence\n", "sentence\r\nsentence\r\n")]
|
||||
#[case("😇\nsentence", "😇\r\nsentence")]
|
||||
#[case("sentence\n😇", "sentence\r\n😇")]
|
||||
#[case("\n", "\r\n")]
|
||||
#[case("", "")]
|
||||
fn test_coerce_crlf(#[case] input: &str, #[case] expected: &str) {
|
||||
let result = coerce_crlf(input);
|
||||
|
||||
assert_eq!(result, expected);
|
||||
|
||||
assert!(
|
||||
input != expected || matches!(result, Cow::Borrowed(_)),
|
||||
"Unnecessary allocation"
|
||||
)
|
||||
}
|
||||
}
|
114
src/painting/prompt_lines.rs
Normal file
114
src/painting/prompt_lines.rs
Normal file
@ -0,0 +1,114 @@
|
||||
use super::utils::{coerce_crlf, estimate_required_lines, line_width};
|
||||
use crate::{menu::Menu, prompt::PromptEditMode, Prompt, PromptHistorySearch};
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub struct PromptLines<'prompt> {
|
||||
pub(crate) prompt_str_left: Cow<'prompt, str>,
|
||||
pub(crate) prompt_str_right: Cow<'prompt, str>,
|
||||
pub(crate) prompt_indicator: Cow<'prompt, str>,
|
||||
pub(crate) before_cursor: Cow<'prompt, str>,
|
||||
pub(crate) after_cursor: Cow<'prompt, str>,
|
||||
pub(crate) hint: Cow<'prompt, str>,
|
||||
}
|
||||
|
||||
impl<'prompt> PromptLines<'prompt> {
|
||||
/// Splits the strings before and after the cursor as well as the hint
|
||||
/// This vector with the str are used to calculate how many lines are
|
||||
/// required to print after the prompt
|
||||
pub fn new(
|
||||
prompt: &'prompt dyn Prompt,
|
||||
prompt_mode: PromptEditMode,
|
||||
history_indicator: Option<PromptHistorySearch>,
|
||||
before_cursor: &'prompt str,
|
||||
after_cursor: &'prompt str,
|
||||
hint: &'prompt str,
|
||||
) -> Self {
|
||||
let prompt_str_left = prompt.render_prompt_left();
|
||||
let prompt_str_right = prompt.render_prompt_right();
|
||||
|
||||
let prompt_indicator = match history_indicator {
|
||||
Some(prompt_search) => prompt.render_prompt_history_search_indicator(prompt_search),
|
||||
None => prompt.render_prompt_indicator(prompt_mode),
|
||||
};
|
||||
|
||||
let before_cursor = coerce_crlf(before_cursor);
|
||||
let after_cursor = coerce_crlf(after_cursor);
|
||||
let hint = coerce_crlf(hint);
|
||||
|
||||
Self {
|
||||
prompt_str_left,
|
||||
prompt_str_right,
|
||||
prompt_indicator,
|
||||
before_cursor,
|
||||
after_cursor,
|
||||
hint,
|
||||
}
|
||||
}
|
||||
|
||||
/// The required lines to paint the buffer are calculated by counting the
|
||||
/// number of newlines in all the strings that form the prompt and buffer.
|
||||
/// The plus 1 is to indicate that there should be at least one line.
|
||||
pub(crate) fn required_lines(&self, terminal_columns: u16, menu: Option<&dyn Menu>) -> u16 {
|
||||
let input = if menu.is_none() {
|
||||
self.prompt_str_left.to_string()
|
||||
+ &self.prompt_indicator
|
||||
+ &self.before_cursor
|
||||
+ &self.after_cursor
|
||||
+ &self.hint
|
||||
} else {
|
||||
self.prompt_str_left.to_string()
|
||||
+ &self.prompt_indicator
|
||||
+ &self.before_cursor
|
||||
+ &self.after_cursor
|
||||
};
|
||||
|
||||
let lines = estimate_required_lines(&input, terminal_columns);
|
||||
|
||||
if let Some(menu) = menu {
|
||||
lines as u16 + menu.menu_required_lines(terminal_columns)
|
||||
} else {
|
||||
lines as u16
|
||||
}
|
||||
}
|
||||
|
||||
/// Estimated distance of the cursor to the prompt.
|
||||
/// This considers line wrapping
|
||||
pub(crate) fn distance_from_prompt(&self, terminal_columns: u16) -> u16 {
|
||||
let input = self.prompt_str_left.to_string() + &self.prompt_indicator + &self.before_cursor;
|
||||
let lines = estimate_required_lines(&input, terminal_columns);
|
||||
lines.saturating_sub(1) as u16
|
||||
}
|
||||
|
||||
/// Total lines that the prompt uses considering that it may wrap the screen
|
||||
pub(crate) fn prompt_lines_with_wrap(&self, screen_width: u16) -> u16 {
|
||||
let complete_prompt = self.prompt_str_left.to_string() + &self.prompt_indicator;
|
||||
let lines = estimate_required_lines(&complete_prompt, screen_width);
|
||||
lines.saturating_sub(1) as u16
|
||||
}
|
||||
|
||||
/// Estimated width of the actual input
|
||||
pub(crate) fn estimate_first_input_line_width(&self) -> u16 {
|
||||
let last_line_left_prompt = self.prompt_str_left.lines().last();
|
||||
|
||||
let prompt_lines_total = self.before_cursor.to_string() + &self.after_cursor + &self.hint;
|
||||
let prompt_lines_first = prompt_lines_total.lines().next();
|
||||
|
||||
let mut estimate = 0; // space in front of the input
|
||||
|
||||
if let Some(last_line_left_prompt) = last_line_left_prompt {
|
||||
estimate += line_width(last_line_left_prompt);
|
||||
}
|
||||
|
||||
estimate += line_width(&self.prompt_indicator);
|
||||
|
||||
if let Some(prompt_lines_first) = prompt_lines_first {
|
||||
estimate += line_width(prompt_lines_first);
|
||||
}
|
||||
|
||||
if estimate > u16::MAX as usize {
|
||||
u16::MAX
|
||||
} else {
|
||||
estimate as u16
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
use super::utils::strip_ansi;
|
||||
use nu_ansi_term::{Color, Style};
|
||||
|
||||
/// A representation of a buffer with styling, used for doing syntax highlighting
|
||||
@ -86,16 +87,6 @@ impl StyledText {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns string with the ANSI escape codes removed
|
||||
///
|
||||
/// If parsing fails silently returns the input string
|
||||
pub(crate) fn strip_ansi(string: &str) -> String {
|
||||
strip_ansi_escapes::strip(string)
|
||||
.map_err(|_| ())
|
||||
.and_then(|x| String::from_utf8(x).map_err(|_| ()))
|
||||
.unwrap_or_else(|_| string.to_owned())
|
||||
}
|
||||
|
||||
fn render_as_string(
|
||||
renderable: &(Style, String),
|
||||
prompt_style: &Style,
|
98
src/painting/utils.rs
Normal file
98
src/painting/utils.rs
Normal file
@ -0,0 +1,98 @@
|
||||
use std::borrow::Cow;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
/// Ensures input uses CRLF line endings.
|
||||
///
|
||||
/// Needed for correct output in raw mode.
|
||||
/// Only replaces solitary LF with CRLF.
|
||||
pub(crate) fn coerce_crlf(input: &str) -> Cow<str> {
|
||||
let mut result = Cow::Borrowed(input);
|
||||
let mut cursor: usize = 0;
|
||||
for (idx, _) in input.match_indices('\n') {
|
||||
if !(idx > 0 && input.as_bytes()[idx - 1] == b'\r') {
|
||||
if let Cow::Borrowed(_) = result {
|
||||
// Best case 1 allocation, worst case 2 allocations
|
||||
let mut owned = String::with_capacity(input.len() + 1);
|
||||
// Optimization to avoid the `AddAssign for Cow<str>`
|
||||
// optimization for `Cow<str>.is_empty` that would replace the
|
||||
// preallocation
|
||||
owned.push_str(&input[cursor..idx]);
|
||||
result = Cow::Owned(owned);
|
||||
} else {
|
||||
result += &input[cursor..idx];
|
||||
}
|
||||
result += "\r\n";
|
||||
// Advance beyond the matched LF char (single byte)
|
||||
cursor = idx + 1;
|
||||
}
|
||||
}
|
||||
if let Cow::Owned(_) = result {
|
||||
result += &input[cursor..input.len()];
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Returns string with the ANSI escape codes removed
|
||||
///
|
||||
/// If parsing fails silently returns the input string
|
||||
pub(crate) fn strip_ansi(string: &str) -> String {
|
||||
strip_ansi_escapes::strip(string)
|
||||
.map_err(|_| ())
|
||||
.and_then(|x| String::from_utf8(x).map_err(|_| ()))
|
||||
.unwrap_or_else(|_| string.to_owned())
|
||||
}
|
||||
|
||||
pub(crate) fn estimate_required_lines(input: &str, screen_width: u16) -> usize {
|
||||
input.lines().fold(0, |acc, line| {
|
||||
let wrap = estimate_single_line_wraps(line, screen_width);
|
||||
|
||||
acc + 1 + wrap
|
||||
})
|
||||
}
|
||||
|
||||
/// Reports the additional lines needed due to wrapping for the given line.
|
||||
///
|
||||
/// Does not account for any potential linebreaks in `line`
|
||||
///
|
||||
/// If `line` fits in `terminal_columns` returns 0
|
||||
pub(crate) fn estimate_single_line_wraps(line: &str, terminal_columns: u16) -> usize {
|
||||
let estimated_width = line_width(line);
|
||||
let terminal_columns: usize = terminal_columns.into();
|
||||
|
||||
// integer ceiling rounding division for positive divisors
|
||||
let estimated_line_count = (estimated_width + terminal_columns - 1) / terminal_columns;
|
||||
|
||||
// Any wrapping will add to our overall line count
|
||||
estimated_line_count.saturating_sub(1)
|
||||
}
|
||||
|
||||
/// Compute the line width for ANSI escaped text
|
||||
pub(crate) fn line_width(line: &str) -> usize {
|
||||
strip_ansi(line).width()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rstest::rstest;
|
||||
|
||||
#[rstest]
|
||||
#[case("sentence\nsentence", "sentence\r\nsentence")]
|
||||
#[case("sentence\r\nsentence", "sentence\r\nsentence")]
|
||||
#[case("sentence\nsentence\n", "sentence\r\nsentence\r\n")]
|
||||
#[case("😇\nsentence", "😇\r\nsentence")]
|
||||
#[case("sentence\n😇", "sentence\r\n😇")]
|
||||
#[case("\n", "\r\n")]
|
||||
#[case("", "")]
|
||||
fn test_coerce_crlf(#[case] input: &str, #[case] expected: &str) {
|
||||
let result = coerce_crlf(input);
|
||||
|
||||
assert_eq!(result, expected);
|
||||
|
||||
assert!(
|
||||
input != expected || matches!(result, Cow::Borrowed(_)),
|
||||
"Unnecessary allocation"
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user