Compare commits
3 Commits
x11_debug
...
diagnostic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3000f6ea22 | ||
|
|
377e24b798 | ||
|
|
a0c0f1ebcd |
@@ -545,7 +545,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ConversationEditor > Editor",
|
||||
"context": "ContextEditor > Editor",
|
||||
"bindings": {
|
||||
"ctrl-enter": "assistant::Assist",
|
||||
"ctrl-s": "workspace::Save",
|
||||
|
||||
@@ -228,7 +228,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ConversationEditor > Editor",
|
||||
"context": "ContextEditor > Editor",
|
||||
"bindings": {
|
||||
"cmd-enter": "assistant::Assist",
|
||||
"cmd-s": "workspace::Save",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
pub mod assistant_panel;
|
||||
pub mod assistant_settings;
|
||||
mod completion_provider;
|
||||
mod conversation_store;
|
||||
mod context_store;
|
||||
mod inline_assistant;
|
||||
mod model_selector;
|
||||
mod prompt_library;
|
||||
@@ -17,7 +17,7 @@ use assistant_slash_command::SlashCommandRegistry;
|
||||
use client::{proto, Client};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
pub(crate) use completion_provider::*;
|
||||
pub(crate) use conversation_store::*;
|
||||
pub(crate) use context_store::*;
|
||||
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
|
||||
pub(crate) use inline_assistant::*;
|
||||
pub(crate) use model_selector::*;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@ use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc, time::Duration};
|
||||
use ui::Context;
|
||||
use util::{paths::CONVERSATIONS_DIR, ResultExt, TryFutureExt};
|
||||
use util::{paths::CONTEXTS_DIR, ResultExt, TryFutureExt};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedMessage {
|
||||
@@ -18,7 +18,7 @@ pub struct SavedMessage {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct SavedConversation {
|
||||
pub struct SavedContext {
|
||||
pub id: Option<String>,
|
||||
pub zed: String,
|
||||
pub version: String,
|
||||
@@ -28,12 +28,12 @@ pub struct SavedConversation {
|
||||
pub summary: String,
|
||||
}
|
||||
|
||||
impl SavedConversation {
|
||||
impl SavedContext {
|
||||
pub const VERSION: &'static str = "0.2.0";
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct SavedConversationV0_1_0 {
|
||||
struct SavedContextV0_1_0 {
|
||||
id: Option<String>,
|
||||
zed: String,
|
||||
version: String,
|
||||
@@ -46,28 +46,26 @@ struct SavedConversationV0_1_0 {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SavedConversationMetadata {
|
||||
pub struct SavedContextMetadata {
|
||||
pub title: String,
|
||||
pub path: PathBuf,
|
||||
pub mtime: chrono::DateTime<chrono::Local>,
|
||||
}
|
||||
|
||||
pub struct ConversationStore {
|
||||
conversations_metadata: Vec<SavedConversationMetadata>,
|
||||
pub struct ContextStore {
|
||||
contexts_metadata: Vec<SavedContextMetadata>,
|
||||
fs: Arc<dyn Fs>,
|
||||
_watch_updates: Task<Option<()>>,
|
||||
}
|
||||
|
||||
impl ConversationStore {
|
||||
impl ContextStore {
|
||||
pub fn new(fs: Arc<dyn Fs>, cx: &mut AppContext) -> Task<Result<Model<Self>>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
const CONVERSATION_WATCH_DURATION: Duration = Duration::from_millis(100);
|
||||
let (mut events, _) = fs
|
||||
.watch(&CONVERSATIONS_DIR, CONVERSATION_WATCH_DURATION)
|
||||
.await;
|
||||
const CONTEXT_WATCH_DURATION: Duration = Duration::from_millis(100);
|
||||
let (mut events, _) = fs.watch(&CONTEXTS_DIR, CONTEXT_WATCH_DURATION).await;
|
||||
|
||||
let this = cx.new_model(|cx: &mut ModelContext<Self>| Self {
|
||||
conversations_metadata: Vec::new(),
|
||||
contexts_metadata: Vec::new(),
|
||||
fs,
|
||||
_watch_updates: cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
@@ -88,46 +86,41 @@ impl ConversationStore {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load(&self, path: PathBuf, cx: &AppContext) -> Task<Result<SavedConversation>> {
|
||||
pub fn load(&self, path: PathBuf, cx: &AppContext) -> Task<Result<SavedContext>> {
|
||||
let fs = self.fs.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
let saved_conversation = fs.load(&path).await?;
|
||||
let saved_conversation_json =
|
||||
serde_json::from_str::<serde_json::Value>(&saved_conversation)?;
|
||||
match saved_conversation_json
|
||||
let saved_context = fs.load(&path).await?;
|
||||
let saved_context_json = serde_json::from_str::<serde_json::Value>(&saved_context)?;
|
||||
match saved_context_json
|
||||
.get("version")
|
||||
.ok_or_else(|| anyhow!("version not found"))?
|
||||
{
|
||||
serde_json::Value::String(version) => match version.as_str() {
|
||||
SavedConversation::VERSION => Ok(serde_json::from_value::<SavedConversation>(
|
||||
saved_conversation_json,
|
||||
)?),
|
||||
SavedContext::VERSION => {
|
||||
Ok(serde_json::from_value::<SavedContext>(saved_context_json)?)
|
||||
}
|
||||
"0.1.0" => {
|
||||
let saved_conversation = serde_json::from_value::<SavedConversationV0_1_0>(
|
||||
saved_conversation_json,
|
||||
)?;
|
||||
Ok(SavedConversation {
|
||||
id: saved_conversation.id,
|
||||
zed: saved_conversation.zed,
|
||||
version: saved_conversation.version,
|
||||
text: saved_conversation.text,
|
||||
messages: saved_conversation.messages,
|
||||
message_metadata: saved_conversation.message_metadata,
|
||||
summary: saved_conversation.summary,
|
||||
let saved_context =
|
||||
serde_json::from_value::<SavedContextV0_1_0>(saved_context_json)?;
|
||||
Ok(SavedContext {
|
||||
id: saved_context.id,
|
||||
zed: saved_context.zed,
|
||||
version: saved_context.version,
|
||||
text: saved_context.text,
|
||||
messages: saved_context.messages,
|
||||
message_metadata: saved_context.message_metadata,
|
||||
summary: saved_context.summary,
|
||||
})
|
||||
}
|
||||
_ => Err(anyhow!(
|
||||
"unrecognized saved conversation version: {}",
|
||||
version
|
||||
)),
|
||||
_ => Err(anyhow!("unrecognized saved context version: {}", version)),
|
||||
},
|
||||
_ => Err(anyhow!("version not found on saved conversation")),
|
||||
_ => Err(anyhow!("version not found on saved context")),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn search(&self, query: String, cx: &AppContext) -> Task<Vec<SavedConversationMetadata>> {
|
||||
let metadata = self.conversations_metadata.clone();
|
||||
pub fn search(&self, query: String, cx: &AppContext) -> Task<Vec<SavedContextMetadata>> {
|
||||
let metadata = self.contexts_metadata.clone();
|
||||
let executor = cx.background_executor().clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
if query.is_empty() {
|
||||
@@ -159,10 +152,10 @@ impl ConversationStore {
|
||||
fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
|
||||
let fs = self.fs.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
fs.create_dir(&CONVERSATIONS_DIR).await?;
|
||||
fs.create_dir(&CONTEXTS_DIR).await?;
|
||||
|
||||
let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
|
||||
let mut conversations = Vec::<SavedConversationMetadata>::new();
|
||||
let mut paths = fs.read_dir(&CONTEXTS_DIR).await?;
|
||||
let mut contexts = Vec::<SavedContextMetadata>::new();
|
||||
while let Some(path) = paths.next().await {
|
||||
let path = path?;
|
||||
if path.extension() != Some(OsStr::new("json")) {
|
||||
@@ -178,13 +171,13 @@ impl ConversationStore {
|
||||
.and_then(|name| name.to_str())
|
||||
.zip(metadata)
|
||||
{
|
||||
// This is used to filter out conversations saved by the new assistant.
|
||||
// This is used to filter out contexts saved by the new assistant.
|
||||
if !re.is_match(file_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(title) = re.replace(file_name, "").lines().next() {
|
||||
conversations.push(SavedConversationMetadata {
|
||||
contexts.push(SavedContextMetadata {
|
||||
title: title.to_string(),
|
||||
path,
|
||||
mtime: metadata.mtime.into(),
|
||||
@@ -192,10 +185,10 @@ impl ConversationStore {
|
||||
}
|
||||
}
|
||||
}
|
||||
conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
|
||||
contexts.sort_unstable_by_key(|context| Reverse(context.mtime));
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.conversations_metadata = conversations;
|
||||
this.contexts_metadata = contexts;
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
@@ -66,7 +66,7 @@ impl InlineAssistant {
|
||||
&mut self,
|
||||
editor: &View<Editor>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
include_conversation: bool,
|
||||
include_context: bool,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let selection = editor.read(cx).selections.newest_anchor().clone();
|
||||
@@ -144,7 +144,7 @@ impl InlineAssistant {
|
||||
self.pending_assists.insert(
|
||||
inline_assist_id,
|
||||
PendingInlineAssist {
|
||||
include_conversation,
|
||||
include_context,
|
||||
editor: editor.downgrade(),
|
||||
inline_assist_editor: Some((block_id, inline_assist_editor.clone())),
|
||||
codegen: codegen.clone(),
|
||||
@@ -375,11 +375,11 @@ impl InlineAssistant {
|
||||
return;
|
||||
};
|
||||
|
||||
let conversation = if pending_assist.include_conversation {
|
||||
let context = if pending_assist.include_context {
|
||||
pending_assist.workspace.as_ref().and_then(|workspace| {
|
||||
let workspace = workspace.upgrade()?.read(cx);
|
||||
let assistant_panel = workspace.panel::<AssistantPanel>(cx)?;
|
||||
assistant_panel.read(cx).active_conversation(cx)
|
||||
assistant_panel.read(cx).active_context(cx)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@@ -461,8 +461,8 @@ impl InlineAssistant {
|
||||
});
|
||||
|
||||
let mut messages = Vec::new();
|
||||
if let Some(conversation) = conversation {
|
||||
let request = conversation.read(cx).to_completion_request(cx);
|
||||
if let Some(context) = context {
|
||||
let request = context.read(cx).to_completion_request(cx);
|
||||
messages = request.messages;
|
||||
}
|
||||
let model = CompletionProvider::global(cx).model();
|
||||
@@ -818,7 +818,7 @@ struct PendingInlineAssist {
|
||||
codegen: Model<Codegen>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
include_conversation: bool,
|
||||
include_context: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::assistant_panel::ConversationEditor;
|
||||
use crate::assistant_panel::ContextEditor;
|
||||
use anyhow::Result;
|
||||
pub use assistant_slash_command::{SlashCommand, SlashCommandOutput, SlashCommandRegistry};
|
||||
use editor::{CompletionProvider, Editor};
|
||||
@@ -29,7 +29,7 @@ pub mod tabs_command;
|
||||
pub(crate) struct SlashCommandCompletionProvider {
|
||||
commands: Arc<SlashCommandRegistry>,
|
||||
cancel_flag: Mutex<Arc<AtomicBool>>,
|
||||
editor: Option<WeakView<ConversationEditor>>,
|
||||
editor: Option<WeakView<ContextEditor>>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ pub(crate) struct SlashCommandLine {
|
||||
impl SlashCommandCompletionProvider {
|
||||
pub fn new(
|
||||
commands: Arc<SlashCommandRegistry>,
|
||||
editor: Option<WeakView<ConversationEditor>>,
|
||||
editor: Option<WeakView<ContextEditor>>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
|
||||
@@ -69,7 +69,6 @@ struct TestPlan<T: RandomizedTest> {
|
||||
pub struct UserTestPlan {
|
||||
pub user_id: UserId,
|
||||
pub username: String,
|
||||
pub allow_client_reconnection: bool,
|
||||
pub allow_client_disconnection: bool,
|
||||
next_root_id: usize,
|
||||
operation_ix: usize,
|
||||
@@ -237,7 +236,6 @@ impl<T: RandomizedTest> TestPlan<T> {
|
||||
next_root_id: 0,
|
||||
operation_ix: 0,
|
||||
allow_client_disconnection,
|
||||
allow_client_reconnection,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -129,10 +129,10 @@ where
|
||||
impl Clamp for RGBAColor {
|
||||
fn clamp(self) -> Self {
|
||||
RGBAColor {
|
||||
r: self.r.min(1.0).max(0.0),
|
||||
g: self.g.min(1.0).max(0.0),
|
||||
b: self.b.min(1.0).max(0.0),
|
||||
a: self.a.min(1.0).max(0.0),
|
||||
r: self.r.clamp(0., 1.),
|
||||
g: self.g.clamp(0., 1.),
|
||||
b: self.b.clamp(0., 1.),
|
||||
a: self.a.clamp(0., 1.),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,7 +322,6 @@ pub fn update_inlay_link_and_hover_points(
|
||||
hover_popover::hover_at_inlay(
|
||||
editor,
|
||||
InlayHover {
|
||||
excerpt: excerpt_id,
|
||||
tooltip: match tooltip {
|
||||
InlayHintTooltip::String(text) => HoverBlock {
|
||||
text,
|
||||
@@ -370,7 +369,6 @@ pub fn update_inlay_link_and_hover_points(
|
||||
hover_popover::hover_at_inlay(
|
||||
editor,
|
||||
InlayHover {
|
||||
excerpt: excerpt_id,
|
||||
tooltip: match tooltip {
|
||||
InlayHintLabelPartTooltip::String(text) => {
|
||||
HoverBlock {
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{
|
||||
hover_links::{InlayHighlight, RangeInEditor},
|
||||
scroll::ScrollAmount,
|
||||
Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot,
|
||||
EditorStyle, ExcerptId, Hover, RangeToAnchorExt,
|
||||
EditorStyle, Hover, RangeToAnchorExt,
|
||||
};
|
||||
use futures::{stream::FuturesUnordered, FutureExt};
|
||||
use gpui::{
|
||||
@@ -49,7 +49,6 @@ pub fn hover_at(editor: &mut Editor, anchor: Option<Anchor>, cx: &mut ViewContex
|
||||
}
|
||||
|
||||
pub struct InlayHover {
|
||||
pub excerpt: ExcerptId,
|
||||
pub range: InlayHighlight,
|
||||
pub tooltip: HoverBlock,
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ mod sys {
|
||||
|
||||
#[link(name = "CoreFoundation", kind = "framework")]
|
||||
#[link(name = "CoreVideo", kind = "framework")]
|
||||
#[allow(improper_ctypes)]
|
||||
#[allow(improper_ctypes, unknown_lints, clippy::duplicated_attributes)]
|
||||
extern "C" {
|
||||
pub fn CVDisplayLinkCreateWithActiveCGDisplays(
|
||||
display_link_out: *mut *mut CVDisplayLink,
|
||||
|
||||
@@ -63,7 +63,9 @@ impl TaffyLayoutEngine {
|
||||
let parent_id = self
|
||||
.taffy
|
||||
// This is safe because LayoutId is repr(transparent) to taffy::tree::NodeId.
|
||||
.new_with_children(taffy_style, unsafe { std::mem::transmute(children) })
|
||||
.new_with_children(taffy_style, unsafe {
|
||||
std::mem::transmute::<&[LayoutId], &[taffy::NodeId]>(children)
|
||||
})
|
||||
.expect(EXPECT_MESSAGE)
|
||||
.into();
|
||||
self.children_to_parents
|
||||
|
||||
@@ -48,7 +48,6 @@ pub struct SyntaxMapMatches<'a> {
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SyntaxMapCapture<'a> {
|
||||
pub depth: usize,
|
||||
pub node: Node<'a>,
|
||||
pub index: u32,
|
||||
pub grammar_index: usize,
|
||||
@@ -886,7 +885,9 @@ impl<'a> SyntaxMapCaptures<'a> {
|
||||
|
||||
// TODO - add a Tree-sitter API to remove the need for this.
|
||||
let cursor = unsafe {
|
||||
std::mem::transmute::<_, &'static mut QueryCursor>(query_cursor.deref_mut())
|
||||
std::mem::transmute::<&mut tree_sitter::QueryCursor, &'static mut QueryCursor>(
|
||||
query_cursor.deref_mut(),
|
||||
)
|
||||
};
|
||||
|
||||
cursor.set_byte_range(range.clone());
|
||||
@@ -933,7 +934,6 @@ impl<'a> SyntaxMapCaptures<'a> {
|
||||
let layer = self.layers[..self.active_layer_count].first()?;
|
||||
let capture = layer.next_capture?;
|
||||
Some(SyntaxMapCapture {
|
||||
depth: layer.depth,
|
||||
grammar_index: layer.grammar_index,
|
||||
index: capture.index,
|
||||
node: capture.node,
|
||||
@@ -1004,7 +1004,9 @@ impl<'a> SyntaxMapMatches<'a> {
|
||||
|
||||
// TODO - add a Tree-sitter API to remove the need for this.
|
||||
let cursor = unsafe {
|
||||
std::mem::transmute::<_, &'static mut QueryCursor>(query_cursor.deref_mut())
|
||||
std::mem::transmute::<&mut tree_sitter::QueryCursor, &'static mut QueryCursor>(
|
||||
query_cursor.deref_mut(),
|
||||
)
|
||||
};
|
||||
|
||||
cursor.set_byte_range(range.clone());
|
||||
|
||||
@@ -29,14 +29,14 @@ pub(super) fn json_task_context() -> ContextProviderWithTasks {
|
||||
ContextProviderWithTasks::new(TaskTemplates(vec![
|
||||
TaskTemplate {
|
||||
label: "package script $ZED_CUSTOM_script".to_owned(),
|
||||
command: "npm run".to_owned(),
|
||||
command: "npm --prefix $ZED_DIRNAME run".to_owned(),
|
||||
args: vec![VariableName::Custom("script".into()).template_value()],
|
||||
tags: vec!["package-script".into()],
|
||||
..TaskTemplate::default()
|
||||
},
|
||||
TaskTemplate {
|
||||
label: "composer script $ZED_CUSTOM_script".to_owned(),
|
||||
command: "composer".to_owned(),
|
||||
command: "composer -d $ZED_DIRNAME".to_owned(),
|
||||
args: vec![VariableName::Custom("script".into()).template_value()],
|
||||
tags: vec!["composer-script".into()],
|
||||
..TaskTemplate::default()
|
||||
|
||||
@@ -10282,7 +10282,7 @@ impl Project {
|
||||
fn deserialize_symbol(serialized_symbol: proto::Symbol) -> Result<CoreSymbol> {
|
||||
let source_worktree_id = WorktreeId::from_proto(serialized_symbol.source_worktree_id);
|
||||
let worktree_id = WorktreeId::from_proto(serialized_symbol.worktree_id);
|
||||
let kind = unsafe { mem::transmute(serialized_symbol.kind) };
|
||||
let kind = unsafe { mem::transmute::<i32, lsp::SymbolKind>(serialized_symbol.kind) };
|
||||
let path = ProjectPath {
|
||||
worktree_id,
|
||||
path: PathBuf::from(serialized_symbol.path).into(),
|
||||
@@ -11393,7 +11393,7 @@ fn serialize_symbol(symbol: &Symbol) -> proto::Symbol {
|
||||
worktree_id: symbol.path.worktree_id.to_proto(),
|
||||
path: symbol.path.path.to_string_lossy().to_string(),
|
||||
name: symbol.name.clone(),
|
||||
kind: unsafe { mem::transmute(symbol.kind) },
|
||||
kind: unsafe { mem::transmute::<lsp::SymbolKind, i32>(symbol.kind) },
|
||||
start: Some(proto::PointUtf16 {
|
||||
row: symbol.range.start.0.row,
|
||||
column: symbol.range.start.0.column,
|
||||
|
||||
@@ -38,6 +38,7 @@ pub struct GitSettings {
|
||||
|
||||
impl GitSettings {
|
||||
pub fn inline_blame_enabled(&self) -> bool {
|
||||
#[allow(unknown_lints, clippy::manual_unwrap_or_default)]
|
||||
match self.inline_blame {
|
||||
Some(InlineBlameSettings { enabled, .. }) => enabled,
|
||||
_ => false,
|
||||
|
||||
@@ -1295,7 +1295,7 @@ async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppC
|
||||
project
|
||||
.language_servers_running_disk_based_diagnostics()
|
||||
.collect::<Vec<_>>(),
|
||||
[LanguageServerId(0); 0]
|
||||
[] as [language::LanguageServerId; 0]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ lazy_static::lazy_static! {
|
||||
} else {
|
||||
HOME.join(".config").join("zed")
|
||||
};
|
||||
pub static ref CONVERSATIONS_DIR: PathBuf = if cfg!(target_os = "macos") {
|
||||
pub static ref CONTEXTS_DIR: PathBuf = if cfg!(target_os = "macos") {
|
||||
CONFIG_DIR.join("conversations")
|
||||
} else {
|
||||
SUPPORT_DIR.join("conversations")
|
||||
|
||||
@@ -3969,8 +3969,7 @@ impl Workspace {
|
||||
fn adjust_padding(padding: Option<f32>) -> f32 {
|
||||
padding
|
||||
.unwrap_or(Self::DEFAULT_PADDING)
|
||||
.min(Self::MAX_PADDING)
|
||||
.max(0.0)
|
||||
.clamp(0.0, Self::MAX_PADDING)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user