mirror of
https://github.com/zyedidia/micro.git
synced 2026-03-16 05:47:06 +09:00
Add help
This commit is contained in:
@@ -967,6 +967,7 @@ func (h *BufHandler) ClearStatus() bool {
|
|||||||
|
|
||||||
// ToggleHelp toggles the help screen
|
// ToggleHelp toggles the help screen
|
||||||
func (h *BufHandler) ToggleHelp() bool {
|
func (h *BufHandler) ToggleHelp() bool {
|
||||||
|
h.openHelp("help")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package action
|
package action
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -274,8 +276,37 @@ func (h *BufHandler) ToggleLogCmd(args []string) {
|
|||||||
func (h *BufHandler) ReloadCmd(args []string) {
|
func (h *BufHandler) ReloadCmd(args []string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *BufHandler) openHelp(page string) error {
|
||||||
|
if data, err := config.FindRuntimeFile(config.RTHelp, page).Data(); err != nil {
|
||||||
|
return errors.New(fmt.Sprint("Unable to load help text", page, "\n", err))
|
||||||
|
} else {
|
||||||
|
helpBuffer := buffer.NewBufferFromString(string(data), page+".md", buffer.BTHelp)
|
||||||
|
helpBuffer.SetName("Help " + page)
|
||||||
|
|
||||||
|
if h.Buf.Type == buffer.BTHelp {
|
||||||
|
h.OpenBuffer(helpBuffer)
|
||||||
|
} else {
|
||||||
|
h.HSplitBuf(helpBuffer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// HelpCmd tries to open the given help page in a horizontal split
|
// HelpCmd tries to open the given help page in a horizontal split
|
||||||
func (h *BufHandler) HelpCmd(args []string) {
|
func (h *BufHandler) HelpCmd(args []string) {
|
||||||
|
if len(args) < 1 {
|
||||||
|
// Open the default help if the user just typed "> help"
|
||||||
|
h.openHelp("help")
|
||||||
|
} else {
|
||||||
|
if config.FindRuntimeFile(config.RTHelp, args[0]) != nil {
|
||||||
|
err := h.openHelp(args[0])
|
||||||
|
if err != nil {
|
||||||
|
InfoBar.Error(err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
InfoBar.Error("Sorry, no help for ", args[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// VSplitCmd opens a vertical split with file given in the first argument
|
// VSplitCmd opens a vertical split with file given in the first argument
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -38,6 +39,25 @@ var (
|
|||||||
ErrFileTooLarge = errors.New("File is too large to hash")
|
ErrFileTooLarge = errors.New("File is too large to hash")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type SharedBuffer struct {
|
||||||
|
*LineArray
|
||||||
|
// Stores the last modification time of the file the buffer is pointing to
|
||||||
|
ModTime time.Time
|
||||||
|
// Type of the buffer (e.g. help, raw, scratch etc..)
|
||||||
|
Type BufType
|
||||||
|
|
||||||
|
isModified bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *SharedBuffer) insert(pos Loc, value []byte) {
|
||||||
|
b.isModified = true
|
||||||
|
b.LineArray.insert(pos, value)
|
||||||
|
}
|
||||||
|
func (b *SharedBuffer) remove(start, end Loc) []byte {
|
||||||
|
b.isModified = true
|
||||||
|
return b.LineArray.remove(start, end)
|
||||||
|
}
|
||||||
|
|
||||||
// Buffer stores the main information about a currently open file including
|
// Buffer stores the main information about a currently open file including
|
||||||
// the actual text (in a LineArray), the undo/redo stack (in an EventHandler)
|
// the actual text (in a LineArray), the undo/redo stack (in an EventHandler)
|
||||||
// all the cursors, the syntax highlighting info, the settings for the buffer
|
// all the cursors, the syntax highlighting info, the settings for the buffer
|
||||||
@@ -46,8 +66,8 @@ var (
|
|||||||
// highlighter attaches information to each line of the buffer for optimization
|
// highlighter attaches information to each line of the buffer for optimization
|
||||||
// purposes so it doesn't have to rehighlight everything on every update.
|
// purposes so it doesn't have to rehighlight everything on every update.
|
||||||
type Buffer struct {
|
type Buffer struct {
|
||||||
*LineArray
|
|
||||||
*EventHandler
|
*EventHandler
|
||||||
|
*SharedBuffer
|
||||||
|
|
||||||
cursors []*Cursor
|
cursors []*Cursor
|
||||||
curCursor int
|
curCursor int
|
||||||
@@ -60,9 +80,6 @@ type Buffer struct {
|
|||||||
// Name of the buffer on the status line
|
// Name of the buffer on the status line
|
||||||
name string
|
name string
|
||||||
|
|
||||||
// Stores the last modification time of the file the buffer is pointing to
|
|
||||||
ModTime *time.Time
|
|
||||||
|
|
||||||
SyntaxDef *highlight.Def
|
SyntaxDef *highlight.Def
|
||||||
Highlighter *highlight.Highlighter
|
Highlighter *highlight.Highlighter
|
||||||
|
|
||||||
@@ -72,9 +89,6 @@ type Buffer struct {
|
|||||||
// Settings customized by the user
|
// Settings customized by the user
|
||||||
Settings map[string]interface{}
|
Settings map[string]interface{}
|
||||||
|
|
||||||
// Type of the buffer (e.g. help, raw, scratch etc..)
|
|
||||||
Type BufType
|
|
||||||
|
|
||||||
Messages []*Message
|
Messages []*Message
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +136,6 @@ func NewBuffer(reader io.Reader, size int64, path string, cursorPosition []strin
|
|||||||
absPath, _ := filepath.Abs(path)
|
absPath, _ := filepath.Abs(path)
|
||||||
|
|
||||||
b := new(Buffer)
|
b := new(Buffer)
|
||||||
b.Type = btype
|
|
||||||
|
|
||||||
b.Settings = config.DefaultLocalSettings()
|
b.Settings = config.DefaultLocalSettings()
|
||||||
for k, v := range config.GlobalSettings {
|
for k, v := range config.GlobalSettings {
|
||||||
@@ -137,28 +150,24 @@ func NewBuffer(reader io.Reader, size int64, path string, cursorPosition []strin
|
|||||||
for _, buf := range OpenBuffers {
|
for _, buf := range OpenBuffers {
|
||||||
if buf.AbsPath == absPath {
|
if buf.AbsPath == absPath {
|
||||||
found = true
|
found = true
|
||||||
b.LineArray = buf.LineArray
|
b.SharedBuffer = buf.SharedBuffer
|
||||||
b.EventHandler = buf.EventHandler
|
b.EventHandler = buf.EventHandler
|
||||||
b.ModTime = buf.ModTime
|
|
||||||
b.isModified = buf.isModified
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if !found {
|
||||||
|
b.SharedBuffer = new(SharedBuffer)
|
||||||
|
b.Type = btype
|
||||||
b.LineArray = NewLineArray(uint64(size), FFAuto, reader)
|
b.LineArray = NewLineArray(uint64(size), FFAuto, reader)
|
||||||
b.EventHandler = NewEventHandler(b.LineArray, b.cursors)
|
b.EventHandler = NewEventHandler(b.SharedBuffer, b.cursors)
|
||||||
b.ModTime = new(time.Time)
|
|
||||||
b.isModified = new(bool)
|
|
||||||
*b.isModified = false
|
|
||||||
*b.ModTime = time.Time{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
b.Path = path
|
b.Path = path
|
||||||
b.AbsPath = absPath
|
b.AbsPath = absPath
|
||||||
|
|
||||||
// The last time this file was modified
|
// The last time this file was modified
|
||||||
*b.ModTime, _ = GetModTime(b.Path)
|
b.ModTime, _ = GetModTime(b.Path)
|
||||||
|
|
||||||
b.UpdateRules()
|
b.UpdateRules()
|
||||||
|
|
||||||
@@ -219,6 +228,11 @@ func (b *Buffer) GetName() string {
|
|||||||
return b.name
|
return b.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//SetName changes the name for this buffer
|
||||||
|
func (b *Buffer) SetName(s string) {
|
||||||
|
b.name = s
|
||||||
|
}
|
||||||
|
|
||||||
// FileType returns the buffer's filetype
|
// FileType returns the buffer's filetype
|
||||||
func (b *Buffer) FileType() string {
|
func (b *Buffer) FileType() string {
|
||||||
return b.Settings["filetype"].(string)
|
return b.Settings["filetype"].(string)
|
||||||
@@ -234,8 +248,8 @@ func (b *Buffer) ReOpen() error {
|
|||||||
}
|
}
|
||||||
b.EventHandler.ApplyDiff(txt)
|
b.EventHandler.ApplyDiff(txt)
|
||||||
|
|
||||||
*b.ModTime, err = GetModTime(b.Path)
|
b.ModTime, err = GetModTime(b.Path)
|
||||||
*b.isModified = false
|
b.isModified = false
|
||||||
for _, c := range b.cursors {
|
for _, c := range b.cursors {
|
||||||
c.Relocate()
|
c.Relocate()
|
||||||
}
|
}
|
||||||
@@ -264,7 +278,7 @@ func (b *Buffer) RuneAt(loc Loc) rune {
|
|||||||
// being opened
|
// being opened
|
||||||
func (b *Buffer) Modified() bool {
|
func (b *Buffer) Modified() bool {
|
||||||
if b.Settings["fastdirty"].(bool) {
|
if b.Settings["fastdirty"].(bool) {
|
||||||
return *b.isModified
|
return b.isModified
|
||||||
}
|
}
|
||||||
|
|
||||||
var buff [md5.Size]byte
|
var buff [md5.Size]byte
|
||||||
@@ -402,6 +416,7 @@ func (b *Buffer) SetCursors(c []*Cursor) {
|
|||||||
func (b *Buffer) AddCursor(c *Cursor) {
|
func (b *Buffer) AddCursor(c *Cursor) {
|
||||||
b.cursors = append(b.cursors, c)
|
b.cursors = append(b.cursors, c)
|
||||||
b.EventHandler.cursors = b.cursors
|
b.EventHandler.cursors = b.cursors
|
||||||
|
log.Println(b.cursors)
|
||||||
b.UpdateCursors()
|
b.UpdateCursors()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package buffer
|
package buffer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
@@ -37,7 +38,7 @@ type Delta struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ExecuteTextEvent runs a text event
|
// ExecuteTextEvent runs a text event
|
||||||
func ExecuteTextEvent(t *TextEvent, buf *LineArray) {
|
func ExecuteTextEvent(t *TextEvent, buf *SharedBuffer) {
|
||||||
if t.EventType == TextEventInsert {
|
if t.EventType == TextEventInsert {
|
||||||
for _, d := range t.Deltas {
|
for _, d := range t.Deltas {
|
||||||
buf.insert(d.Start, d.Text)
|
buf.insert(d.Start, d.Text)
|
||||||
@@ -60,25 +61,25 @@ func ExecuteTextEvent(t *TextEvent, buf *LineArray) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UndoTextEvent undoes a text event
|
// UndoTextEvent undoes a text event
|
||||||
func UndoTextEvent(t *TextEvent, buf *LineArray) {
|
func UndoTextEvent(t *TextEvent, buf *SharedBuffer) {
|
||||||
t.EventType = -t.EventType
|
t.EventType = -t.EventType
|
||||||
ExecuteTextEvent(t, buf)
|
ExecuteTextEvent(t, buf)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EventHandler executes text manipulations and allows undoing and redoing
|
// EventHandler executes text manipulations and allows undoing and redoing
|
||||||
type EventHandler struct {
|
type EventHandler struct {
|
||||||
buf *LineArray
|
buf *SharedBuffer
|
||||||
cursors []*Cursor
|
cursors []*Cursor
|
||||||
UndoStack *TEStack
|
UndoStack *TEStack
|
||||||
RedoStack *TEStack
|
RedoStack *TEStack
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewEventHandler returns a new EventHandler
|
// NewEventHandler returns a new EventHandler
|
||||||
func NewEventHandler(la *LineArray, cursors []*Cursor) *EventHandler {
|
func NewEventHandler(buf *SharedBuffer, cursors []*Cursor) *EventHandler {
|
||||||
eh := new(EventHandler)
|
eh := new(EventHandler)
|
||||||
eh.UndoStack = new(TEStack)
|
eh.UndoStack = new(TEStack)
|
||||||
eh.RedoStack = new(TEStack)
|
eh.RedoStack = new(TEStack)
|
||||||
eh.buf = la
|
eh.buf = buf
|
||||||
eh.cursors = cursors
|
eh.cursors = cursors
|
||||||
return eh
|
return eh
|
||||||
}
|
}
|
||||||
@@ -93,12 +94,12 @@ func (eh *EventHandler) ApplyDiff(new string) {
|
|||||||
loc := eh.buf.Start()
|
loc := eh.buf.Start()
|
||||||
for _, d := range diff {
|
for _, d := range diff {
|
||||||
if d.Type == dmp.DiffDelete {
|
if d.Type == dmp.DiffDelete {
|
||||||
eh.Remove(loc, loc.MoveLA(utf8.RuneCountInString(d.Text), eh.buf))
|
eh.Remove(loc, loc.MoveLA(utf8.RuneCountInString(d.Text), eh.buf.LineArray))
|
||||||
} else {
|
} else {
|
||||||
if d.Type == dmp.DiffInsert {
|
if d.Type == dmp.DiffInsert {
|
||||||
eh.Insert(loc, d.Text)
|
eh.Insert(loc, d.Text)
|
||||||
}
|
}
|
||||||
loc = loc.MoveLA(utf8.RuneCountInString(d.Text), eh.buf)
|
loc = loc.MoveLA(utf8.RuneCountInString(d.Text), eh.buf.LineArray)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,15 +114,16 @@ func (eh *EventHandler) Insert(start Loc, textStr string) {
|
|||||||
Time: time.Now(),
|
Time: time.Now(),
|
||||||
}
|
}
|
||||||
eh.Execute(e)
|
eh.Execute(e)
|
||||||
e.Deltas[0].End = start.MoveLA(utf8.RuneCount(text), eh.buf)
|
e.Deltas[0].End = start.MoveLA(utf8.RuneCount(text), eh.buf.LineArray)
|
||||||
end := e.Deltas[0].End
|
end := e.Deltas[0].End
|
||||||
|
|
||||||
|
log.Println(eh.cursors)
|
||||||
for _, c := range eh.cursors {
|
for _, c := range eh.cursors {
|
||||||
move := func(loc Loc) Loc {
|
move := func(loc Loc) Loc {
|
||||||
if start.Y != end.Y && loc.GreaterThan(start) {
|
if start.Y != end.Y && loc.GreaterThan(start) {
|
||||||
loc.Y += end.Y - start.Y
|
loc.Y += end.Y - start.Y
|
||||||
} else if loc.Y == start.Y && loc.GreaterEqual(start) {
|
} else if loc.Y == start.Y && loc.GreaterEqual(start) {
|
||||||
loc = loc.MoveLA(utf8.RuneCount(text), eh.buf)
|
loc = loc.MoveLA(utf8.RuneCount(text), eh.buf.LineArray)
|
||||||
}
|
}
|
||||||
return loc
|
return loc
|
||||||
}
|
}
|
||||||
@@ -149,7 +151,7 @@ func (eh *EventHandler) Remove(start, end Loc) {
|
|||||||
if start.Y != end.Y && loc.GreaterThan(end) {
|
if start.Y != end.Y && loc.GreaterThan(end) {
|
||||||
loc.Y -= end.Y - start.Y
|
loc.Y -= end.Y - start.Y
|
||||||
} else if loc.Y == end.Y && loc.GreaterEqual(end) {
|
} else if loc.Y == end.Y && loc.GreaterEqual(end) {
|
||||||
loc = loc.MoveLA(-DiffLA(start, end, eh.buf), eh.buf)
|
loc = loc.MoveLA(-DiffLA(start, end, eh.buf.LineArray), eh.buf.LineArray)
|
||||||
}
|
}
|
||||||
return loc
|
return loc
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ type LineArray struct {
|
|||||||
lines []Line
|
lines []Line
|
||||||
endings FileFormat
|
endings FileFormat
|
||||||
initsize uint64
|
initsize uint64
|
||||||
isModified *bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append efficiently appends lines together
|
// Append efficiently appends lines together
|
||||||
@@ -162,7 +161,6 @@ func (la *LineArray) newlineBelow(y int) {
|
|||||||
|
|
||||||
// Inserts a byte array at a given location
|
// Inserts a byte array at a given location
|
||||||
func (la *LineArray) insert(pos Loc, value []byte) {
|
func (la *LineArray) insert(pos Loc, value []byte) {
|
||||||
*la.isModified = true
|
|
||||||
x, y := runeToByteIndex(pos.X, la.lines[pos.Y].data), pos.Y
|
x, y := runeToByteIndex(pos.X, la.lines[pos.Y].data), pos.Y
|
||||||
for i := 0; i < len(value); i++ {
|
for i := 0; i < len(value); i++ {
|
||||||
if value[i] == '\n' {
|
if value[i] == '\n' {
|
||||||
@@ -203,7 +201,6 @@ func (la *LineArray) split(pos Loc) {
|
|||||||
|
|
||||||
// removes from start to end
|
// removes from start to end
|
||||||
func (la *LineArray) remove(start, end Loc) []byte {
|
func (la *LineArray) remove(start, end Loc) []byte {
|
||||||
*la.isModified = true
|
|
||||||
sub := la.Substr(start, end)
|
sub := la.Substr(start, end)
|
||||||
startX := runeToByteIndex(start.X, la.lines[start.Y].data)
|
startX := runeToByteIndex(start.X, la.lines[start.Y].data)
|
||||||
endX := runeToByteIndex(end.X, la.lines[end.Y].data)
|
endX := runeToByteIndex(end.X, la.lines[end.Y].data)
|
||||||
@@ -222,7 +219,6 @@ func (la *LineArray) remove(start, end Loc) []byte {
|
|||||||
|
|
||||||
// deleteToEnd deletes from the end of a line to the position
|
// deleteToEnd deletes from the end of a line to the position
|
||||||
func (la *LineArray) deleteToEnd(pos Loc) {
|
func (la *LineArray) deleteToEnd(pos Loc) {
|
||||||
*la.isModified = true
|
|
||||||
la.lines[pos.Y].data = la.lines[pos.Y].data[:pos.X]
|
la.lines[pos.Y].data = la.lines[pos.Y].data[:pos.X]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ func (b *Buffer) SaveAs(filename string) error {
|
|||||||
|
|
||||||
// Update the last time this file was updated after saving
|
// Update the last time this file was updated after saving
|
||||||
defer func() {
|
defer func() {
|
||||||
*b.ModTime, _ = GetModTime(filename)
|
b.ModTime, _ = GetModTime(filename)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Removes any tilde and replaces with the absolute path to home
|
// Removes any tilde and replaces with the absolute path to home
|
||||||
@@ -146,7 +146,7 @@ func (b *Buffer) SaveAs(filename string) error {
|
|||||||
b.Path = filename
|
b.Path = filename
|
||||||
absPath, _ := filepath.Abs(filename)
|
absPath, _ := filepath.Abs(filename)
|
||||||
b.AbsPath = absPath
|
b.AbsPath = absPath
|
||||||
*b.isModified = false
|
b.isModified = false
|
||||||
return b.Serialize()
|
return b.Serialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,8 +182,8 @@ func (b *Buffer) SaveAsWithSudo(filename string) error {
|
|||||||
err := cmd.Wait()
|
err := cmd.Wait()
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
*b.isModified = false
|
b.isModified = false
|
||||||
*b.ModTime, _ = GetModTime(filename)
|
b.ModTime, _ = GetModTime(filename)
|
||||||
return b.Serialize()
|
return b.Serialize()
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func (b *Buffer) Serialize() error {
|
|||||||
err := gob.NewEncoder(file).Encode(SerializedBuffer{
|
err := gob.NewEncoder(file).Encode(SerializedBuffer{
|
||||||
b.EventHandler,
|
b.EventHandler,
|
||||||
b.GetActiveCursor().Loc,
|
b.GetActiveCursor().Loc,
|
||||||
*b.ModTime,
|
b.ModTime,
|
||||||
})
|
})
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
@@ -61,10 +61,10 @@ func (b *Buffer) Unserialize() error {
|
|||||||
|
|
||||||
if b.Settings["saveundo"].(bool) {
|
if b.Settings["saveundo"].(bool) {
|
||||||
// We should only use last time's eventhandler if the file wasn't modified by someone else in the meantime
|
// We should only use last time's eventhandler if the file wasn't modified by someone else in the meantime
|
||||||
if *b.ModTime == buffer.ModTime {
|
if b.ModTime == buffer.ModTime {
|
||||||
b.EventHandler = buffer.EventHandler
|
b.EventHandler = buffer.EventHandler
|
||||||
b.EventHandler.cursors = b.cursors
|
b.EventHandler.cursors = b.cursors
|
||||||
b.EventHandler.buf = b.LineArray
|
b.EventHandler.buf = b.SharedBuffer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func (b *Buffer) SetOption(option, value string) error {
|
|||||||
} else if option == "filetype" {
|
} else if option == "filetype" {
|
||||||
b.UpdateRules()
|
b.UpdateRules()
|
||||||
} else if option == "fileformat" {
|
} else if option == "fileformat" {
|
||||||
*b.isModified = true
|
b.isModified = true
|
||||||
} else if option == "syntax" {
|
} else if option == "syntax" {
|
||||||
if !nativeValue.(bool) {
|
if !nativeValue.(bool) {
|
||||||
b.ClearMatches()
|
b.ClearMatches()
|
||||||
|
|||||||
Reference in New Issue
Block a user