✨ Add formatting method
This commit is contained in:
@@ -5,7 +5,8 @@
|
||||
import { EditorSelection } from "@codemirror/state";
|
||||
import { Command } from "@codemirror/view";
|
||||
import { blockState, getActiveNoteBlock, getFirstNoteBlock, getLastNoteBlock, getNoteBlockFromPos } from "./state";
|
||||
import { Block, EditorOptions } from "./types";
|
||||
import { Block, EditorOptions, DELIMITER_REGEX } from "./types";
|
||||
import { formatBlockContent } from "./formatCode";
|
||||
|
||||
/**
|
||||
* 获取块分隔符
|
||||
@@ -143,17 +144,24 @@ export const addNewBlockAfterLast = (options: EditorOptions): Command => ({ stat
|
||||
export function changeLanguageTo(state: any, dispatch: any, block: Block, language: string, auto: boolean) {
|
||||
if (state.readOnly) return false;
|
||||
|
||||
const delimRegex = /^\n∞∞∞[a-z]+?(-a)?\n/g;
|
||||
if (state.doc.sliceString(block.delimiter.from, block.delimiter.to).match(delimRegex)) {
|
||||
dispatch(state.update({
|
||||
const currentDelimiter = state.doc.sliceString(block.delimiter.from, block.delimiter.to);
|
||||
|
||||
// 重置正则表达式的 lastIndex
|
||||
DELIMITER_REGEX.lastIndex = 0;
|
||||
if (currentDelimiter.match(DELIMITER_REGEX)) {
|
||||
const newDelimiter = `\n∞∞∞${language}${auto ? '-a' : ''}\n`;
|
||||
|
||||
dispatch({
|
||||
changes: {
|
||||
from: block.delimiter.from,
|
||||
to: block.delimiter.to,
|
||||
insert: `\n∞∞∞${language}${auto ? '-a' : ''}\n`,
|
||||
insert: newDelimiter,
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
return true;
|
||||
} else {
|
||||
throw new Error("Invalid delimiter: " + state.doc.sliceString(block.delimiter.from, block.delimiter.to));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,13 +170,17 @@ export function changeLanguageTo(state: any, dispatch: any, block: Block, langua
|
||||
*/
|
||||
export function changeCurrentBlockLanguage(state: any, dispatch: any, language: string | null, auto: boolean) {
|
||||
const block = getActiveNoteBlock(state);
|
||||
if (!block) return;
|
||||
if (!block) {
|
||||
console.warn("No active block found");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果 language 为 null,我们只想更改自动检测标志
|
||||
if (language === null) {
|
||||
language = block.language.name;
|
||||
}
|
||||
changeLanguageTo(state, dispatch, block, language, auto);
|
||||
|
||||
return changeLanguageTo(state, dispatch, block, language, auto);
|
||||
}
|
||||
|
||||
// 选择和移动辅助函数
|
||||
@@ -352,4 +364,11 @@ function moveCurrentBlock(state: any, dispatch: any, up: boolean) {
|
||||
}));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化当前块
|
||||
*/
|
||||
export const formatCurrentBlock: Command = (view) => {
|
||||
return formatBlockContent(view);
|
||||
}
|
96
frontend/src/views/editor/extensions/codeblock/formatCode.ts
Normal file
96
frontend/src/views/editor/extensions/codeblock/formatCode.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { EditorSelection } from "@codemirror/state"
|
||||
|
||||
import * as prettier from "prettier/standalone"
|
||||
import { getActiveNoteBlock } from "./state"
|
||||
import { getLanguage } from "./lang-parser/languages"
|
||||
import { SupportedLanguage } from "./types"
|
||||
|
||||
export const formatBlockContent = (view) => {
|
||||
const state = view.state
|
||||
if (state.readOnly)
|
||||
return false
|
||||
const block = getActiveNoteBlock(state)
|
||||
|
||||
if (!block) {
|
||||
return false
|
||||
}
|
||||
|
||||
const language = getLanguage(block.language.name as SupportedLanguage)
|
||||
if (!language || !language.prettier) {
|
||||
return false
|
||||
}
|
||||
|
||||
// get current cursor position
|
||||
const cursorPos = state.selection.asSingle().ranges[0].head
|
||||
// get block content
|
||||
const content = state.sliceDoc(block.content.from, block.content.to)
|
||||
|
||||
let useFormat = false
|
||||
if (cursorPos == block.content.from || cursorPos == block.content.to) {
|
||||
useFormat = true
|
||||
}
|
||||
|
||||
// 执行异步格式化,但在回调中获取最新状态
|
||||
const performFormat = async () => {
|
||||
let formattedContent
|
||||
try {
|
||||
if (useFormat) {
|
||||
formattedContent = {
|
||||
formatted: await prettier.format(content, {
|
||||
parser: language.prettier!.parser,
|
||||
plugins: language.prettier!.plugins,
|
||||
tabWidth: state.tabSize,
|
||||
}),
|
||||
}
|
||||
formattedContent.cursorOffset = cursorPos == block.content.from ? 0 : formattedContent.formatted.length
|
||||
} else {
|
||||
// formatWithCursor 有性能问题,改用简单格式化 + 光标位置计算
|
||||
const formatted = await prettier.format(content, {
|
||||
parser: language.prettier!.parser,
|
||||
plugins: language.prettier!.plugins,
|
||||
tabWidth: state.tabSize,
|
||||
})
|
||||
formattedContent = {
|
||||
formatted: formatted,
|
||||
cursorOffset: Math.min(cursorPos - block.content.from, formatted.length)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
const hyphens = "----------------------------------------------------------------------------"
|
||||
const errorMessage = (e as Error).message;
|
||||
console.log(`Error when trying to format block:\n${hyphens}\n${errorMessage}\n${hyphens}`)
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
// 重新获取当前状态和块信息,确保状态一致
|
||||
const currentState = view.state
|
||||
const currentBlock = getActiveNoteBlock(currentState)
|
||||
|
||||
if (!currentBlock) {
|
||||
console.warn('Block not found after formatting')
|
||||
return false
|
||||
}
|
||||
|
||||
view.dispatch(currentState.update({
|
||||
changes: {
|
||||
from: currentBlock.content.from,
|
||||
to: currentBlock.content.to,
|
||||
insert: formattedContent.formatted,
|
||||
},
|
||||
selection: EditorSelection.cursor(currentBlock.content.from + Math.min(formattedContent.cursorOffset, formattedContent.formatted.length)),
|
||||
}, {
|
||||
userEvent: "input",
|
||||
scrollIntoView: true,
|
||||
}))
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to apply formatting changes:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 执行异步格式化
|
||||
performFormat()
|
||||
return true // 立即返回 true,表示命令已开始执行
|
||||
}
|
@@ -214,6 +214,13 @@ export function createCodeBlockExtension(options: CodeBlockOptions = {}): Extens
|
||||
run: transposeChars,
|
||||
preventDefault: true
|
||||
},
|
||||
|
||||
// 代码格式化命令
|
||||
{
|
||||
key: 'Mod-Shift-f', // 格式化代码
|
||||
run: commands.formatCurrentBlock,
|
||||
preventDefault: true
|
||||
},
|
||||
])
|
||||
];
|
||||
|
||||
@@ -249,6 +256,9 @@ export {
|
||||
// 命令
|
||||
export * from './commands';
|
||||
|
||||
// 格式化功能
|
||||
export { formatBlockContent } from './formatCode';
|
||||
|
||||
// 选择功能
|
||||
export {
|
||||
selectAll,
|
||||
|
@@ -13,9 +13,10 @@ BlockDelimiter {
|
||||
}
|
||||
|
||||
BlockLanguage {
|
||||
"text" | "math" | "json" | "python" | "html" | "sql" | "markdown" |
|
||||
"java" | "php" | "css" | "xml" | "cpp" | "rust" | "ruby" | "shell" |
|
||||
"yaml" | "go" | "javascript" | "typescript"
|
||||
"text" | "json" | "py" | "html" | "sql" | "md" | "java" | "php" |
|
||||
"css" | "xml" | "cpp" | "rs" | "cs" | "rb" | "sh" | "yaml" | "toml" |
|
||||
"go" | "clj" | "ex" | "erl" | "js" | "ts" | "swift" | "kt" | "groovy" |
|
||||
"ps1" | "dart" | "scala"
|
||||
}
|
||||
|
||||
@tokens {
|
||||
|
@@ -29,9 +29,17 @@ import { groovy } from "@codemirror/legacy-modes/mode/groovy";
|
||||
import { powerShell } from "@codemirror/legacy-modes/mode/powershell";
|
||||
import { scala } from "@codemirror/legacy-modes/mode/clike";
|
||||
import { toml } from "@codemirror/legacy-modes/mode/toml";
|
||||
|
||||
import { elixir } from "codemirror-lang-elixir";
|
||||
import { SupportedLanguage } from '../types';
|
||||
|
||||
import typescriptPlugin from "prettier/plugins/typescript"
|
||||
import babelPrettierPlugin from "prettier/plugins/babel"
|
||||
import htmlPrettierPlugin from "prettier/plugins/html"
|
||||
import cssPrettierPlugin from "prettier/plugins/postcss"
|
||||
import markdownPrettierPlugin from "prettier/plugins/markdown"
|
||||
import yamlPrettierPlugin from "prettier/plugins/yaml"
|
||||
import * as prettierPluginEstree from "prettier/plugins/estree";
|
||||
|
||||
/**
|
||||
* 语言信息类
|
||||
*/
|
||||
@@ -39,7 +47,11 @@ export class LanguageInfo {
|
||||
constructor(
|
||||
public token: SupportedLanguage,
|
||||
public name: string,
|
||||
public parser: any
|
||||
public parser: any,
|
||||
public prettier?: {
|
||||
parser: string;
|
||||
plugins: any[];
|
||||
}
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -48,28 +60,49 @@ export class LanguageInfo {
|
||||
*/
|
||||
export const LANGUAGES: LanguageInfo[] = [
|
||||
new LanguageInfo("text", "Plain Text", null),
|
||||
new LanguageInfo("json", "JSON", jsonLanguage.parser),
|
||||
new LanguageInfo("json", "JSON", jsonLanguage.parser, {
|
||||
parser: "json",
|
||||
plugins: [babelPrettierPlugin, prettierPluginEstree]
|
||||
}),
|
||||
new LanguageInfo("py", "Python", pythonLanguage.parser),
|
||||
new LanguageInfo("html", "HTML", htmlLanguage.parser),
|
||||
new LanguageInfo("html", "HTML", htmlLanguage.parser, {
|
||||
parser: "html",
|
||||
plugins: [htmlPrettierPlugin]
|
||||
}),
|
||||
new LanguageInfo("sql", "SQL", StandardSQL.language.parser),
|
||||
new LanguageInfo("md", "Markdown", markdownLanguage.parser),
|
||||
new LanguageInfo("md", "Markdown", markdownLanguage.parser, {
|
||||
parser: "markdown",
|
||||
plugins: [markdownPrettierPlugin]
|
||||
}),
|
||||
new LanguageInfo("java", "Java", javaLanguage.parser),
|
||||
new LanguageInfo("php", "PHP", phpLanguage.configure({top:"Program"}).parser),
|
||||
new LanguageInfo("css", "CSS", cssLanguage.parser),
|
||||
new LanguageInfo("css", "CSS", cssLanguage.parser, {
|
||||
parser: "css",
|
||||
plugins: [cssPrettierPlugin]
|
||||
}),
|
||||
new LanguageInfo("xml", "XML", xmlLanguage.parser),
|
||||
new LanguageInfo("cpp", "C++", cppLanguage.parser),
|
||||
new LanguageInfo("rs", "Rust", rustLanguage.parser),
|
||||
new LanguageInfo("cs", "C#", StreamLanguage.define(csharp).parser),
|
||||
new LanguageInfo("rb", "Ruby", StreamLanguage.define(ruby).parser),
|
||||
new LanguageInfo("sh", "Shell", StreamLanguage.define(shell).parser),
|
||||
new LanguageInfo("yaml", "YAML", yamlLanguage.parser),
|
||||
new LanguageInfo("yaml", "YAML", yamlLanguage.parser, {
|
||||
parser: "yaml",
|
||||
plugins: [yamlPrettierPlugin]
|
||||
}),
|
||||
new LanguageInfo("toml", "TOML", StreamLanguage.define(toml).parser),
|
||||
new LanguageInfo("go", "Go", StreamLanguage.define(go).parser),
|
||||
new LanguageInfo("clj", "Clojure", StreamLanguage.define(clojure).parser),
|
||||
new LanguageInfo("ex", "Elixir", null), // 暂无解析器
|
||||
new LanguageInfo("ex", "Elixir", elixir().language.parser),
|
||||
new LanguageInfo("erl", "Erlang", StreamLanguage.define(erlang).parser),
|
||||
new LanguageInfo("js", "JavaScript", javascriptLanguage.parser),
|
||||
new LanguageInfo("ts", "TypeScript", typescriptLanguage.parser),
|
||||
new LanguageInfo("js", "JavaScript", javascriptLanguage.parser, {
|
||||
parser: "babel",
|
||||
plugins: [babelPrettierPlugin, prettierPluginEstree]
|
||||
}),
|
||||
new LanguageInfo("ts", "TypeScript", typescriptLanguage.parser, {
|
||||
parser: "typescript",
|
||||
plugins: [typescriptPlugin, prettierPluginEstree]
|
||||
}),
|
||||
new LanguageInfo("swift", "Swift", StreamLanguage.define(swift).parser),
|
||||
new LanguageInfo("kt", "Kotlin", StreamLanguage.define(kotlin).parser),
|
||||
new LanguageInfo("groovy", "Groovy", StreamLanguage.define(groovy).parser),
|
||||
|
@@ -3,14 +3,14 @@ import {LRParser} from "@lezer/lr"
|
||||
import {blockContent} from "./external-tokens.js"
|
||||
export const parser = LRParser.deserialize({
|
||||
version: 14,
|
||||
states: "!jQQOQOOOVOQO'#C`O!dOPO'#C_OOOO'#Cc'#CcQQOQOOOOOO'#Ca'#CaO!iOSO,58zOOOO,58y,58yOOOO-E6a-E6aOOOP1G.f1G.fO!qOSO1G.fOOOP7+$Q7+$Q",
|
||||
stateData: "!v~OXPO~OYTOZTO[TO]TO^TO_TO`TOaTObTOcTOdTOeTOfTOgTOhTOiTOjTOkTOlTO~OPVO~OUYOmXO~OmZO~O",
|
||||
states: "!jQQOQOOOVOQO'#C`O#SOPO'#C_OOOO'#Cc'#CcQQOQOOOOOO'#Ca'#CaO#XOSO,58zOOOO,58y,58yOOOO-E6a-E6aOOOP1G.f1G.fO#aOSO1G.fOOOP7+$Q7+$Q",
|
||||
stateData: "#f~OXPO~OYTOZTO[TO]TO^TO_TO`TOaTObTOcTOdTOeTOfTOgTOhTOiTOjTOkTOlTOmTOnTOoTOpTOqTOrTOsTOtTOuTOvTO~OPVO~OUYOwXO~OwZO~O",
|
||||
goto: "jWPPPX]aPdTROSTQOSRUPQSORWS",
|
||||
nodeNames: "⚠ BlockContent Document Block BlockDelimiter BlockLanguage Auto",
|
||||
maxTerm: 29,
|
||||
maxTerm: 39,
|
||||
skippedNodes: [0],
|
||||
repeatNodeCount: 1,
|
||||
tokenData: ",k~R]YZz}!O!e#V#W!p#Z#[#a#[#]#l#^#_$T#a#b%x#d#e'X#f#g([#g#h)R#h#i*O#l#m+q#m#n,SR!PPmQ%&x%&y!SP!VP%&x%&y!YP!]P%&x%&y!`P!eOXP~!hP#T#U!k~!pOU~~!sQ#d#e!y#g#h#U~!|P#d#e#P~#UOe~~#XP#g#h#[~#aOc~~#dP#c#d#g~#lOj~~#oP#h#i#r~#uP#a#b#x~#{P#`#a$O~$TO^~~$WQ#T#U$^#g#h%g~$aP#j#k$d~$gP#T#U$j~$oPa~#g#h$r~$uP#V#W$x~${P#f#g%O~%RP#]#^%U~%XP#d#e%[~%_P#h#i%b~%gOk~~%jP#c#d%m~%pP#b#c%s~%xO[~~%{P#T#U&O~&RQ#f#g&X#h#i&|~&[P#_#`&_~&bP#W#X&e~&hP#c#d&k~&nP#k#l&q~&tP#b#c&w~&|O`~~'PP#[#]'S~'XOZ~~'[Q#[#]'b#m#n'm~'eP#d#e'h~'mOb~~'pP#h#i's~'vP#[#]'y~'|P#c#d(P~(SP#b#c(V~([O]~~(_P#i#j(b~(eQ#U#V(k#g#h(v~(nP#m#n(q~(vOg~~(yP#h#i(|~)ROf~~)UQ#[#])[#e#f)s~)_P#X#Y)b~)eP#`#a)h~)kP#`#a)n~)sOh~~)vP#`#a)y~*OO_~~*RQ#X#Y*X#m#n*j~*[P#l#m*_~*bP#h#i*e~*jOY~~*mP#d#e*p~*sP#X#Y*v~*yP#g#h*|~+PP#V#W+S~+VP#f#g+Y~+]P#]#^+`~+cP#d#e+f~+iP#h#i+l~+qOl~~+tP#a#b+w~+zP#`#a+}~,SOd~~,VP#T#U,Y~,]P#a#b,`~,cP#`#a,f~,kOi~",
|
||||
tokenData: ",s~R`YZ!T}!O!n#V#W!y#W#X#z#X#Y$c#Z#[$|#[#]%y#^#_&b#_#`'a#a#b'l#d#e'w#f#g(p#g#h)T#h#i*t#l#m+y#m#n,[R!YPwQ%&x%&y!]P!`P%&x%&y!cP!fP%&x%&y!iP!nOXP~!qP#T#U!t~!yOU~~!|R#`#a#V#d#e#b#g#h#m~#YP#^#_#]~#bOl~~#eP#d#e#h~#mOd~~#rPf~#g#h#u~#zOb~~#}P#T#U$Q~$TP#f#g$W~$ZP#h#i$^~$cOu~~$fQ#f#g$l#l#m$w~$oP#`#a$r~$wOn~~$|Om~~%PQ#c#d%V#f#g%[~%[Ok~~%_P#c#d%b~%eP#c#d%h~%kP#j#k%n~%qP#m#n%t~%yOs~~%|P#h#i&P~&SP#a#b&V~&YP#`#a&]~&bO]~~&eQ#T#U&k#g#h&|~&nP#j#k&q~&tP#T#U&w~&|O`~~'RPo~#c#d'U~'XP#b#c'[~'aOZ~~'dP#h#i'g~'lOr~~'oP#W#X'r~'wO_~~'zR#[#](T#g#h(`#m#n(k~(WP#d#e(Z~(`Oa~~(cP!R!S(f~(kOt~~(pO[~~(sQ#U#V(y#g#h)O~)OOg~~)TOe~~)WS#V#W)d#[#]){#e#f*Q#k#l*]~)gP#T#U)j~)mP#`#a)p~)sP#T#U)v~){Ov~~*QOh~~*TP#`#a*W~*]O^~~*`P#]#^*c~*fP#Y#Z*i~*lP#h#i*o~*tOq~~*wR#X#Y+Q#c#d+c#g#h+t~+TP#l#m+W~+ZP#h#i+^~+cOY~~+fP#a#b+i~+lP#`#a+o~+tOj~~+yOp~~+|P#a#b,P~,SP#`#a,V~,[Oc~~,_P#T#U,b~,eP#a#b,h~,kP#`#a,n~,sOi~",
|
||||
tokenizers: [blockContent, 0, 1],
|
||||
topRules: {"Document":[0,2]},
|
||||
tokenPrec: 0
|
||||
|
@@ -4,10 +4,12 @@
|
||||
|
||||
import { EditorState, Annotation } from '@codemirror/state';
|
||||
import { EditorView, ViewPlugin } from '@codemirror/view';
|
||||
import { blockState } from '../state';
|
||||
import { redoDepth } from '@codemirror/commands';
|
||||
import { blockState, getActiveNoteBlock } from '../state';
|
||||
import { levenshteinDistance } from './levenshtein';
|
||||
import { LANGUAGES } from '../lang-parser/languages';
|
||||
import { SupportedLanguage, Block } from '../types';
|
||||
import { changeLanguageTo } from '../commands';
|
||||
|
||||
// ===== 类型定义 =====
|
||||
|
||||
@@ -100,27 +102,6 @@ function cancelIdleCallbackCompat(id: number): void {
|
||||
*/
|
||||
const languageChangeAnnotation = Annotation.define<boolean>();
|
||||
|
||||
/**
|
||||
* 更新代码块语言
|
||||
*/
|
||||
function updateBlockLanguage(
|
||||
state: EditorState,
|
||||
dispatch: (transaction: any) => void,
|
||||
block: Block,
|
||||
newLanguage: SupportedLanguage
|
||||
): void {
|
||||
const newDelimiter = `\n∞∞∞${newLanguage}-a\n`;
|
||||
const transaction = state.update({
|
||||
changes: {
|
||||
from: block.delimiter.from,
|
||||
to: block.delimiter.to,
|
||||
insert: newDelimiter,
|
||||
},
|
||||
annotations: [languageChangeAnnotation.of(true)]
|
||||
});
|
||||
dispatch(transaction);
|
||||
}
|
||||
|
||||
// ===== Web Worker 管理器 =====
|
||||
|
||||
/**
|
||||
@@ -237,22 +218,18 @@ export function createLanguageDetection(config: LanguageDetectionConfig = {}): V
|
||||
}
|
||||
|
||||
private performDetection(state: EditorState): void {
|
||||
const selection = state.selection.asSingle().ranges[0];
|
||||
const blocks = state.field(blockState);
|
||||
|
||||
const block = blocks.find(b =>
|
||||
b.content.from <= selection.from && b.content.to >= selection.from
|
||||
);
|
||||
const block = getActiveNoteBlock(state);
|
||||
|
||||
if (!block || !block.language.auto) return;
|
||||
|
||||
const blocks = state.field(blockState);
|
||||
const blockIndex = blocks.indexOf(block);
|
||||
const content = state.doc.sliceString(block.content.from, block.content.to);
|
||||
|
||||
// 内容为空时重置为默认语言
|
||||
if (content === "") {
|
||||
if (content === "" && redoDepth(state) === 0) {
|
||||
if (block.language.name !== finalConfig.defaultLanguage) {
|
||||
updateBlockLanguage(state, this.view.dispatch, block, finalConfig.defaultLanguage);
|
||||
changeLanguageTo(state, this.view.dispatch, block, finalConfig.defaultLanguage, true);
|
||||
}
|
||||
contentCache.delete(blockIndex);
|
||||
return;
|
||||
@@ -281,7 +258,10 @@ export function createLanguageDetection(config: LanguageDetectionConfig = {}): V
|
||||
SUPPORTED_LANGUAGES.has(result.language) &&
|
||||
LANGUAGE_MAP.has(result.language)) {
|
||||
|
||||
updateBlockLanguage(state, this.view.dispatch, block, result.language);
|
||||
// 只有在用户没有撤销操作时才更改语言
|
||||
if (redoDepth(state) === 0) {
|
||||
changeLanguageTo(state, this.view.dispatch, block, result.language, true);
|
||||
}
|
||||
}
|
||||
|
||||
contentCache.set(blockIndex, content);
|
||||
|
@@ -24,6 +24,7 @@ export interface Block {
|
||||
* 支持的语言类型
|
||||
*/
|
||||
export type SupportedLanguage =
|
||||
| 'auto' // 自动检测
|
||||
| 'text'
|
||||
| 'json'
|
||||
| 'py' // Python
|
||||
@@ -58,6 +59,7 @@ export type SupportedLanguage =
|
||||
* 支持的语言列表
|
||||
*/
|
||||
export const SUPPORTED_LANGUAGES: SupportedLanguage[] = [
|
||||
'auto',
|
||||
'text',
|
||||
'json',
|
||||
'py',
|
||||
|
Reference in New Issue
Block a user