Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
redox-os
ion
Commits
f7c44d79
Commit
f7c44d79
authored
Jan 19, 2020
by
AdminXVII
Browse files
chore(cleanup): Apply some clippy lints and remove warnings
parent
02cba084
Changes
15
Expand all
Hide whitespace changes
Inline
Side-by-side
src/lib/assignments/actions.rs
View file @
f7c44d79
...
...
@@ -44,7 +44,7 @@ pub struct AssignmentActions<'a> {
}
impl
<
'a
>
AssignmentActions
<
'a
>
{
pub
fn
new
(
keys
:
&
'a
str
,
operator
:
Operator
,
values
:
&
'a
str
)
->
AssignmentActions
<
'a
>
{
pub
const
fn
new
(
keys
:
&
'a
str
,
operator
:
Operator
,
values
:
&
'a
str
)
->
AssignmentActions
<
'a
>
{
AssignmentActions
{
keys
:
KeyIterator
::
new
(
keys
),
operator
,
...
...
src/lib/builtins/mod.rs
View file @
f7c44d79
...
...
@@ -291,7 +291,7 @@ pub fn source_sh(args: &[types::Str], _shell: &mut Shell<'_>) -> Status {
Ok
(
v
)
=>
v
,
Err
(
e
)
=>
return
Status
::
error
(
format!
(
"Could not read env: {}"
,
e
)),
};
let
mut
iter
=
var
.splitn
(
2
,
"="
);
let
mut
iter
=
var
.splitn
(
2
,
'='
);
let
name
=
iter
.next
()
.unwrap
();
let
val
=
match
iter
.next
()
{
Some
(
v
)
=>
v
,
...
...
src/lib/expansion/methods/strings.rs
View file @
f7c44d79
...
...
@@ -159,10 +159,13 @@ impl<'a> StringMethod<'a> {
"trim_start"
=>
output
.push_str
(
get_var!
()
.trim_start
()),
"repeat"
=>
match
MethodArgs
::
new
(
self
.pattern
,
expand
)
.join
(
" "
)
?
.parse
::
<
usize
>
()
{
Ok
(
repeat
)
=>
output
.push_str
(
&
get_var!
()
.repeat
(
repeat
)),
Err
(
_
)
=>
Err
(
MethodError
::
WrongArgument
(
"repeat"
,
"argument is not a valid positive integer"
,
))
?
,
Err
(
_
)
=>
{
return
Err
(
MethodError
::
WrongArgument
(
"repeat"
,
"argument is not a valid positive integer"
,
)
.into
())
}
},
"replace"
=>
{
let
params
=
{
...
...
@@ -174,7 +177,13 @@ impl<'a> StringMethod<'a> {
(
Some
(
replace
),
Some
(
with
))
=>
{
output
.push_str
(
&
get_var!
()
.replace
(
replace
.as_str
(),
&
with
));
}
_
=>
Err
(
MethodError
::
WrongArgument
(
"replace"
,
"two arguments are required"
))
?
,
_
=>
{
return
Err
(
MethodError
::
WrongArgument
(
"replace"
,
"two arguments are required"
,
)
.into
())
}
}
}
"replacen"
=>
{
...
...
@@ -188,13 +197,20 @@ impl<'a> StringMethod<'a> {
if
let
Ok
(
nth
)
=
nth
.parse
::
<
usize
>
()
{
output
.push_str
(
&
get_var!
()
.replacen
(
replace
.as_str
(),
&
with
,
nth
));
}
else
{
Err
(
MethodError
::
WrongArgument
(
return
Err
(
MethodError
::
WrongArgument
(
"replacen"
,
"third argument isn't a valid integer"
,
))
?
)
.into
());
}
}
_
=>
Err
(
MethodError
::
WrongArgument
(
"replacen"
,
"three arguments required"
))
?
,
_
=>
{
return
Err
(
MethodError
::
WrongArgument
(
"replacen"
,
"three arguments required"
,
)
.into
())
}
}
}
"regex_replace"
=>
{
...
...
@@ -206,10 +222,16 @@ impl<'a> StringMethod<'a> {
match
params
{
(
Some
(
replace
),
Some
(
with
))
=>
match
Regex
::
new
(
&
replace
)
{
Ok
(
re
)
=>
output
.push_str
(
&
re
.replace_all
(
&
get_var!
(),
&
with
[
..
])),
Err
(
why
)
=>
Err
(
MethodError
::
InvalidRegex
(
replace
.to_string
(),
why
))
?
,
Err
(
why
)
=>
{
return
Err
(
MethodError
::
InvalidRegex
(
replace
.to_string
(),
why
)
.into
())
}
},
_
=>
{
Err
(
MethodError
::
WrongArgument
(
"regex_replace"
,
"two arguments required"
))
?
return
Err
(
MethodError
::
WrongArgument
(
"regex_replace"
,
"two arguments required"
,
)
.into
())
}
}
}
...
...
@@ -326,7 +348,11 @@ impl<'a> StringMethod<'a> {
output
.push_str
(
&
first_str
)
};
}
_
=>
Err
(
Error
::
from
(
MethodError
::
InvalidScalarMethod
(
self
.method
.to_string
())))
?
,
_
=>
{
return
Err
(
Error
::
from
(
MethodError
::
InvalidScalarMethod
(
self
.method
.to_string
()))
.into
()
)
}
}
Ok
(())
}
...
...
src/lib/expansion/pipelines.rs
View file @
f7c44d79
...
...
@@ -199,7 +199,7 @@ impl<'a> Pipeline<RefinedJob<'a>> {
impl
<
'a
>
Pipeline
<
Job
<
'a
>>
{
/// A useless, empty pipeline
pub
fn
new
()
->
Self
{
Pipeline
{
pipe
:
PipeType
::
Normal
,
items
:
Vec
::
new
()
}
}
pub
const
fn
new
()
->
Self
{
Pipeline
{
pipe
:
PipeType
::
Normal
,
items
:
Vec
::
new
()
}
}
/// Expand the pipeline to a set of arguments for execution
pub
fn
expand
(
...
...
src/lib/expansion/words/mod.rs
View file @
f7c44d79
This diff is collapsed.
Click to expand it.
src/lib/lib.rs
View file @
f7c44d79
...
...
@@ -71,7 +71,7 @@
#![allow(unknown_lints)]
#![deny(missing_docs)]
#![warn(clippy::all,
clippy::pedantic,
clippy::nursery,
clippy::cargo)]
#![warn(clippy::all,
clippy::nursery,
clippy::cargo)]
#![allow(clippy::cast_possible_wrap,
clippy::cast_sign_loss,
clippy::cast_possible_truncation)]
use
ion_ranges
as
ranges
;
...
...
src/lib/shell/assignments.rs
View file @
f7c44d79
...
...
@@ -130,13 +130,13 @@ impl<'b> Shell<'b> {
match
(
&
rhs
,
&
key
.kind
)
{
(
Value
::
HashMap
(
_
),
Primitive
::
Indexed
(
..
))
=>
{
Err
(
"cannot insert hmap into index"
.to_string
())
?
return
Err
(
"cannot insert hmap into index"
.to_string
())
}
(
Value
::
BTreeMap
(
_
),
Primitive
::
Indexed
(
..
))
=>
{
Err
(
"cannot insert bmap into index"
.to_string
())
?
return
Err
(
"cannot insert bmap into index"
.to_string
())
}
(
Value
::
Array
(
_
),
Primitive
::
Indexed
(
..
))
=>
{
Err
(
"multi-dimensional arrays are not yet supported"
.to_string
())
?
return
Err
(
"multi-dimensional arrays are not yet supported"
.to_string
())
}
_
if
[
Operator
::
Equal
,
Operator
::
OptionalEqual
]
.contains
(
&
operator
)
=>
{
backup
.push
((
key
,
rhs
))
...
...
src/lib/shell/flow.rs
View file @
f7c44d79
...
...
@@ -645,10 +645,11 @@ fn expand_pipeline<'a>(
pline
.items
.extend
(
item_iter
.cloned
());
}
else
{
// Error in expansion
Err
(
PipelineError
::
InvalidAlias
(
return
Err
(
PipelineError
::
InvalidAlias
(
item
.job.args
[
0
]
.to_string
(),
alias
.0
.to_string
(),
))
?
;
)
.into
());
}
}
_
=>
(),
...
...
src/lib/shell/flow_control.rs
View file @
f7c44d79
...
...
@@ -206,6 +206,7 @@ impl<'a> fmt::Display for Statement<'a> {
impl
<
'a
>
Statement
<
'a
>
{
/// Check if the statement is a block-based statement
#[must_use]
pub
fn
is_block
(
&
self
)
->
bool
{
match
*
self
{
Statement
::
Case
(
_
)
...
...
@@ -252,7 +253,7 @@ impl<'a> Function<'a> {
args
:
&
[
S
],
)
->
Result
<
(),
IonError
>
{
if
args
.len
()
-
1
!=
self
.args
.len
()
{
Err
(
FunctionError
::
InvalidArgumentCount
)
?
;
return
Err
(
FunctionError
::
InvalidArgumentCount
.into
()
);
}
let
values
=
self
...
...
@@ -293,9 +294,11 @@ impl<'a> Function<'a> {
}
/// Get the function's description
#[must_use]
pub
fn
description
(
&
self
)
->
Option
<&
types
::
Str
>
{
self
.description
.as_ref
()
}
/// Create a new function
#[must_use]
pub
const
fn
new
(
description
:
Option
<
types
::
Str
>
,
name
:
types
::
Str
,
...
...
src/lib/shell/job.rs
View file @
f7c44d79
...
...
@@ -104,7 +104,7 @@ pub struct TeeItem {
}
impl
TeeItem
{
pub
fn
new
()
->
Self
{
Self
{
sinks
:
Vec
::
new
(),
source
:
None
}
}
pub
const
fn
new
()
->
Self
{
Self
{
sinks
:
Vec
::
new
(),
source
:
None
}
}
pub
fn
add
(
&
mut
self
,
sink
:
File
)
{
self
.sinks
.push
(
sink
);
}
...
...
src/lib/shell/mod.rs
View file @
f7c44d79
...
...
@@ -80,23 +80,28 @@ pub enum IonError {
}
impl
From
<
ParseError
>
for
IonError
{
fn
from
(
cause
:
ParseError
)
->
Self
{
IonError
::
InvalidSyntax
(
cause
)
}
#[must_use]
fn
from
(
cause
:
ParseError
)
->
Self
{
Self
::
InvalidSyntax
(
cause
)
}
}
impl
From
<
FunctionError
>
for
IonError
{
fn
from
(
cause
:
FunctionError
)
->
Self
{
IonError
::
Function
(
cause
)
}
#[must_use]
fn
from
(
cause
:
FunctionError
)
->
Self
{
Self
::
Function
(
cause
)
}
}
impl
From
<
BlockError
>
for
IonError
{
fn
from
(
cause
:
BlockError
)
->
Self
{
IonError
::
StatementFlowError
(
cause
)
}
#[must_use]
fn
from
(
cause
:
BlockError
)
->
Self
{
Self
::
StatementFlowError
(
cause
)
}
}
impl
From
<
PipelineError
>
for
IonError
{
fn
from
(
cause
:
PipelineError
)
->
Self
{
IonError
::
PipelineExecutionError
(
cause
)
}
#[must_use]
fn
from
(
cause
:
PipelineError
)
->
Self
{
Self
::
PipelineExecutionError
(
cause
)
}
}
impl
From
<
ExpansionError
<
IonError
>>
for
IonError
{
fn
from
(
cause
:
ExpansionError
<
Self
>
)
->
Self
{
IonError
::
ExpansionError
(
cause
)
}
#[must_use]
fn
from
(
cause
:
ExpansionError
<
Self
>
)
->
Self
{
Self
::
ExpansionError
(
cause
)
}
}
/// Options for the shell
...
...
@@ -160,6 +165,7 @@ pub type PreCommandCallback<'a> = Box<dyn Fn(&Shell<'_>, &Pipeline<RefinedJob<'_
pub
type
BackgroundEventCallback
=
Arc
<
dyn
Fn
(
usize
,
Pid
,
BackgroundEvent
)
+
Send
+
Sync
>
;
impl
<
'a
>
Default
for
Shell
<
'a
>
{
#[must_use]
fn
default
()
->
Self
{
Self
::
new
()
}
}
...
...
@@ -186,9 +192,11 @@ impl<'a> Shell<'a> {
}
/// Create a new shell with default settings
#[must_use]
pub
fn
new
()
->
Self
{
Self
::
with_builtins
(
BuiltinMap
::
default
())
}
/// Create a shell with custom builtins
#[must_use]
pub
fn
with_builtins
(
builtins
:
BuiltinMap
<
'a
>
)
->
Self
{
Self
::
install_signal_handler
();
...
...
@@ -232,9 +240,11 @@ impl<'a> Shell<'a> {
}
/// Access the directory stack
#[must_use]
pub
const
fn
dir_stack
(
&
self
)
->
&
DirectoryStack
{
&
self
.directory_stack
}
/// Mutable access to the directory stack
#[must_use]
pub
fn
dir_stack_mut
(
&
mut
self
)
->
&
mut
DirectoryStack
{
&
mut
self
.directory_stack
}
/// Resets the flow control fields to their default values.
...
...
@@ -246,6 +256,7 @@ impl<'a> Shell<'a> {
}
/// Get the depth of the current block
#[must_use]
pub
fn
block_len
(
&
self
)
->
usize
{
self
.flow_control
.len
()
}
/// A method for executing a function, using `args` as the input.
...
...
@@ -338,13 +349,14 @@ impl<'a> Shell<'a> {
}
if
self
.opts.err_exit
&&
!
exit_status
.is_success
()
{
Err
(
PipelineError
::
EarlyExit
)
?
return
Err
(
PipelineError
::
EarlyExit
.into
());
}
Ok
(
exit_status
)
}
/// Get the pid of the last executed job
#[must_use]
pub
fn
previous_job
(
&
self
)
->
Option
<
usize
>
{
if
self
.previous_job
==
!
0
{
None
...
...
@@ -359,6 +371,7 @@ impl<'a> Shell<'a> {
}
/// Set the callback to call before each command
#[must_use]
pub
fn
background_event_mut
(
&
mut
self
)
->
&
mut
Option
<
BackgroundEventCallback
>
{
&
mut
self
.background_event
}
...
...
@@ -369,6 +382,7 @@ impl<'a> Shell<'a> {
}
/// Set the callback to call before each command
#[must_use]
pub
fn
pre_command_mut
(
&
mut
self
)
->
&
mut
Option
<
PreCommandCallback
<
'a
>>
{
&
mut
self
.pre_command
}
...
...
@@ -382,6 +396,7 @@ impl<'a> Shell<'a> {
pub
fn
on_command_mut
(
&
mut
self
)
->
&
mut
Option
<
OnCommandCallback
<
'a
>>
{
&
mut
self
.on_command
}
/// Get access to the builtins
#[must_use]
pub
const
fn
builtins
(
&
self
)
->
&
BuiltinMap
<
'a
>
{
&
self
.builtins
}
/// Get a mutable access to the builtins
...
...
@@ -389,21 +404,27 @@ impl<'a> Shell<'a> {
/// Warning: Previously defined functions will rely on previous versions of the builtins, even
/// if they are redefined. It is strongly advised to avoid mutating the builtins while the shell
/// is running
#[must_use]
pub
fn
builtins_mut
(
&
mut
self
)
->
&
mut
BuiltinMap
<
'a
>
{
&
mut
self
.builtins
}
/// Access to the shell options
#[must_use]
pub
const
fn
opts
(
&
self
)
->
&
Options
{
&
self
.opts
}
/// Mutable access to the shell options
#[must_use]
pub
fn
opts_mut
(
&
mut
self
)
->
&
mut
Options
{
&
mut
self
.opts
}
/// Access to the variables
#[must_use]
pub
const
fn
variables
(
&
self
)
->
&
Variables
<
'a
>
{
&
self
.variables
}
/// Mutable access to the variables
#[must_use]
pub
fn
variables_mut
(
&
mut
self
)
->
&
mut
Variables
<
'a
>
{
&
mut
self
.variables
}
/// Access to the variables
#[must_use]
pub
fn
background_jobs
<
'mutex
>
(
&
'mutex
self
,
)
->
impl
Deref
<
Target
=
Vec
<
BackgroundProcess
>>
+
'mutex
{
...
...
@@ -430,6 +451,7 @@ impl<'a> Shell<'a> {
pub
fn
set_previous_status
(
&
mut
self
,
status
:
Status
)
{
self
.previous_status
=
status
;
}
/// Get the last command's return code and/or the code for the error
#[must_use]
pub
const
fn
previous_status
(
&
self
)
->
Status
{
self
.previous_status
}
fn
assign
(
&
mut
self
,
key
:
&
Key
<
'_
>
,
value
:
Value
<
Rc
<
Function
<
'a
>>>
)
->
Result
<
(),
String
>
{
...
...
src/lib/shell/pipe_exec/job_control.rs
View file @
f7c44d79
...
...
@@ -31,9 +31,9 @@ pub enum ProcessState {
impl
fmt
::
Display
for
ProcessState
{
fn
fmt
(
&
self
,
f
:
&
mut
fmt
::
Formatter
<
'_
>
)
->
fmt
::
Result
{
match
*
self
{
ProcessState
::
Running
=>
write!
(
f
,
"Running"
),
ProcessState
::
Stopped
=>
write!
(
f
,
"Stopped"
),
ProcessState
::
Empty
=>
write!
(
f
,
"Empty"
),
Self
::
Running
=>
write!
(
f
,
"Running"
),
Self
::
Stopped
=>
write!
(
f
,
"Stopped"
),
Self
::
Empty
=>
write!
(
f
,
"Empty"
),
}
}
}
...
...
@@ -72,12 +72,15 @@ impl BackgroundProcess {
}
/// Get the pid associated with the job
#[must_use]
pub
const
fn
pid
(
&
self
)
->
Pid
{
self
.pid
}
/// Check if the process is still running
#[must_use]
pub
fn
is_running
(
&
self
)
->
bool
{
self
.state
==
ProcessState
::
Running
}
/// Check if this is in fact a process
#[must_use]
pub
fn
exists
(
&
self
)
->
bool
{
self
.state
!=
ProcessState
::
Empty
}
/// Stop capturing information about the process. *This action is irreversible*
...
...
src/lib/shell/pipe_exec/mod.rs
View file @
f7c44d79
...
...
@@ -119,7 +119,8 @@ pub enum PipelineError {
}
impl
From
<
RedirectError
>
for
PipelineError
{
fn
from
(
cause
:
RedirectError
)
->
Self
{
PipelineError
::
RedirectPipeError
(
cause
)
}
#[must_use]
fn
from
(
cause
:
RedirectError
)
->
Self
{
Self
::
RedirectPipeError
(
cause
)
}
}
/// Create an OS pipe and write the contents of a byte slice to one end
...
...
@@ -146,11 +147,11 @@ pub fn stdin_of<T: AsRef<str>>(input: &T) -> Result<File, PipelineError> {
impl
Input
{
pub
(
self
)
fn
get_infile
(
&
self
)
->
Result
<
File
,
PipelineError
>
{
match
self
{
Input
::
File
(
ref
filename
)
=>
match
File
::
open
(
filename
.as_str
())
{
Self
::
File
(
ref
filename
)
=>
match
File
::
open
(
filename
.as_str
())
{
Ok
(
file
)
=>
Ok
(
file
),
Err
(
why
)
=>
Err
(
RedirectError
::
File
(
filename
.to_string
(),
why
)
.into
()),
},
Input
::
HereString
(
ref
string
)
=>
stdin_of
(
&
string
),
Self
::
HereString
(
ref
string
)
=>
stdin_of
(
&
string
),
}
}
}
...
...
@@ -295,7 +296,6 @@ fn prepare<'a>(
impl
<
'b
>
Shell
<
'b
>
{
/// For tee jobs
fn
exec_multi_out
(
&
mut
self
,
items
:
&
mut
(
Option
<
TeeItem
>
,
Option
<
TeeItem
>
),
redirection
:
RedirectFrom
,
)
->
Status
{
...
...
@@ -322,7 +322,7 @@ impl<'b> Shell<'b> {
}
/// For cat jobs
fn
exec_multi_in
(
&
mut
self
,
sources
:
&
mut
[
File
],
stdin
:
&
mut
Option
<
File
>
)
->
Status
{
fn
exec_multi_in
(
sources
:
&
mut
[
File
],
stdin
:
&
mut
Option
<
File
>
)
->
Status
{
let
stdout
=
io
::
stdout
();
let
mut
stdout
=
stdout
.lock
();
for
file
in
stdin
.iter_mut
()
.chain
(
sources
)
{
...
...
@@ -546,12 +546,12 @@ fn spawn_proc(
}),
Variant
::
Cat
{
ref
mut
sources
}
=>
{
fork_exec_internal
(
stdout
,
None
,
stdin
,
*
group
,
|
_
,
_
,
mut
stdin
|
{
s
hell
.
exec_multi_in
(
sources
,
&
mut
stdin
)
S
hell
::
exec_multi_in
(
sources
,
&
mut
stdin
)
})
}
Variant
::
Tee
{
ref
mut
items
}
=>
{
fork_exec_internal
(
stdout
,
stderr
,
stdin
,
*
group
,
|
_
,
_
,
_
|
{
s
hell
.
exec_multi_out
(
items
,
redirection
)
S
hell
::
exec_multi_out
(
items
,
redirection
)
})
}
}
?
;
...
...
src/lib/shell/signals.rs
View file @
f7c44d79
...
...
@@ -23,7 +23,7 @@ pub struct SignalHandler;
impl
SignalHandler
{
pub
fn
new
()
->
Self
{
block
();
S
ignalHandler
S
elf
}
}
...
...
src/lib/shell/variables.rs
View file @
f7c44d79
...
...
@@ -88,6 +88,7 @@ impl<'a> Variables<'a> {
self
.0
.append_scopes
(
scopes
)
}
#[must_use]
pub
(
crate
)
fn
index_scope_for_var
(
&
self
,
name
:
&
str
)
->
Option
<
usize
>
{
self
.0
.index_scope_for_var
(
name
)
}
...
...
@@ -109,6 +110,7 @@ impl<'a> Variables<'a> {
/// Further minimizes the directory path in the same manner that Fish does by default.
/// That is, if more than two parents are visible in the path, all parent directories
/// of the current directory will be reduced to a single character.
#[must_use]
fn
get_minimal_directory
(
&
self
)
->
types
::
Str
{
let
swd
=
self
.get_simplified_directory
();
...
...
@@ -140,12 +142,14 @@ impl<'a> Variables<'a> {
///
/// Useful for getting smaller prompts, this will produce a simplified variant of the
/// working directory which the leading `HOME` prefix replaced with a tilde character.
#[must_use]
fn
get_simplified_directory
(
&
self
)
->
types
::
Str
{
let
home
=
self
.get_str
(
"HOME"
)
.unwrap_or_else
(|
_
|
"?"
.into
());
env
::
var
(
"PWD"
)
.unwrap
()
.replace
(
&*
home
,
"~"
)
.into
()
}
/// Indicates if name is valid for functions and variables
#[must_use]
pub
fn
is_valid_name
(
name
:
&
str
)
->
bool
{
let
mut
iter
=
name
.chars
();
iter
.next
()
.map_or
(
false
,
|
c
|
c
.is_alphabetic
()
||
c
==
'_'
)
...
...
@@ -197,6 +201,7 @@ impl<'a> Variables<'a> {
}
/// Get a variable on the current scope
#[must_use]
pub
fn
get
(
&
self
,
mut
name
:
&
str
)
->
Option
<&
Value
<
Rc
<
Function
<
'a
>>>>
{
const
GLOBAL_NS
:
&
str
=
"global::"
;
const
SUPER_NS
:
&
str
=
"super::"
;
...
...
@@ -220,6 +225,7 @@ impl<'a> Variables<'a> {
}
/// Get a mutable access to a variable on the current scope
#[must_use]
pub
fn
get_mut
(
&
mut
self
,
name
:
&
str
)
->
Option
<&
mut
Value
<
Rc
<
Function
<
'a
>>>>
{
if
name
.starts_with
(
"super::"
)
||
name
.starts_with
(
"global::"
)
{
// Cannot mutate outer namespace
...
...
@@ -230,6 +236,7 @@ impl<'a> Variables<'a> {
}
impl
<
'a
>
Default
for
Variables
<
'a
>
{
#[must_use]
fn
default
()
->
Self
{
let
mut
map
:
Scopes
<
types
::
Str
,
Value
<
Rc
<
Function
<
'a
>>>>
=
Scopes
::
with_capacity
(
64
);
map
.set
(
"HISTORY_SIZE"
,
"1000"
);
...
...
Write
Preview
Supports
Markdown
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