mirror of
https://github.com/zoriya/jj.nvim.git
synced 2026-08-05 02:36:07 +00:00
Change the :J describe behaviour to open an editable buffer instead of an input box (#6)
Co-authored-by: fautore <fautore@protonmail.com>
This commit is contained in:
@@ -16,7 +16,7 @@ This plugin aims to be something like vim-fugitive but for driving the jj-vcs CL
|
||||
- Terminal-based output display for jj commands
|
||||
- Support jj subcommands including your aliases through the cmdline.
|
||||
- First class citizens with ui integration
|
||||
- `describe` - Set change descriptions
|
||||
- `describe` - Set change descriptions with a Git-style commit message editor
|
||||
- `status` / `st` - Show repository status
|
||||
- `log` - Display log history with configurable options
|
||||
- `diff` - Show changes
|
||||
@@ -91,11 +91,66 @@ The plugin provides a `:J` command that accepts jj subcommands:
|
||||
snacks = {
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
-- Choose the editor mode for describe command
|
||||
-- "buffer" - Opens a Git-style commit message buffer with syntax highlighting (default)
|
||||
-- "input" - Uses a simple vim.ui.input prompt
|
||||
describe_editor = "buffer",
|
||||
|
||||
-- Customize syntax highlighting colors for the describe buffer
|
||||
highlights = {
|
||||
added = { fg = "#3fb950", ctermfg = "Green" }, -- Added files
|
||||
modified = { fg = "#56d4dd", ctermfg = "Cyan" }, -- Modified files
|
||||
deleted = { fg = "#f85149", ctermfg = "Red" }, -- Deleted files
|
||||
renamed = { fg = "#d29922", ctermfg = "Yellow" }, -- Renamed files
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Describe Editor Modes
|
||||
|
||||
The `describe_editor` option lets you choose how you want to write commit descriptions:
|
||||
|
||||
- **`"buffer"`** (default) - Opens a full buffer editor similar to Git's commit message editor
|
||||
- Shows file changes with syntax highlighting
|
||||
- Multi-line editing with proper formatting
|
||||
- Close with `q` or `<Esc>`, save with `:w` or `:wq`
|
||||
|
||||
- **`"input"`** - Simple single-line input prompt
|
||||
- Quick and minimal
|
||||
- Good for short, single-line descriptions
|
||||
- Uses `vim.ui.input()` which can be customized by UI plugins like dressing.nvim
|
||||
|
||||
Example:
|
||||
```lua
|
||||
require("jj").setup({
|
||||
describe_editor = "input", -- Use simple input mode
|
||||
})
|
||||
```
|
||||
|
||||
### Highlight Customization
|
||||
|
||||
The `highlights` option allows you to customize the colors used in the describe buffer's file status display. Each highlight accepts standard Neovim highlight attributes:
|
||||
|
||||
- `fg` - Foreground color (hex or color name)
|
||||
- `bg` - Background color
|
||||
- `ctermfg` - Terminal foreground color
|
||||
- `ctermbg` - Terminal background color
|
||||
- `bold`, `italic`, `underline` - Text styles
|
||||
|
||||
Example with custom colors:
|
||||
|
||||
```lua
|
||||
require("jj").setup({
|
||||
highlights = {
|
||||
modified = { fg = "#89ddff", bold = true },
|
||||
added = { fg = "#c3e88d", ctermfg = "LightGreen" },
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## Example config
|
||||
|
||||
```lua
|
||||
@@ -105,35 +160,38 @@ The plugin provides a `:J` command that accepts jj subcommands:
|
||||
"folke/snacks.nvim", -- Optional only if you use picker's
|
||||
},
|
||||
config = function()
|
||||
require("jj").setup({})
|
||||
local cmd = require "jj.cmd"
|
||||
vim.keymap.set("n", "<leader>jd", cmd.describe, { desc = "JJ describe" })
|
||||
vim.keymap.set("n", "<leader>jl", cmd.log, { desc = "JJ log" })
|
||||
vim.keymap.set("n", "<leader>je", cmd.edit, { desc = "JJ edit" })
|
||||
vim.keymap.set("n", "<leader>jn", cmd.new, { desc = "JJ new" })
|
||||
vim.keymap.set("n", "<leader>js", cmd.status, { desc = "JJ status" })
|
||||
vim.keymap.set("n", "<leader>dj", cmd.diff, { desc = "JJ diff" })
|
||||
vim.keymap.set("n", "<leader>sj", cmd.squash, { desc = "JJ squash" })
|
||||
require("jj").setup({
|
||||
highlights = {
|
||||
-- Customize colors if desired
|
||||
modified = { fg = "#89ddff" },
|
||||
}
|
||||
})
|
||||
|
||||
-- Use the exposed functions directly from the main module
|
||||
local jj = require("jj")
|
||||
vim.keymap.set("n", "<leader>jd", jj.describe, { desc = "JJ describe" })
|
||||
vim.keymap.set("n", "<leader>jl", jj.log, { desc = "JJ log" })
|
||||
vim.keymap.set("n", "<leader>je", jj.edit, { desc = "JJ edit" })
|
||||
vim.keymap.set("n", "<leader>jn", jj.new, { desc = "JJ new" })
|
||||
vim.keymap.set("n", "<leader>js", jj.status, { desc = "JJ status" })
|
||||
vim.keymap.set("n", "<leader>dj", jj.diff, { desc = "JJ diff" })
|
||||
vim.keymap.set("n", "<leader>sj", jj.squash, { desc = "JJ squash" })
|
||||
|
||||
-- Pickers
|
||||
vim.keymap.set("n", "<leader>gj", function()
|
||||
require("jj.picker").status()
|
||||
end, { desc = "JJ Picker status" })
|
||||
vim.keymap.set("n", "<leader>gl", function()
|
||||
require("jj.picker").file_history()
|
||||
end, { desc = "JJ Picker file history" })
|
||||
|
||||
|
||||
local picker = require("jj.picker")
|
||||
vim.keymap.set("n", "<leader>gj", picker.status, { desc = "JJ Picker status" })
|
||||
vim.keymap.set("n", "<leader>gl", picker.file_history, { desc = "JJ Picker file history" })
|
||||
|
||||
-- Some functions like `describe` or `log` can take parameters
|
||||
vim.keymap.set("n", "<leader>jl", function()
|
||||
cmd.log {
|
||||
vim.keymap.set("n", "<leader>jL", function()
|
||||
jj.log {
|
||||
revisions = "all()",
|
||||
}
|
||||
end, { desc = "JJ log" })
|
||||
end, { desc = "JJ log all" })
|
||||
|
||||
-- This is an alias i use for moving bookmarks its so good
|
||||
vim.keymap.set("n", "<leader>jt", function()
|
||||
local cmd = require("jj.cmd")
|
||||
cmd.j "tug"
|
||||
cmd.log {}
|
||||
end, { desc = "JJ tug" })
|
||||
|
||||
+77
-25
@@ -3,6 +3,11 @@ local M = {}
|
||||
|
||||
local utils = require("jj.utils")
|
||||
|
||||
-- Config for cmd module
|
||||
M.config = {
|
||||
describe_editor = "buffer", -- "buffer" or "input"
|
||||
}
|
||||
|
||||
local state = {
|
||||
-- The current terminal buffer for jj commands
|
||||
--- @type integer|nil
|
||||
@@ -587,11 +592,9 @@ local function execute_describe(description)
|
||||
return
|
||||
end
|
||||
|
||||
local cmd = string.format("jj describe -m '%s'", description)
|
||||
local _, success = utils.execute_command(cmd, "Failed to describe")
|
||||
if not success then
|
||||
return
|
||||
else
|
||||
-- Use --stdin to properly handle multi-line descriptions and special characters
|
||||
local _, success = utils.execute_command("jj describe --stdin", "Failed to describe", description)
|
||||
if success then
|
||||
utils.notify("Description set.", vim.log.levels.INFO)
|
||||
end
|
||||
end
|
||||
@@ -613,35 +616,84 @@ function M.describe(description, opts)
|
||||
end
|
||||
|
||||
-- Check if a description was provided otherwise require for input
|
||||
if not description then
|
||||
local merged_opts = vim.tbl_deep_extend("force", default_describe_opts, opts or {})
|
||||
if merged_opts.with_status then
|
||||
-- Show the status in a terminal buffer
|
||||
M.status()
|
||||
end
|
||||
|
||||
vim.ui.input({
|
||||
prompt = "Description: ",
|
||||
default = "",
|
||||
}, function(input)
|
||||
-- If the user inputed something, execute the describe command
|
||||
if input then
|
||||
execute_describe(input)
|
||||
if description then
|
||||
-- Description provided directly
|
||||
execute_describe(description)
|
||||
else
|
||||
-- Use buffer editor mode
|
||||
if M.config.describe_editor == "buffer" then
|
||||
-- Build initial lines
|
||||
local status_files = utils.get_status_files()
|
||||
local text = { "JJ: This commit contains the following changes:" }
|
||||
for _, item in ipairs(status_files) do
|
||||
table.insert(text, string.format("JJ: %s %s", item.status, item.file))
|
||||
end
|
||||
-- Close the current terminal when finished
|
||||
close_terminal_buffer()
|
||||
end)
|
||||
table.insert(text, "JJ:") -- blank line
|
||||
table.insert(text, 'JJ: Lines starting with "JJ:" (like this one) will be ignored when finalizing')
|
||||
table.insert(text, "") -- Empty line to separate from user input
|
||||
table.insert(text, "") -- Another empty line where user can start typing
|
||||
|
||||
utils.open_ephemeral_buffer(text, function(buf_lines)
|
||||
local user_lines = {}
|
||||
for _, line in ipairs(buf_lines) do
|
||||
if not line:match("^JJ:") then
|
||||
table.insert(user_lines, line)
|
||||
end
|
||||
end
|
||||
-- Join lines and trim leading/trailing whitespace
|
||||
local trimmed_description = table.concat(user_lines, "\n"):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
execute_describe(trimmed_description)
|
||||
local merged_opts = vim.tbl_deep_extend("force", default_describe_opts, opts or {})
|
||||
if merged_opts.with_status then
|
||||
M.status()
|
||||
end
|
||||
end)
|
||||
else
|
||||
local merged_opts = vim.tbl_deep_extend("force", default_describe_opts, opts or {})
|
||||
if merged_opts.with_status then
|
||||
-- Show the status in a terminal buffer
|
||||
M.status()
|
||||
end
|
||||
|
||||
vim.ui.input({
|
||||
prompt = "Description: ",
|
||||
default = "",
|
||||
}, function(input)
|
||||
-- If the user inputed something, execute the describe command
|
||||
if input then
|
||||
execute_describe(input)
|
||||
end
|
||||
-- Close the current terminal when finished
|
||||
close_terminal_buffer()
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Jujutsu status
|
||||
function M.status()
|
||||
--- Jujutsu status.
|
||||
--
|
||||
-- it executes `jj st` and either:
|
||||
-- 1. Shows the output in a notification (if `opts.notify` is true), or
|
||||
-- 2. Displays it in the buffer by default.
|
||||
--
|
||||
-- @param opts? table Optional settings:
|
||||
-- @field notify boolean If true, show the status in a notification instead of buffer.
|
||||
function M.status(opts)
|
||||
if not utils.ensure_jj() then
|
||||
return
|
||||
end
|
||||
|
||||
local cmd = "jj st"
|
||||
run(cmd)
|
||||
|
||||
if opts and opts.notify then
|
||||
local output = utils.execute_command(cmd, "Failed to get status")
|
||||
if output then
|
||||
utils.notify(output, vim.log.levels.INFO)
|
||||
end
|
||||
else
|
||||
-- Default behavior: show in buffer
|
||||
run(cmd)
|
||||
end
|
||||
end
|
||||
|
||||
--- @class jj.cmd.new_opts
|
||||
|
||||
+17
-1
@@ -1,6 +1,7 @@
|
||||
local M = {}
|
||||
local cmd = require("jj.cmd")
|
||||
local picker = require("jj.picker")
|
||||
local utils = require("jj.utils")
|
||||
|
||||
--- Jujutsu plugin configuration
|
||||
--- @class jj.Config
|
||||
@@ -10,6 +11,15 @@ M.config = {
|
||||
picker = {
|
||||
snacks = {},
|
||||
},
|
||||
--- @type jj.utils.highlights Highlight configuration for describe buffer
|
||||
highlights = {
|
||||
added = { fg = "#3fb950", ctermfg = "Green" },
|
||||
modified = { fg = "#56d4dd", ctermfg = "Cyan" },
|
||||
deleted = { fg = "#f85149", ctermfg = "Red" },
|
||||
renamed = { fg = "#d29922", ctermfg = "Yellow" },
|
||||
},
|
||||
--- @type string Editor mode for describe command: "buffer" (Git-style editor) or "input" (simple input prompt)
|
||||
describe_editor = "buffer",
|
||||
}
|
||||
|
||||
--- Setup the plugin
|
||||
@@ -17,7 +27,13 @@ M.config = {
|
||||
function M.setup(opts)
|
||||
M.config = vim.tbl_deep_extend("force", M.config, opts or {})
|
||||
|
||||
picker.setup(opts.picker)
|
||||
picker.setup(opts and opts.picker or {})
|
||||
utils.setup({ highlights = M.config.highlights })
|
||||
|
||||
-- Pass describe_editor config to cmd module
|
||||
if opts and opts.describe_editor then
|
||||
cmd.config.describe_editor = opts.describe_editor
|
||||
end
|
||||
|
||||
cmd.register_command()
|
||||
end
|
||||
|
||||
+232
-2
@@ -1,9 +1,56 @@
|
||||
--- @class jj.utils
|
||||
--- @field highlights jj.utils.highlights Highlight configuration
|
||||
|
||||
---@class jj.utils.highlights
|
||||
---@field added table Highlight settings for added lines
|
||||
---@field modified table Highlight settings for modified lines
|
||||
---@field deleted table Highlight settings for deleted lines
|
||||
---@field renamed table Highlight settings for renamed lines
|
||||
|
||||
local M = {
|
||||
executable_cache = {},
|
||||
dependency_cache = {},
|
||||
highlights_initialized = false,
|
||||
highlights = {
|
||||
added = { fg = "#3fb950", ctermfg = "Green" },
|
||||
modified = { fg = "#56d4dd", ctermfg = "Cyan" },
|
||||
deleted = { fg = "#f85149", ctermfg = "Red" },
|
||||
renamed = { fg = "#d29922", ctermfg = "Yellow" },
|
||||
},
|
||||
}
|
||||
|
||||
-- Initialize highlight groups once
|
||||
local function init_highlights()
|
||||
if M.highlights_initialized then
|
||||
return
|
||||
end
|
||||
|
||||
vim.api.nvim_set_hl(0, "JJComment", { link = "Comment" })
|
||||
vim.api.nvim_set_hl(0, "JJAdded", M.highlights.added)
|
||||
vim.api.nvim_set_hl(0, "JJModified", M.highlights.modified)
|
||||
vim.api.nvim_set_hl(0, "JJDeleted", M.highlights.deleted)
|
||||
vim.api.nvim_set_hl(0, "JJRenamed", M.highlights.renamed)
|
||||
|
||||
M.highlights_initialized = true
|
||||
end
|
||||
|
||||
--- Setup function to configure highlights and other options
|
||||
---@param opts? jj.utils Configuration options
|
||||
function M.setup(opts)
|
||||
opts = opts or {}
|
||||
|
||||
-- Merge user highlights with defaults
|
||||
if opts.highlights then
|
||||
M.highlights = vim.tbl_deep_extend("force", M.highlights, opts.highlights)
|
||||
end
|
||||
|
||||
-- Reset highlights flag to force re-initialization with new highlights
|
||||
if M.highlights_initialized then
|
||||
M.highlights_initialized = false
|
||||
init_highlights()
|
||||
end
|
||||
end
|
||||
|
||||
--- Cache for executable checks to avoid repeated system calls
|
||||
|
||||
--- Check if an executable exists in PATH
|
||||
@@ -54,10 +101,16 @@ end
|
||||
--- Execute a system command and return output with error handling
|
||||
--- @param cmd string The command to execute
|
||||
--- @param error_prefix string|nil Optional error message prefix
|
||||
--- @param input string|nil Optional input to pass to stdin
|
||||
--- @return string|nil output The command output, or nil if failed
|
||||
--- @return boolean success Whether the command succeeded
|
||||
function M.execute_command(cmd, error_prefix)
|
||||
local output = vim.fn.system(cmd)
|
||||
function M.execute_command(cmd, error_prefix, input)
|
||||
local output
|
||||
if input then
|
||||
output = vim.fn.system(cmd, input)
|
||||
else
|
||||
output = vim.fn.system(cmd)
|
||||
end
|
||||
local success = vim.v.shell_error == 0
|
||||
|
||||
if not success then
|
||||
@@ -99,6 +152,51 @@ function M.get_jj_root()
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Get a list of files with their status in the current jj repository.
|
||||
--- @return table[] A list of tables with {status = string, file = string}
|
||||
function M.get_status_files()
|
||||
if not M.ensure_jj() then
|
||||
return {}
|
||||
end
|
||||
|
||||
local result, success = M.execute_command("jj status", "Error getting status")
|
||||
if not success or not result then
|
||||
return {}
|
||||
end
|
||||
|
||||
local files = {}
|
||||
-- Parse jj status output: "M filename", "A filename", "D filename", "R old => new"
|
||||
for line in result:gmatch("[^\r\n]+") do
|
||||
local status, file = line:match("^([MADR])%s+(.+)$")
|
||||
if status and file then
|
||||
table.insert(files, { status = status, file = file })
|
||||
end
|
||||
end
|
||||
|
||||
return files
|
||||
end
|
||||
|
||||
--- Get a list of files modified in the current jj repository.
|
||||
--- @return string[] A list of modified file paths
|
||||
function M.get_modified_files()
|
||||
if not M.ensure_jj() then
|
||||
return {}
|
||||
end
|
||||
|
||||
local result, success = M.execute_command("jj diff --name-only", "Error getting diff")
|
||||
if not success or not result then
|
||||
return {}
|
||||
end
|
||||
|
||||
local files = {}
|
||||
-- Split the result into lines and add each file to the table
|
||||
for file in result:gmatch("[^\r\n]+") do
|
||||
table.insert(files, file)
|
||||
end
|
||||
|
||||
return files
|
||||
end
|
||||
|
||||
---- Notify function to display messages with a title
|
||||
--- @param message string The message to display
|
||||
--- @param level? number The log level (default: INFO)
|
||||
@@ -107,4 +205,136 @@ function M.notify(message, level)
|
||||
vim.notify(message, level, { title = "JJ", timeout = 3000 })
|
||||
end
|
||||
|
||||
---@param initial_text string[] Lines to initialize the buffer with
|
||||
---@param on_done fun(buf: string[])? Optional callback called with user text on buffer write
|
||||
function M.open_ephemeral_buffer(initial_text, on_done)
|
||||
-- Initialize highlight groups once
|
||||
init_highlights()
|
||||
|
||||
-- Create a horizontal split at the bottom, half the screen height
|
||||
local height = math.floor(vim.o.lines / 2)
|
||||
vim.cmd(string.format("botright %dsplit", height))
|
||||
|
||||
-- Create a new unlisted, scratch buffer
|
||||
local buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_buf_set_name(buf, "jujutsu:///DESCRIBE_EDITMSG")
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, initial_text)
|
||||
vim.api.nvim_win_set_buf(0, buf)
|
||||
|
||||
-- Configure buffer options
|
||||
vim.bo[buf].buftype = "acwrite" -- Allow custom write handling
|
||||
vim.bo[buf].bufhidden = "wipe" -- Automatically wipe buffer when hidden
|
||||
vim.bo[buf].swapfile = false -- Disable swapfile
|
||||
vim.bo[buf].modifiable = true -- Allow editing
|
||||
|
||||
-- Create a namespace for our highlights
|
||||
local ns_id = vim.api.nvim_create_namespace("jj_describe_highlights")
|
||||
|
||||
-- Function to apply highlights to the buffer
|
||||
local function apply_highlights()
|
||||
-- Clear existing highlights
|
||||
vim.api.nvim_buf_clear_namespace(buf, ns_id, 0, -1)
|
||||
|
||||
-- Get all lines
|
||||
local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
|
||||
|
||||
for i, line in ipairs(lines) do
|
||||
local line_idx = i - 1 -- 0-indexed
|
||||
|
||||
-- First, check if line starts with JJ: and highlight it as comment
|
||||
if line:match("^JJ:") then
|
||||
-- Highlight the "JJ:" prefix as comment (first 3 characters)
|
||||
vim.api.nvim_buf_set_extmark(buf, ns_id, line_idx, 0, {
|
||||
end_col = 3,
|
||||
hl_group = "JJComment",
|
||||
})
|
||||
|
||||
-- Then check for status indicators and highlight the rest of the line
|
||||
local status_pos = line:find("[MADR] ", 4) -- Find status after "JJ:"
|
||||
if status_pos then
|
||||
local status = line:sub(status_pos, status_pos) -- Get the status character
|
||||
local hl_group = nil
|
||||
|
||||
if status == "A" then
|
||||
hl_group = "JJAdded"
|
||||
elseif status == "M" then
|
||||
hl_group = "JJModified"
|
||||
elseif status == "D" then
|
||||
hl_group = "JJDeleted"
|
||||
elseif status == "R" then
|
||||
hl_group = "JJRenamed"
|
||||
end
|
||||
|
||||
if hl_group then
|
||||
-- Highlight from the status character to the end of the line
|
||||
vim.api.nvim_buf_set_extmark(buf, ns_id, line_idx, status_pos - 1, {
|
||||
end_col = #line,
|
||||
hl_group = hl_group,
|
||||
})
|
||||
else
|
||||
-- No status, keep rest as comment
|
||||
vim.api.nvim_buf_set_extmark(buf, ns_id, line_idx, 3, {
|
||||
end_col = #line,
|
||||
hl_group = "JJComment",
|
||||
})
|
||||
end
|
||||
else
|
||||
-- No status indicator, highlight rest of line as comment
|
||||
vim.api.nvim_buf_set_extmark(buf, ns_id, line_idx, 3, {
|
||||
end_col = #line,
|
||||
hl_group = "JJComment",
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Apply highlights initially
|
||||
apply_highlights()
|
||||
|
||||
-- Reapply highlights when text changes
|
||||
vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, {
|
||||
buffer = buf,
|
||||
callback = apply_highlights,
|
||||
})
|
||||
|
||||
-- Position cursor at the end (after the last JJ: line) and enter insert mode
|
||||
vim.schedule(function()
|
||||
-- Get the number of lines in the buffer
|
||||
local line_count = vim.api.nvim_buf_line_count(buf)
|
||||
-- Move cursor to the last line, column 0
|
||||
vim.api.nvim_win_set_cursor(0, { line_count, 0 })
|
||||
-- Enter insert mode
|
||||
vim.cmd("startinsert")
|
||||
end)
|
||||
|
||||
-- Handle :w and :wq commands
|
||||
vim.api.nvim_create_autocmd("BufWriteCmd", {
|
||||
buffer = buf,
|
||||
callback = function()
|
||||
local buf_lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
|
||||
if on_done then
|
||||
on_done(buf_lines)
|
||||
end
|
||||
vim.bo[buf].modified = false
|
||||
end,
|
||||
})
|
||||
|
||||
-- Add keymap to close the buffer with 'q' in normal mode
|
||||
vim.keymap.set(
|
||||
"n",
|
||||
"q",
|
||||
"<cmd>close!<CR>",
|
||||
{ buffer = buf, noremap = true, silent = true, desc = "Close describe buffer" }
|
||||
)
|
||||
|
||||
-- Add keymap to close the buffer with '<Esc>' in normal mode
|
||||
vim.keymap.set(
|
||||
"n",
|
||||
"<Esc>",
|
||||
"<cmd>close!<CR>",
|
||||
{ buffer = buf, noremap = true, silent = true, desc = "Close describe buffer" }
|
||||
)
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
Reference in New Issue
Block a user