Skip to content
GitLab
Projects
Groups
Snippets
Help
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
L
liner
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Service Desk
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Operations
Operations
Incidents
Environments
Packages & Registries
Packages & Registries
Container Registry
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
AdminXVII
liner
Commits
af7084b4
Commit
af7084b4
authored
Jun 21, 2019
by
AdminXVII
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Update to Rust 2018
parent
75790748
Pipeline
#4809
failed with stage
in 1 minute and 53 seconds
Changes
10
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
103 additions
and
99 deletions
+103
-99
Cargo.toml
Cargo.toml
+1
-0
src/buffer.rs
src/buffer.rs
+8
-6
src/context.rs
src/context.rs
+15
-11
src/editor.rs
src/editor.rs
+22
-24
src/event.rs
src/event.rs
+2
-5
src/history.rs
src/history.rs
+19
-10
src/keymap/emacs.rs
src/keymap/emacs.rs
+4
-4
src/keymap/mod.rs
src/keymap/mod.rs
+3
-3
src/keymap/vi.rs
src/keymap/vi.rs
+26
-28
src/util.rs
src/util.rs
+3
-8
No files found.
Cargo.toml
View file @
af7084b4
...
...
@@ -11,6 +11,7 @@ exclude = [
"CONTRIBUTING.md"
,
".travis.yml"
,
]
edition
=
"2018"
[lib]
name
=
"liner"
...
...
src/buffer.rs
View file @
af7084b4
...
...
@@ -88,6 +88,12 @@ impl FromIterator<char> for Buffer {
}
}
impl
Default
for
Buffer
{
fn
default
()
->
Self
{
Self
::
new
()
}
}
impl
Buffer
{
pub
fn
new
()
->
Self
{
Buffer
{
...
...
@@ -210,17 +216,13 @@ impl Buffer {
pub
fn
remove
(
&
mut
self
,
start
:
usize
,
end
:
usize
)
->
usize
{
let
s
=
self
.remove_raw
(
start
,
end
);
let
num_removed
=
s
.len
();
let
act
=
Action
::
Remove
{
start
:
start
,
text
:
s
,
};
self
.push_action
(
act
);
self
.push_action
(
Action
::
Remove
{
start
,
text
:
s
});
num_removed
}
pub
fn
insert
(
&
mut
self
,
start
:
usize
,
text
:
&
[
char
])
{
let
act
=
Action
::
Insert
{
start
:
start
,
start
,
text
:
text
.into
(),
};
act
.do_on
(
self
);
...
...
src/context.rs
View file @
af7084b4
...
...
@@ -5,7 +5,7 @@ use termion::raw::IntoRawMode;
use
super
::
*
;
use
keymap
;
pub
type
ColorClosure
=
Box
<
Fn
(
&
str
)
->
String
>
;
pub
type
ColorClosure
=
Box
<
dyn
Fn
(
&
str
)
->
String
>
;
/// The default for `Context.word_divider_fn`.
pub
fn
get_buffer_words
(
buf
:
&
Buffer
)
->
Vec
<
(
usize
,
usize
)
>
{
...
...
@@ -48,11 +48,17 @@ pub enum KeyBindings {
pub
struct
Context
{
pub
history
:
History
,
pub
word_divider_fn
:
Box
<
Fn
(
&
Buffer
)
->
Vec
<
(
usize
,
usize
)
>>
,
pub
word_divider_fn
:
Box
<
dyn
Fn
(
&
Buffer
)
->
Vec
<
(
usize
,
usize
)
>>
,
pub
key_bindings
:
KeyBindings
,
pub
buf
:
String
,
}
impl
Default
for
Context
{
fn
default
()
->
Self
{
Self
::
new
()
}
}
impl
Context
{
pub
fn
new
()
->
Self
{
Context
{
...
...
@@ -103,17 +109,15 @@ impl Context {
f
:
Option
<
ColorClosure
>
,
buffer
:
B
,
)
->
io
::
Result
<
String
>
{
let
res
=
{
let
mut
stdout
=
stdout
()
.into_raw_mode
()
?
;
let
mut
ed
=
Editor
::
new_with_init_buffer
(
stdout
,
prompt
,
f
,
self
,
buffer
)
?
;
match
self
.key_bindings
{
KeyBindings
::
Emacs
=>
Self
::
handle_keys
(
keymap
::
Emacs
::
new
(),
ed
,
handler
),
KeyBindings
::
Vi
=>
Self
::
handle_keys
(
keymap
::
Vi
::
new
(),
ed
,
handler
),
}
};
let
stdout
=
stdout
()
.into_raw_mode
()
?
;
let
keybindings
=
self
.key_bindings
;
let
ed
=
Editor
::
new_with_init_buffer
(
stdout
,
prompt
,
f
,
self
,
buffer
)
?
;
match
keybindings
{
KeyBindings
::
Emacs
=>
Self
::
handle_keys
(
keymap
::
Emacs
::
new
(),
ed
,
handler
),
KeyBindings
::
Vi
=>
Self
::
handle_keys
(
keymap
::
Vi
::
new
(),
ed
,
handler
),
}
//self.revert_all_history();
res
}
fn
handle_keys
<
'a
,
W
:
Write
,
M
:
KeyMap
,
C
:
Completer
>
(
...
...
src/editor.rs
View file @
af7084b4
...
...
@@ -4,12 +4,12 @@ use std::io;
use
termion
::{
self
,
clear
,
color
,
cursor
};
use
super
::
complete
::
Completer
;
use
context
::
ColorClosure
;
use
event
::
*
;
use
crate
::
context
::
ColorClosure
;
use
crate
::
event
::
*
;
use
crate
::
util
;
use
crate
::
Buffer
;
use
crate
::
Context
;
use
itertools
::
Itertools
;
use
util
;
use
Buffer
;
use
Context
;
/// Represents the position of the cursor relative to words in the buffer.
#[derive(Debug,
Clone,
Copy,
PartialEq,
Eq)]
...
...
@@ -158,13 +158,13 @@ impl<'a, W: io::Write> Editor<'a, W> {
let
mut
ed
=
Editor
{
prompt
:
prompt
.into
(),
cursor
:
0
,
out
:
out
,
out
,
closure
:
f
,
new_buf
:
buffer
.into
(),
hist_buf
:
Buffer
::
new
(),
hist_buf_valid
:
false
,
cur_history_loc
:
None
,
context
:
context
,
context
,
show_completions_hint
:
None
,
show_autosuggestions
:
true
,
term_cursor_line
:
1
,
...
...
@@ -264,7 +264,7 @@ impl<'a, W: io::Write> Editor<'a, W> {
fn
refresh_search
(
&
mut
self
,
forward
:
bool
)
{
let
search_history_loc
=
self
.search_history_loc
();
self
.history_subset_index
=
self
.context.history
.search_index
(
&
self
.new_buf
);
if
self
.history_subset_index
.len
()
>
0
{
if
!
self
.history_subset_index
.is_empty
()
{
self
.history_subset_loc
=
if
forward
{
Some
(
0
)
}
else
{
...
...
@@ -302,7 +302,7 @@ impl<'a, W: io::Write> Editor<'a, W> {
if
!
self
.is_search
()
{
self
.freshen_history
();
self
.refresh_search
(
forward
);
}
else
if
self
.history_subset_index
.len
()
>
0
{
}
else
if
!
self
.history_subset_index
.is_empty
()
{
self
.history_subset_loc
=
if
let
Some
(
p
)
=
self
.history_subset_loc
{
if
forward
{
if
p
<
self
.history_subset_index
.len
()
-
1
{
...
...
@@ -310,12 +310,10 @@ impl<'a, W: io::Write> Editor<'a, W> {
}
else
{
Some
(
0
)
}
}
else
if
p
>
0
{
Some
(
p
-
1
)
}
else
{
if
p
>
0
{
Some
(
p
-
1
)
}
else
{
Some
(
self
.history_subset_index
.len
()
-
1
)
}
Some
(
self
.history_subset_index
.len
()
-
1
)
}
}
else
{
None
...
...
@@ -381,7 +379,7 @@ impl<'a, W: io::Write> Editor<'a, W> {
let
col_width
=
2
+
w
as
usize
/
cols
;
let
cols
=
max
(
1
,
w
as
usize
/
col_width
);
let
mut
lines
=
completions
.len
()
/
cols
;
let
lines
=
completions
.len
()
/
cols
;
let
mut
i
=
0
;
for
(
index
,
com
)
in
completions
.iter
()
.enumerate
()
{
...
...
@@ -562,7 +560,7 @@ impl<'a, W: io::Write> Editor<'a, W> {
None
=>
{
self
.history_subset_index
=
self
.context.history
.get_history_subset
(
&
self
.new_buf
);
if
self
.history_subset_index
.len
()
>
0
{
if
!
self
.history_subset_index
.is_empty
()
{
self
.history_subset_loc
=
Some
(
self
.history_subset_index
.len
()
-
1
);
self
.cur_history_loc
=
Some
(
self
.history_subset_index
[
self
.history_subset_index
.len
()
-
1
],
...
...
@@ -574,7 +572,7 @@ impl<'a, W: io::Write> Editor<'a, W> {
}
else
{
match
self
.cur_history_loc
{
Some
(
i
)
if
i
>
0
=>
self
.cur_history_loc
=
Some
(
i
-
1
),
None
if
self
.context.history
.len
()
>
0
=>
{
None
if
!
self
.context.history
.is_empty
()
=>
{
self
.cur_history_loc
=
Some
(
self
.context.history
.len
()
-
1
)
}
_
=>
(),
...
...
@@ -617,13 +615,13 @@ impl<'a, W: io::Write> Editor<'a, W> {
/// Moves to the start of history (ie. the earliest history entry).
pub
fn
move_to_start_of_history
(
&
mut
self
)
->
io
::
Result
<
()
>
{
self
.hist_buf_valid
=
false
;
if
self
.context.history
.len
()
>
0
{
self
.cur_history_loc
=
Some
(
0
);
self
.move_cursor_to_end_of_line
()
}
else
{
if
self
.context.history
.is_empty
()
{
self
.cur_history_loc
=
None
;
self
.no_newline
=
true
;
self
.display
()
}
else
{
self
.cur_history_loc
=
Some
(
0
);
self
.move_cursor_to_end_of_line
()
}
}
...
...
@@ -863,13 +861,13 @@ impl<'a, W: io::Write> Editor<'a, W> {
fn
search_prompt
(
&
mut
self
)
->
(
String
,
usize
)
{
if
self
.is_search
()
{
// If we are searching override prompt to search prompt.
let
(
hplace
,
color
)
=
if
self
.history_subset_index
.len
()
>
0
{
let
(
hplace
,
color
)
=
if
self
.history_subset_index
.is_empty
()
{
(
0
,
color
::
Red
.fg_str
())
}
else
{
(
self
.history_subset_loc
.unwrap_or
(
0
)
+
1
,
color
::
Green
.fg_str
(),
)
}
else
{
(
0
,
color
::
Red
.fg_str
())
};
(
format!
(
...
...
src/event.rs
View file @
af7084b4
use
crate
::
Editor
;
use
std
::
io
::
Write
;
use
termion
::
event
::
Key
;
use
Editor
;
pub
struct
Event
<
'a
,
'out
:
'a
,
W
:
Write
+
'a
>
{
pub
editor
:
&
'a
mut
Editor
<
'out
,
W
>
,
...
...
@@ -9,10 +9,7 @@ pub struct Event<'a, 'out: 'a, W: Write + 'a> {
impl
<
'a
,
'out
:
'a
,
W
:
Write
+
'a
>
Event
<
'a
,
'out
,
W
>
{
pub
fn
new
(
editor
:
&
'a
mut
Editor
<
'out
,
W
>
,
kind
:
EventKind
)
->
Self
{
Event
{
editor
:
editor
,
kind
:
kind
,
}
Event
{
editor
,
kind
}
}
}
...
...
src/history.rs
View file @
af7084b4
...
...
@@ -41,6 +41,12 @@ pub struct History {
compaction_writes
:
usize
,
}
impl
Default
for
History
{
fn
default
()
->
Self
{
Self
::
new
()
}
}
impl
History
{
/// Create new History structure.
pub
fn
new
()
->
History
{
...
...
@@ -121,7 +127,7 @@ impl History {
Err
(
_
)
=>
break
,
}
}
self
.t
o_max_siz
e
();
self
.t
runcat
e
();
if
!
self
.load_duplicates
{
let
mut
tmp_buffers
:
Vec
<
Buffer
>
=
Vec
::
with_capacity
(
self
.buffers
.len
());
// Remove duplicates from loaded history if we do not want it.
...
...
@@ -185,7 +191,7 @@ impl History {
let
mut
file
=
BufWriter
::
new
(
File
::
create
(
&
path
)
?
);
// Write the commands to the history file.
for
command
in
buf
.into_iter
()
{
let
_
=
file
.write_all
(
&
String
::
from
(
command
)
.as_bytes
());
let
_
=
file
.write_all
(
&
command
.as_bytes
());
let
_
=
file
.write_all
(
b
"
\n
"
);
}
}
...
...
@@ -224,6 +230,11 @@ impl History {
self
.buffers
.len
()
}
/// Is the history empty
pub
fn
is_empty
(
&
self
)
->
bool
{
self
.buffers
.is_empty
()
}
/// Add a command to the history buffer and remove the oldest commands when the max history
/// size has been met. If writing to the disk is enabled, this function will be used for
/// logging history to the designated history file.
...
...
@@ -301,8 +312,7 @@ impl History {
I
:
Iterator
<
Item
=
usize
>
,
{
vals
.filter_map
(|
i
|
self
.buffers
.get
(
i
)
.map
(|
t
|
(
i
,
t
)))
.filter
(|(
_
i
,
tested
)|
tested
.starts_with
(
search_term
))
.next
()
.find
(|(
_
i
,
tested
)|
tested
.starts_with
(
search_term
))
.map
(|(
i
,
_
)|
i
)
}
...
...
@@ -331,11 +341,10 @@ impl History {
if
starts
{
v
.push
(
*
i
);
}
if
contains
&&
!
starts
&&
tested
!=
search_term
{
return
true
;
}
contains
&&
!
starts
&&
tested
!=
search_term
}
else
{
false
}
return
false
;
})
.collect
();
ret
.append
(
&
mut
v
);
...
...
@@ -356,7 +365,7 @@ impl History {
self
.file_name
.as_ref
()
.map
(|
s
|
s
.as_str
())
}
fn
t
o_max_siz
e
(
&
mut
self
)
{
fn
t
runcat
e
(
&
mut
self
)
{
// Find how many lines we need to move backwards
// in the file to remove all the old commands.
if
self
.buffers
.len
()
>=
self
.max_file_size
{
...
...
@@ -368,7 +377,7 @@ impl History {
}
fn
overwrite_history
<
P
:
AsRef
<
Path
>>
(
&
mut
self
,
path
:
P
)
->
io
::
Result
<
String
>
{
self
.t
o_max_siz
e
();
self
.t
runcat
e
();
let
mut
file
=
BufWriter
::
new
(
File
::
create
(
&
path
)
?
);
// Write the commands to the history file.
...
...
src/keymap/emacs.rs
View file @
af7084b4
use
std
::
io
::{
self
,
Write
};
use
termion
::
event
::
Key
;
use
CursorPosition
;
use
Editor
;
use
KeyMap
;
use
crate
::
CursorPosition
;
use
crate
::
Editor
;
use
crate
::
KeyMap
;
/// Emacs keybindings for `Editor`. This is the default for `Context::read_line()`.
///
...
...
@@ -61,7 +61,7 @@ impl Emacs {
fn
handle_last_arg_fetch
<
'a
,
W
:
Write
>
(
&
mut
self
,
ed
:
&
mut
Editor
<
'a
,
W
>
)
->
io
::
Result
<
()
>
{
// Empty history means no last arg to fetch.
if
ed
.context
()
.history
.
len
()
==
0
{
if
ed
.context
()
.history
.
is_empty
()
{
return
Ok
(());
}
...
...
src/keymap/mod.rs
View file @
af7084b4
use
super
::
complete
::
Completer
;
use
event
::
*
;
use
crate
::
complete
::
Completer
;
use
crate
::
event
::
*
;
use
crate
::
Editor
;
use
std
::
io
::{
self
,
ErrorKind
,
Write
};
use
termion
::
event
::
Key
;
use
Editor
;
pub
trait
KeyMap
:
Default
{
fn
handle_key_core
<
'a
,
W
:
Write
>
(
...
...
src/keymap/vi.rs
View file @
af7084b4
...
...
@@ -2,8 +2,9 @@ use std::io::{self, Write};
use
std
::{
cmp
,
mem
};
use
termion
::
event
::
Key
;
use
Editor
;
use
KeyMap
;
use
crate
::
buffer
::
Buffer
;
use
crate
::
Editor
;
use
crate
::
KeyMap
;
#[derive(Debug,
Clone,
Copy,
PartialEq,
Eq)]
enum
CharMovement
{
...
...
@@ -281,7 +282,7 @@ fn vi_move_word_end<W: Write>(
ed
.move_cursor_to
(
cursor
)
}
fn
find_char
(
buf
:
&
::
buffer
::
Buffer
,
start
:
usize
,
ch
:
char
,
count
:
usize
)
->
Option
<
usize
>
{
fn
find_char
(
buf
:
&
Buffer
,
start
:
usize
,
ch
:
char
,
count
:
usize
)
->
Option
<
usize
>
{
assert
!
(
count
>
0
);
buf
.chars
()
.enumerate
()
...
...
@@ -291,7 +292,7 @@ fn find_char(buf: &::buffer::Buffer, start: usize, ch: char, count: usize) -> Op
.map
(|(
i
,
_
)|
i
)
}
fn
find_char_rev
(
buf
:
&
::
buffer
::
Buffer
,
start
:
usize
,
ch
:
char
,
count
:
usize
)
->
Option
<
usize
>
{
fn
find_char_rev
(
buf
:
&
Buffer
,
start
:
usize
,
ch
:
char
,
count
:
usize
)
->
Option
<
usize
>
{
assert
!
(
count
>
0
);
let
rstart
=
buf
.num_chars
()
-
start
;
buf
.chars
()
...
...
@@ -393,25 +394,22 @@ impl Vi {
ed
.no_eol
=
self
.mode
()
==
Mode
::
Normal
;
self
.movement_reset
=
self
.mode
()
!=
Mode
::
Insert
;
match
last_mode
{
Delete
(
start_pos
)
=>
{
// perform the delete operation
match
move_type
{
Exclusive
=>
ed
.delete_until
(
start_pos
)
?
,
Inclusive
=>
ed
.delete_until_inclusive
(
start_pos
)
?
,
}
if
let
Delete
(
start_pos
)
=
last_mode
{
// perform the delete operation
match
move_type
{
Exclusive
=>
ed
.delete_until
(
start_pos
)
?
,
Inclusive
=>
ed
.delete_until_inclusive
(
start_pos
)
?
,
}
// update the last state
mem
::
swap
(
&
mut
self
.last_command
,
&
mut
self
.current_command
);
self
.last_insert
=
self
.current_insert
;
self
.last_count
=
self
.count
;
// update the last state
mem
::
swap
(
&
mut
self
.last_command
,
&
mut
self
.current_command
);
self
.last_insert
=
self
.current_insert
;
self
.last_count
=
self
.count
;
// reset our counts
self
.count
=
0
;
self
.secondary_count
=
0
;
}
_
=>
{}
};
// reset our counts
self
.count
=
0
;
self
.secondary_count
=
0
;
}
// in normal mode, count goes back to 0 after movement
if
original_mode
==
Normal
{
...
...
@@ -759,7 +757,7 @@ impl Vi {
ed
.move_cursor_to_start_of_line
()
?
;
self
.pop_mode_after_movement
(
Exclusive
,
ed
)
}
Key
::
Char
(
i
@
'0'
..
.
'9'
)
=>
{
Key
::
Char
(
i
@
'0'
..
=
'9'
)
=>
{
let
i
=
i
.to_digit
(
10
)
.unwrap
();
// count = count * 10 + i
self
.count
=
self
.count
.saturating_mul
(
10
)
.saturating_add
(
i
);
...
...
@@ -898,7 +896,7 @@ impl Vi {
self
.handle_key_normal
(
key
,
ed
)
}
// handle numeric keys
(
Key
::
Char
(
'0'
..
.
'9'
),
_
)
=>
self
.handle_key_normal
(
key
,
ed
),
(
Key
::
Char
(
'0'
..
=
'9'
),
_
)
=>
self
.handle_key_normal
(
key
,
ed
),
(
Key
::
Char
(
'c'
),
Some
(
Key
::
Char
(
'c'
)))
|
(
Key
::
Char
(
'd'
),
None
)
=>
{
// updating the last command buffer doesn't really make sense in this context.
// Repeating 'dd' will simply erase and already erased line. Any other commands
...
...
@@ -2701,7 +2699,7 @@ mod tests {
let
out
=
Vec
::
new
();
let
mut
ed
=
Editor
::
new
(
out
,
"prompt"
.to_owned
(),
None
,
&
mut
context
)
.unwrap
();
ed
.insert_str_after_cursor
(
"..
.
"
)
.unwrap
();
ed
.insert_str_after_cursor
(
"..
=
"
)
.unwrap
();
let
pos1
=
ed
.cursor
();
ed
.insert_str_after_cursor
(
"word"
)
.unwrap
();
let
pos2
=
ed
.cursor
();
...
...
@@ -2723,9 +2721,9 @@ mod tests {
let
out
=
Vec
::
new
();
let
mut
ed
=
Editor
::
new
(
out
,
"prompt"
.to_owned
(),
None
,
&
mut
context
)
.unwrap
();
ed
.insert_str_after_cursor
(
"..
.
"
)
.unwrap
();
ed
.insert_str_after_cursor
(
"..
=
"
)
.unwrap
();
let
pos1
=
ed
.cursor
();
ed
.insert_str_after_cursor
(
"..
.
"
)
.unwrap
();
ed
.insert_str_after_cursor
(
"..
=
"
)
.unwrap
();
let
pos2
=
ed
.cursor
();
ed
.insert_str_after_cursor
(
"word"
)
.unwrap
();
let
pos3
=
ed
.cursor
();
...
...
@@ -2755,7 +2753,7 @@ mod tests {
let
pos2
=
ed
.cursor
();
ed
.insert_str_after_cursor
(
"some"
)
.unwrap
();
let
pos3
=
ed
.cursor
();
ed
.insert_str_after_cursor
(
"..
.
"
)
.unwrap
();
ed
.insert_str_after_cursor
(
"..
=
"
)
.unwrap
();
let
pos4
=
ed
.cursor
();
ed
.insert_str_after_cursor
(
"words"
)
.unwrap
();
let
pos5
=
ed
.cursor
();
...
...
@@ -2816,7 +2814,7 @@ mod tests {
let
pos1
=
ed
.cursor
();
ed
.insert_str_after_cursor
(
"some"
)
.unwrap
();
let
pos2
=
ed
.cursor
();
ed
.insert_str_after_cursor
(
"..
.
"
)
.unwrap
();
ed
.insert_str_after_cursor
(
"..
=
"
)
.unwrap
();
ed
.insert_str_after_cursor
(
"words"
)
.unwrap
();
let
pos3
=
ed
.cursor
();
...
...
src/util.rs
View file @
af7084b4
...
...
@@ -66,14 +66,9 @@ pub fn remove_codes(input: &str) -> Cow<str> {
']'
=>
s
=
AnsiState
::
Osc
,
_
=>
s
=
AnsiState
::
Norm
,
},
AnsiState
::
Csi
=>
match
c
{
'A'
...
'Z'
|
'a'
...
'z'
=>
s
=
AnsiState
::
Norm
,
_
=>
(),
},
AnsiState
::
Osc
=>
match
c
{
'\x07'
=>
s
=
AnsiState
::
Norm
,
_
=>
(),
},
AnsiState
::
Csi
if
c
.is_ascii_alphabetic
()
=>
s
=
AnsiState
::
Norm
,
AnsiState
::
Osc
if
c
==
'\x07'
=>
s
=
AnsiState
::
Norm
,
_
=>
(),
}
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment