Finished converting codebase back to plain Rope's.
Everything appears to be working identically to how it was before converting back.
This commit is contained in:
parent
fee6fc21a3
commit
9c462ee332
|
@ -37,6 +37,15 @@ impl Buffer {
|
|||
}
|
||||
|
||||
|
||||
pub fn new_from_str(s: &str) -> Buffer {
|
||||
Buffer {
|
||||
text: Rope::new_from_str(s),
|
||||
file_path: None,
|
||||
undo_stack: UndoStack::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn new_from_file(path: &Path) -> IoResult<Buffer> {
|
||||
let mut f = BufferedReader::new(try!(File::open(path)));
|
||||
let string = f.read_to_string().unwrap();
|
||||
|
|
|
@ -47,7 +47,7 @@ pub trait LineFormatter {
|
|||
let (line_block, col_i_adjusted) = block_index_and_offset(col_i);
|
||||
|
||||
// Get an iter into the right block
|
||||
let g_iter = line.grapheme_iter_between_indices(line_block * LINE_BLOCK_LENGTH, line_block * (LINE_BLOCK_LENGTH+1));
|
||||
let g_iter = line.grapheme_iter_between_indices(line_block * LINE_BLOCK_LENGTH, (line_block+1) * LINE_BLOCK_LENGTH);
|
||||
return self.index_to_v2d(g_iter, col_i_adjusted).1;
|
||||
}
|
||||
|
||||
|
@ -64,7 +64,7 @@ pub trait LineFormatter {
|
|||
// Find the right block in the line, and the index within that block
|
||||
let (line_block, col_i_adjusted) = block_index_and_offset(col_i);
|
||||
|
||||
let (mut y, x) = self.index_to_v2d(buf.get_line(line_i).grapheme_iter_between_indices(line_block*LINE_BLOCK_LENGTH, line_block*(LINE_BLOCK_LENGTH+1)), col_i_adjusted);
|
||||
let (mut y, x) = self.index_to_v2d(buf.get_line(line_i).grapheme_iter_between_indices(line_block * LINE_BLOCK_LENGTH, (line_block+1) * LINE_BLOCK_LENGTH), col_i_adjusted);
|
||||
let mut new_y = y as isize + offset;
|
||||
|
||||
// First, find the right line while keeping track of the vertical offset
|
||||
|
@ -72,7 +72,7 @@ pub trait LineFormatter {
|
|||
let mut block_index: usize = line_block;
|
||||
loop {
|
||||
line = buf.get_line(line_i);
|
||||
let (h, _) = self.dimensions(line.grapheme_iter_between_indices(line_block*LINE_BLOCK_LENGTH, line_block*(LINE_BLOCK_LENGTH+1)));
|
||||
let (h, _) = self.dimensions(line.grapheme_iter_between_indices(line_block * LINE_BLOCK_LENGTH, (line_block+1) * LINE_BLOCK_LENGTH));
|
||||
|
||||
if new_y >= 0 && new_y < h as isize {
|
||||
y = new_y as usize;
|
||||
|
@ -110,7 +110,7 @@ pub trait LineFormatter {
|
|||
else {
|
||||
block_index -= 1;
|
||||
}
|
||||
let (h, _) = self.dimensions(line.grapheme_iter_between_indices(line_block*LINE_BLOCK_LENGTH, line_block*(LINE_BLOCK_LENGTH+1)));
|
||||
let (h, _) = self.dimensions(line.grapheme_iter_between_indices(line_block * LINE_BLOCK_LENGTH, (line_block+1) * LINE_BLOCK_LENGTH));
|
||||
new_y += h as isize;
|
||||
}
|
||||
else {
|
||||
|
@ -121,12 +121,15 @@ pub trait LineFormatter {
|
|||
|
||||
// Next, convert the resulting coordinates back into buffer-wide
|
||||
// coordinates.
|
||||
col_i = (block_index * LINE_BLOCK_LENGTH) + self.v2d_to_index(line.grapheme_iter_between_indices(line_block*LINE_BLOCK_LENGTH, line_block*(LINE_BLOCK_LENGTH+1)), (y, x), rounding);
|
||||
col_i = (block_index * LINE_BLOCK_LENGTH) + self.v2d_to_index(line.grapheme_iter_between_indices(line_block * LINE_BLOCK_LENGTH, (line_block+1) * LINE_BLOCK_LENGTH), (y, x), rounding);
|
||||
|
||||
return buf.line_col_to_index((line_i, col_i));
|
||||
}
|
||||
|
||||
|
||||
/// Takes a grapheme index and a desired visual horizontal position, and
|
||||
/// returns a grapheme index on the same visual line as the given index,
|
||||
/// but offset to have the desired horizontal position.
|
||||
fn index_set_horizontal_v2d(&self, buf: &Buffer, index: usize, horizontal: usize, rounding: RoundingBehavior) -> usize {
|
||||
let (line_i, col_i) = buf.index_to_line_col(index);
|
||||
let line = buf.get_line(line_i);
|
||||
|
|
|
@ -80,9 +80,10 @@ impl LineFormatter for ConsoleLineFormatter {
|
|||
{
|
||||
// TODO: handle rounding modes
|
||||
let mut i = 0;
|
||||
|
||||
|
||||
for (_, pos, _) in self.iter(g_iter) {
|
||||
if pos.0 > v2d.0 {
|
||||
i -= 1;
|
||||
break;
|
||||
}
|
||||
else if pos.0 == v2d.0 && pos.1 >= v2d.1 {
|
||||
|
@ -162,3 +163,279 @@ fn grapheme_vis_width_at_vis_pos(g: &str, pos: usize, tab_width: usize) -> usize
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(unused_imports)]
|
||||
use super::*;
|
||||
use formatter::LineFormatter;
|
||||
use formatter::RoundingBehavior::{Round, Floor, Ceiling};
|
||||
use buffer::Buffer;
|
||||
|
||||
|
||||
#[test]
|
||||
fn dimensions_1() {
|
||||
let text = "Hello there, stranger!"; // 22 graphemes long
|
||||
let mut f = ConsoleLineFormatter::new(4);
|
||||
f.wrap_width = 80;
|
||||
|
||||
assert_eq!(f.dimensions(text.graphemes(true)), (1, 22));
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn dimensions_2() {
|
||||
let text = "Hello there, stranger! How are you doing this fine day?"; // 56 graphemes long
|
||||
let mut f = ConsoleLineFormatter::new(4);
|
||||
f.wrap_width = 12;
|
||||
|
||||
assert_eq!(f.dimensions(text.graphemes(true)), (5, 12));
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn index_to_v2d_1() {
|
||||
let text = "Hello there, stranger!"; // 22 graphemes long
|
||||
let mut f = ConsoleLineFormatter::new(4);
|
||||
f.wrap_width = 80;
|
||||
|
||||
assert_eq!(f.index_to_v2d(text.graphemes(true), 0), (0, 0));
|
||||
assert_eq!(f.index_to_v2d(text.graphemes(true), 5), (0, 5));
|
||||
assert_eq!(f.index_to_v2d(text.graphemes(true), 22), (0, 22));
|
||||
assert_eq!(f.index_to_v2d(text.graphemes(true), 23), (0, 22));
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn index_to_v2d_2() {
|
||||
let text = "Hello there, stranger! How are you doing this fine day?"; // 56 graphemes long
|
||||
let mut f = ConsoleLineFormatter::new(4);
|
||||
f.wrap_width = 12;
|
||||
|
||||
assert_eq!(f.index_to_v2d(text.graphemes(true), 0), (0, 0));
|
||||
assert_eq!(f.index_to_v2d(text.graphemes(true), 5), (0, 5));
|
||||
assert_eq!(f.index_to_v2d(text.graphemes(true), 11), (0, 11));
|
||||
|
||||
assert_eq!(f.index_to_v2d(text.graphemes(true), 12), (1, 0));
|
||||
assert_eq!(f.index_to_v2d(text.graphemes(true), 15), (1, 3));
|
||||
assert_eq!(f.index_to_v2d(text.graphemes(true), 23), (1, 11));
|
||||
|
||||
assert_eq!(f.index_to_v2d(text.graphemes(true), 24), (2, 0));
|
||||
assert_eq!(f.index_to_v2d(text.graphemes(true), 28), (2, 4));
|
||||
assert_eq!(f.index_to_v2d(text.graphemes(true), 35), (2, 11));
|
||||
|
||||
assert_eq!(f.index_to_v2d(text.graphemes(true), 36), (3, 0));
|
||||
assert_eq!(f.index_to_v2d(text.graphemes(true), 43), (3, 7));
|
||||
assert_eq!(f.index_to_v2d(text.graphemes(true), 47), (3, 11));
|
||||
|
||||
assert_eq!(f.index_to_v2d(text.graphemes(true), 48), (4, 0));
|
||||
assert_eq!(f.index_to_v2d(text.graphemes(true), 50), (4, 2));
|
||||
assert_eq!(f.index_to_v2d(text.graphemes(true), 56), (4, 8));
|
||||
|
||||
assert_eq!(f.index_to_v2d(text.graphemes(true), 57), (4, 8));
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn v2d_to_index_1() {
|
||||
let text = "Hello there, stranger!"; // 22 graphemes long
|
||||
let mut f = ConsoleLineFormatter::new(4);
|
||||
f.wrap_width = 80;
|
||||
|
||||
assert_eq!(f.v2d_to_index(text.graphemes(true), (0,0), (Floor, Floor)), 0);
|
||||
assert_eq!(f.v2d_to_index(text.graphemes(true), (0,5), (Floor, Floor)), 5);
|
||||
assert_eq!(f.v2d_to_index(text.graphemes(true), (0,22), (Floor, Floor)), 22);
|
||||
assert_eq!(f.v2d_to_index(text.graphemes(true), (0,23), (Floor, Floor)), 22);
|
||||
assert_eq!(f.v2d_to_index(text.graphemes(true), (1,0), (Floor, Floor)), 22);
|
||||
assert_eq!(f.v2d_to_index(text.graphemes(true), (1,1), (Floor, Floor)), 22);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn v2d_to_index_2() {
|
||||
let text = "Hello there, stranger! How are you doing this fine day?"; // 56 graphemes long
|
||||
let mut f = ConsoleLineFormatter::new(4);
|
||||
f.wrap_width = 12;
|
||||
|
||||
assert_eq!(f.v2d_to_index(text.graphemes(true), (0,0), (Floor, Floor)), 0);
|
||||
assert_eq!(f.v2d_to_index(text.graphemes(true), (0,11), (Floor, Floor)), 11);
|
||||
assert_eq!(f.v2d_to_index(text.graphemes(true), (0,12), (Floor, Floor)), 11);
|
||||
|
||||
assert_eq!(f.v2d_to_index(text.graphemes(true), (1,0), (Floor, Floor)), 12);
|
||||
assert_eq!(f.v2d_to_index(text.graphemes(true), (1,11), (Floor, Floor)), 23);
|
||||
assert_eq!(f.v2d_to_index(text.graphemes(true), (1,12), (Floor, Floor)), 23);
|
||||
|
||||
assert_eq!(f.v2d_to_index(text.graphemes(true), (2,0), (Floor, Floor)), 24);
|
||||
assert_eq!(f.v2d_to_index(text.graphemes(true), (2,11), (Floor, Floor)), 35);
|
||||
assert_eq!(f.v2d_to_index(text.graphemes(true), (2,12), (Floor, Floor)), 35);
|
||||
|
||||
assert_eq!(f.v2d_to_index(text.graphemes(true), (3,0), (Floor, Floor)), 36);
|
||||
assert_eq!(f.v2d_to_index(text.graphemes(true), (3,11), (Floor, Floor)), 47);
|
||||
assert_eq!(f.v2d_to_index(text.graphemes(true), (3,12), (Floor, Floor)), 47);
|
||||
|
||||
assert_eq!(f.v2d_to_index(text.graphemes(true), (4,0), (Floor, Floor)), 48);
|
||||
assert_eq!(f.v2d_to_index(text.graphemes(true), (4,7), (Floor, Floor)), 55);
|
||||
assert_eq!(f.v2d_to_index(text.graphemes(true), (4,8), (Floor, Floor)), 56);
|
||||
assert_eq!(f.v2d_to_index(text.graphemes(true), (4,9), (Floor, Floor)), 56);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn index_to_horizontal_v2d_1() {
|
||||
let b = Buffer::new_from_str("Hello there, stranger!\nHow are you doing this fine day?"); // 55 graphemes long
|
||||
let mut f = ConsoleLineFormatter::new(4);
|
||||
f.wrap_width = 80;
|
||||
|
||||
assert_eq!(f.index_to_horizontal_v2d(&b, 0), 0);
|
||||
assert_eq!(f.index_to_horizontal_v2d(&b, 5), 5);
|
||||
assert_eq!(f.index_to_horizontal_v2d(&b, 26), 3);
|
||||
assert_eq!(f.index_to_horizontal_v2d(&b, 55), 32);
|
||||
assert_eq!(f.index_to_horizontal_v2d(&b, 56), 32);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn index_to_horizontal_v2d_2() {
|
||||
let b = Buffer::new_from_str("Hello there, stranger!\nHow are you doing this fine day?"); // 55 graphemes long
|
||||
let mut f = ConsoleLineFormatter::new(4);
|
||||
f.wrap_width = 12;
|
||||
|
||||
assert_eq!(f.index_to_horizontal_v2d(&b, 0), 0);
|
||||
assert_eq!(f.index_to_horizontal_v2d(&b, 11), 11);
|
||||
|
||||
assert_eq!(f.index_to_horizontal_v2d(&b, 12), 0);
|
||||
assert_eq!(f.index_to_horizontal_v2d(&b, 22), 10);
|
||||
|
||||
assert_eq!(f.index_to_horizontal_v2d(&b, 23), 0);
|
||||
assert_eq!(f.index_to_horizontal_v2d(&b, 34), 11);
|
||||
|
||||
assert_eq!(f.index_to_horizontal_v2d(&b, 35), 0);
|
||||
assert_eq!(f.index_to_horizontal_v2d(&b, 46), 11);
|
||||
|
||||
assert_eq!(f.index_to_horizontal_v2d(&b, 47), 0);
|
||||
assert_eq!(f.index_to_horizontal_v2d(&b, 55), 8);
|
||||
assert_eq!(f.index_to_horizontal_v2d(&b, 56), 8);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn index_set_horizontal_v2d_1() {
|
||||
let b = Buffer::new_from_str("Hello there, stranger!\nHow are you doing this fine day?"); // 55 graphemes long
|
||||
let mut f = ConsoleLineFormatter::new(4);
|
||||
f.wrap_width = 80;
|
||||
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 0, 0, Floor), 0);
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 0, 22, Floor), 22);
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 0, 23, Floor), 22);
|
||||
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 8, 0, Floor), 0);
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 8, 22, Floor), 22);
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 8, 23, Floor), 22);
|
||||
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 22, 0, Floor), 0);
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 22, 22, Floor), 22);
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 22, 23, Floor), 22);
|
||||
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 23, 0, Floor), 23);
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 23, 32, Floor), 55);
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 23, 33, Floor), 55);
|
||||
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 28, 0, Floor), 23);
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 28, 32, Floor), 55);
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 28, 33, Floor), 55);
|
||||
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 55, 0, Floor), 23);
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 55, 32, Floor), 55);
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 55, 33, Floor), 55);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn index_set_horizontal_v2d_2() {
|
||||
let b = Buffer::new_from_str("Hello there, stranger! How are you doing this fine day?"); // 55 graphemes long
|
||||
let mut f = ConsoleLineFormatter::new(4);
|
||||
f.wrap_width = 12;
|
||||
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 0, 0, Floor), 0);
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 0, 11, Floor), 11);
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 0, 12, Floor), 11);
|
||||
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 8, 0, Floor), 0);
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 8, 11, Floor), 11);
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 8, 12, Floor), 11);
|
||||
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 11, 0, Floor), 0);
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 11, 11, Floor), 11);
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 11, 12, Floor), 11);
|
||||
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 12, 0, Floor), 12);
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 12, 11, Floor), 23);
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 12, 12, Floor), 23);
|
||||
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 17, 0, Floor), 12);
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 17, 11, Floor), 23);
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 17, 12, Floor), 23);
|
||||
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 23, 0, Floor), 12);
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 23, 11, Floor), 23);
|
||||
assert_eq!(f.index_set_horizontal_v2d(&b, 23, 12, Floor), 23);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn index_offset_vertical_v2d_1() {
|
||||
let b = Buffer::new_from_str("Hello there, stranger!\nHow are you doing this fine day?"); // 55 graphemes long
|
||||
let mut f = ConsoleLineFormatter::new(4);
|
||||
f.wrap_width = 80;
|
||||
|
||||
assert_eq!(f.index_offset_vertical_v2d(&b, 0, 0, (Floor, Floor)), 0);
|
||||
assert_eq!(f.index_offset_vertical_v2d(&b, 0, 1, (Floor, Floor)), 23);
|
||||
assert_eq!(f.index_offset_vertical_v2d(&b, 23, -1, (Floor, Floor)), 0);
|
||||
|
||||
assert_eq!(f.index_offset_vertical_v2d(&b, 2, 0, (Floor, Floor)), 2);
|
||||
assert_eq!(f.index_offset_vertical_v2d(&b, 2, 1, (Floor, Floor)), 25);
|
||||
assert_eq!(f.index_offset_vertical_v2d(&b, 25, -1, (Floor, Floor)), 2);
|
||||
|
||||
assert_eq!(f.index_offset_vertical_v2d(&b, 22, 0, (Floor, Floor)), 22);
|
||||
assert_eq!(f.index_offset_vertical_v2d(&b, 22, 1, (Floor, Floor)), 45);
|
||||
assert_eq!(f.index_offset_vertical_v2d(&b, 45, -1, (Floor, Floor)), 22);
|
||||
|
||||
assert_eq!(f.index_offset_vertical_v2d(&b, 54, 0, (Floor, Floor)), 54);
|
||||
assert_eq!(f.index_offset_vertical_v2d(&b, 54, 1, (Floor, Floor)), 55);
|
||||
assert_eq!(f.index_offset_vertical_v2d(&b, 54, -1, (Floor, Floor)), 22);
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn index_offset_vertical_v2d_2() {
|
||||
let b = Buffer::new_from_str("Hello there, stranger! How are you doing this fine day?"); // 55 graphemes long
|
||||
let mut f = ConsoleLineFormatter::new(4);
|
||||
f.wrap_width = 12;
|
||||
|
||||
assert_eq!(f.index_offset_vertical_v2d(&b, 0, 0, (Floor, Floor)), 0);
|
||||
assert_eq!(f.index_offset_vertical_v2d(&b, 0, 1, (Floor, Floor)), 12);
|
||||
assert_eq!(f.index_offset_vertical_v2d(&b, 0, 2, (Floor, Floor)), 24);
|
||||
|
||||
assert_eq!(f.index_offset_vertical_v2d(&b, 0, 0, (Floor, Floor)), 0);
|
||||
assert_eq!(f.index_offset_vertical_v2d(&b, 12, -1, (Floor, Floor)), 0);
|
||||
assert_eq!(f.index_offset_vertical_v2d(&b, 24, -2, (Floor, Floor)), 0);
|
||||
|
||||
assert_eq!(f.index_offset_vertical_v2d(&b, 4, 0, (Floor, Floor)), 4);
|
||||
assert_eq!(f.index_offset_vertical_v2d(&b, 4, 1, (Floor, Floor)), 16);
|
||||
assert_eq!(f.index_offset_vertical_v2d(&b, 4, 2, (Floor, Floor)), 28);
|
||||
|
||||
assert_eq!(f.index_offset_vertical_v2d(&b, 4, 0, (Floor, Floor)), 4);
|
||||
assert_eq!(f.index_offset_vertical_v2d(&b, 16, -1, (Floor, Floor)), 4);
|
||||
assert_eq!(f.index_offset_vertical_v2d(&b, 28, -2, (Floor, Floor)), 4);
|
||||
|
||||
assert_eq!(f.index_offset_vertical_v2d(&b, 11, 0, (Floor, Floor)), 11);
|
||||
assert_eq!(f.index_offset_vertical_v2d(&b, 11, 1, (Floor, Floor)), 23);
|
||||
assert_eq!(f.index_offset_vertical_v2d(&b, 11, 2, (Floor, Floor)), 35);
|
||||
|
||||
assert_eq!(f.index_offset_vertical_v2d(&b, 11, 0, (Floor, Floor)), 11);
|
||||
assert_eq!(f.index_offset_vertical_v2d(&b, 23, -1, (Floor, Floor)), 11);
|
||||
assert_eq!(f.index_offset_vertical_v2d(&b, 35, -2, (Floor, Floor)), 11);
|
||||
}
|
||||
}
|
5
todo.md
5
todo.md
|
@ -15,11 +15,6 @@
|
|||
- Loading/saving code for different encodings.
|
||||
- Auto-detecting text encodings from file data (this one will be tricky).
|
||||
|
||||
- Switch back to using a Rope directly, instead of using a tree of lines.
|
||||
The code for everything is sufficiently clear now that doing this shouldn't
|
||||
be difficult, as the needed API's are pretty obvious. And then we have
|
||||
fewer moving parts. Yay!
|
||||
|
||||
- Word wrap.
|
||||
- Get non-wrapping text working again.
|
||||
- File opening by entering path
|
||||
|
|
Loading…
Reference in New Issue
Block a user