mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-08 07:35:01 +03:00
lsp: Support Goto Declaration (#15785)
Adds support for [Goto Declaration](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_declaration) LSP command. I am particularly interested in [this for Rust projects](https://rust-analyzer.github.io/manual.html#go-to-declaration), to be able to navigate to the place where a trait method is declared, coming from a trait method implementation. I noticed this was something I could do in VSCode before, but was somehow missing is Zed. Thanks to the already existing infrastructure for Goto Definition, I just followed and copy-paste-adapted it for Goto Declaration. As a bonus, I added `ctrl-F12` and `alt-ctrl-F12` as default macOS keybindings for `GoToDeclaration` and `GoToDeclarationSplit`, respectively. They are not keybindings from another editor, but I figured they made sense to be grouped along with the other *F12 commands. ### Release Notes: - Added "Go to declaration" editor action. - vim: Breaking change to keybindings after introduction of the `Go to declaration` editor action. The new keybindings are the following (and can be found [here](https://zed.dev/docs/vim), alongside the other key bindings): - `g d` - Go to definition - `g D` - Go to declaration - `g y` - Go to type definition - `g I` - Go to implementation https://github.com/user-attachments/assets/ee5c10a8-94f0-4e50-afbb-6f71db540c1b --------- Co-authored-by: Thorsten Ball <mrnugget@gmail.com>
This commit is contained in:
parent
82db5dedfb
commit
7b5fdcee7f
@ -127,7 +127,9 @@
|
|||||||
"cmd-'": "editor::ToggleHunkDiff",
|
"cmd-'": "editor::ToggleHunkDiff",
|
||||||
"cmd-\"": "editor::ExpandAllHunkDiffs",
|
"cmd-\"": "editor::ExpandAllHunkDiffs",
|
||||||
"cmd-alt-g b": "editor::ToggleGitBlame",
|
"cmd-alt-g b": "editor::ToggleGitBlame",
|
||||||
"cmd-i": "editor::ShowSignatureHelp"
|
"cmd-i": "editor::ShowSignatureHelp",
|
||||||
|
"ctrl-f12": "editor::GoToDeclaration",
|
||||||
|
"alt-ctrl-f12": "editor::GoToDeclarationSplit"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -89,8 +89,9 @@
|
|||||||
"g t": "pane::ActivateNextItem",
|
"g t": "pane::ActivateNextItem",
|
||||||
"g shift-t": "pane::ActivatePrevItem",
|
"g shift-t": "pane::ActivatePrevItem",
|
||||||
"g d": "editor::GoToDefinition",
|
"g d": "editor::GoToDefinition",
|
||||||
"g shift-d": "editor::GoToTypeDefinition",
|
"g shift-d": "editor::GoToDeclaration",
|
||||||
"g cmd-d": "editor::GoToImplementation",
|
"g y": "editor::GoToTypeDefinition",
|
||||||
|
"g shift-i": "editor::GoToImplementation",
|
||||||
"g x": "editor::OpenUrl",
|
"g x": "editor::OpenUrl",
|
||||||
"g n": "vim::SelectNextMatch",
|
"g n": "vim::SelectNextMatch",
|
||||||
"g shift-n": "vim::SelectPreviousMatch",
|
"g shift-n": "vim::SelectPreviousMatch",
|
||||||
|
@ -210,6 +210,8 @@ gpui::actions!(
|
|||||||
Format,
|
Format,
|
||||||
GoToDefinition,
|
GoToDefinition,
|
||||||
GoToDefinitionSplit,
|
GoToDefinitionSplit,
|
||||||
|
GoToDeclaration,
|
||||||
|
GoToDeclarationSplit,
|
||||||
GoToDiagnostic,
|
GoToDiagnostic,
|
||||||
GoToHunk,
|
GoToHunk,
|
||||||
GoToImplementation,
|
GoToImplementation,
|
||||||
|
@ -1539,6 +1539,7 @@ pub(crate) struct NavigationData {
|
|||||||
|
|
||||||
enum GotoDefinitionKind {
|
enum GotoDefinitionKind {
|
||||||
Symbol,
|
Symbol,
|
||||||
|
Declaration,
|
||||||
Type,
|
Type,
|
||||||
Implementation,
|
Implementation,
|
||||||
}
|
}
|
||||||
@ -8948,6 +8949,22 @@ impl Editor {
|
|||||||
self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, false, cx)
|
self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, false, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn go_to_declaration(
|
||||||
|
&mut self,
|
||||||
|
_: &GoToDeclaration,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Task<Result<bool>> {
|
||||||
|
self.go_to_definition_of_kind(GotoDefinitionKind::Declaration, false, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn go_to_declaration_split(
|
||||||
|
&mut self,
|
||||||
|
_: &GoToDeclaration,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Task<Result<bool>> {
|
||||||
|
self.go_to_definition_of_kind(GotoDefinitionKind::Declaration, true, cx)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn go_to_implementation(
|
pub fn go_to_implementation(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &GoToImplementation,
|
_: &GoToImplementation,
|
||||||
@ -9008,6 +9025,7 @@ impl Editor {
|
|||||||
let project = workspace.read(cx).project().clone();
|
let project = workspace.read(cx).project().clone();
|
||||||
let definitions = project.update(cx, |project, cx| match kind {
|
let definitions = project.update(cx, |project, cx| match kind {
|
||||||
GotoDefinitionKind::Symbol => project.definition(&buffer, head, cx),
|
GotoDefinitionKind::Symbol => project.definition(&buffer, head, cx),
|
||||||
|
GotoDefinitionKind::Declaration => project.declaration(&buffer, head, cx),
|
||||||
GotoDefinitionKind::Type => project.type_definition(&buffer, head, cx),
|
GotoDefinitionKind::Type => project.type_definition(&buffer, head, cx),
|
||||||
GotoDefinitionKind::Implementation => project.implementation(&buffer, head, cx),
|
GotoDefinitionKind::Implementation => project.implementation(&buffer, head, cx),
|
||||||
});
|
});
|
||||||
|
@ -300,6 +300,12 @@ impl EditorElement {
|
|||||||
register_action(view, cx, |editor, a, cx| {
|
register_action(view, cx, |editor, a, cx| {
|
||||||
editor.go_to_definition_split(a, cx).detach_and_log_err(cx);
|
editor.go_to_definition_split(a, cx).detach_and_log_err(cx);
|
||||||
});
|
});
|
||||||
|
register_action(view, cx, |editor, a, cx| {
|
||||||
|
editor.go_to_declaration(a, cx).detach_and_log_err(cx);
|
||||||
|
});
|
||||||
|
register_action(view, cx, |editor, a, cx| {
|
||||||
|
editor.go_to_declaration_split(a, cx).detach_and_log_err(cx);
|
||||||
|
});
|
||||||
register_action(view, cx, |editor, a, cx| {
|
register_action(view, cx, |editor, a, cx| {
|
||||||
editor.go_to_implementation(a, cx).detach_and_log_err(cx);
|
editor.go_to_implementation(a, cx).detach_and_log_err(cx);
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
||||||
|
use crate::GoToDeclaration;
|
||||||
use crate::{
|
use crate::{
|
||||||
selections_collection::SelectionsCollection, Copy, CopyPermalinkToLine, Cut, DisplayPoint,
|
selections_collection::SelectionsCollection, Copy, CopyPermalinkToLine, Cut, DisplayPoint,
|
||||||
DisplaySnapshot, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToImplementation,
|
DisplaySnapshot, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToImplementation,
|
||||||
@ -163,6 +164,7 @@ pub fn deploy_context_menu(
|
|||||||
.on_blur_subscription(Subscription::new(|| {}))
|
.on_blur_subscription(Subscription::new(|| {}))
|
||||||
.action("Rename Symbol", Box::new(Rename))
|
.action("Rename Symbol", Box::new(Rename))
|
||||||
.action("Go to Definition", Box::new(GoToDefinition))
|
.action("Go to Definition", Box::new(GoToDefinition))
|
||||||
|
.action("Go to Declaration", Box::new(GoToDeclaration))
|
||||||
.action("Go to Type Definition", Box::new(GoToTypeDefinition))
|
.action("Go to Type Definition", Box::new(GoToTypeDefinition))
|
||||||
.action("Go to Implementation", Box::new(GoToImplementation))
|
.action("Go to Implementation", Box::new(GoToImplementation))
|
||||||
.action("Find All References", Box::new(FindAllReferences))
|
.action("Find All References", Box::new(FindAllReferences))
|
||||||
|
@ -117,6 +117,10 @@ pub struct GetDefinition {
|
|||||||
pub position: PointUtf16,
|
pub position: PointUtf16,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) struct GetDeclaration {
|
||||||
|
pub position: PointUtf16,
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) struct GetTypeDefinition {
|
pub(crate) struct GetTypeDefinition {
|
||||||
pub position: PointUtf16,
|
pub position: PointUtf16,
|
||||||
}
|
}
|
||||||
@ -521,6 +525,106 @@ impl LspCommand for GetDefinition {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait(?Send)]
|
||||||
|
impl LspCommand for GetDeclaration {
|
||||||
|
type Response = Vec<LocationLink>;
|
||||||
|
type LspRequest = lsp::request::GotoDeclaration;
|
||||||
|
type ProtoRequest = proto::GetDeclaration;
|
||||||
|
|
||||||
|
fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
|
||||||
|
capabilities
|
||||||
|
.server_capabilities
|
||||||
|
.declaration_provider
|
||||||
|
.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_lsp(
|
||||||
|
&self,
|
||||||
|
path: &Path,
|
||||||
|
_: &Buffer,
|
||||||
|
_: &Arc<LanguageServer>,
|
||||||
|
_: &AppContext,
|
||||||
|
) -> lsp::GotoDeclarationParams {
|
||||||
|
lsp::GotoDeclarationParams {
|
||||||
|
text_document_position_params: lsp::TextDocumentPositionParams {
|
||||||
|
text_document: lsp::TextDocumentIdentifier {
|
||||||
|
uri: lsp::Url::from_file_path(path).unwrap(),
|
||||||
|
},
|
||||||
|
position: point_to_lsp(self.position),
|
||||||
|
},
|
||||||
|
work_done_progress_params: Default::default(),
|
||||||
|
partial_result_params: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn response_from_lsp(
|
||||||
|
self,
|
||||||
|
message: Option<lsp::GotoDeclarationResponse>,
|
||||||
|
project: Model<Project>,
|
||||||
|
buffer: Model<Buffer>,
|
||||||
|
server_id: LanguageServerId,
|
||||||
|
cx: AsyncAppContext,
|
||||||
|
) -> Result<Vec<LocationLink>> {
|
||||||
|
location_links_from_lsp(message, project, buffer, server_id, cx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDeclaration {
|
||||||
|
proto::GetDeclaration {
|
||||||
|
project_id,
|
||||||
|
buffer_id: buffer.remote_id().into(),
|
||||||
|
position: Some(language::proto::serialize_anchor(
|
||||||
|
&buffer.anchor_before(self.position),
|
||||||
|
)),
|
||||||
|
version: serialize_version(&buffer.version()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn from_proto(
|
||||||
|
message: proto::GetDeclaration,
|
||||||
|
_: Model<Project>,
|
||||||
|
buffer: Model<Buffer>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Result<Self> {
|
||||||
|
let position = message
|
||||||
|
.position
|
||||||
|
.and_then(deserialize_anchor)
|
||||||
|
.ok_or_else(|| anyhow!("invalid position"))?;
|
||||||
|
buffer
|
||||||
|
.update(&mut cx, |buffer, _| {
|
||||||
|
buffer.wait_for_version(deserialize_version(&message.version))
|
||||||
|
})?
|
||||||
|
.await?;
|
||||||
|
Ok(Self {
|
||||||
|
position: buffer.update(&mut cx, |buffer, _| position.to_point_utf16(buffer))?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn response_to_proto(
|
||||||
|
response: Vec<LocationLink>,
|
||||||
|
project: &mut Project,
|
||||||
|
peer_id: PeerId,
|
||||||
|
_: &clock::Global,
|
||||||
|
cx: &mut AppContext,
|
||||||
|
) -> proto::GetDeclarationResponse {
|
||||||
|
let links = location_links_to_proto(response, project, peer_id, cx);
|
||||||
|
proto::GetDeclarationResponse { links }
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn response_from_proto(
|
||||||
|
self,
|
||||||
|
message: proto::GetDeclarationResponse,
|
||||||
|
project: Model<Project>,
|
||||||
|
_: Model<Buffer>,
|
||||||
|
cx: AsyncAppContext,
|
||||||
|
) -> Result<Vec<LocationLink>> {
|
||||||
|
location_links_from_proto(message.links, project, cx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buffer_id_from_proto(message: &proto::GetDeclaration) -> Result<BufferId> {
|
||||||
|
BufferId::new(message.buffer_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait(?Send)]
|
#[async_trait(?Send)]
|
||||||
impl LspCommand for GetImplementation {
|
impl LspCommand for GetImplementation {
|
||||||
type Response = Vec<LocationLink>;
|
type Response = Vec<LocationLink>;
|
||||||
|
@ -714,6 +714,7 @@ impl Project {
|
|||||||
client.add_model_request_handler(Self::handle_lsp_command::<GetCompletions>);
|
client.add_model_request_handler(Self::handle_lsp_command::<GetCompletions>);
|
||||||
client.add_model_request_handler(Self::handle_lsp_command::<GetHover>);
|
client.add_model_request_handler(Self::handle_lsp_command::<GetHover>);
|
||||||
client.add_model_request_handler(Self::handle_lsp_command::<GetDefinition>);
|
client.add_model_request_handler(Self::handle_lsp_command::<GetDefinition>);
|
||||||
|
client.add_model_request_handler(Self::handle_lsp_command::<GetDeclaration>);
|
||||||
client.add_model_request_handler(Self::handle_lsp_command::<GetTypeDefinition>);
|
client.add_model_request_handler(Self::handle_lsp_command::<GetTypeDefinition>);
|
||||||
client.add_model_request_handler(Self::handle_lsp_command::<GetDocumentHighlights>);
|
client.add_model_request_handler(Self::handle_lsp_command::<GetDocumentHighlights>);
|
||||||
client.add_model_request_handler(Self::handle_lsp_command::<GetReferences>);
|
client.add_model_request_handler(Self::handle_lsp_command::<GetReferences>);
|
||||||
@ -5463,6 +5464,30 @@ impl Project {
|
|||||||
self.definition_impl(buffer, position, cx)
|
self.definition_impl(buffer, position, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn declaration_impl(
|
||||||
|
&self,
|
||||||
|
buffer: &Model<Buffer>,
|
||||||
|
position: PointUtf16,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<Vec<LocationLink>>> {
|
||||||
|
self.request_lsp(
|
||||||
|
buffer.clone(),
|
||||||
|
LanguageServerToQuery::Primary,
|
||||||
|
GetDeclaration { position },
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn declaration<T: ToPointUtf16>(
|
||||||
|
&self,
|
||||||
|
buffer: &Model<Buffer>,
|
||||||
|
position: T,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<Vec<LocationLink>>> {
|
||||||
|
let position = position.to_point_utf16(buffer.read(cx));
|
||||||
|
self.declaration_impl(buffer, position, cx)
|
||||||
|
}
|
||||||
|
|
||||||
fn type_definition_impl(
|
fn type_definition_impl(
|
||||||
&self,
|
&self,
|
||||||
buffer: &Model<Buffer>,
|
buffer: &Model<Buffer>,
|
||||||
|
@ -48,6 +48,8 @@ message Envelope {
|
|||||||
|
|
||||||
GetDefinition get_definition = 32;
|
GetDefinition get_definition = 32;
|
||||||
GetDefinitionResponse get_definition_response = 33;
|
GetDefinitionResponse get_definition_response = 33;
|
||||||
|
GetDeclaration get_declaration = 237;
|
||||||
|
GetDeclarationResponse get_declaration_response = 238; // current max
|
||||||
GetTypeDefinition get_type_definition = 34;
|
GetTypeDefinition get_type_definition = 34;
|
||||||
GetTypeDefinitionResponse get_type_definition_response = 35;
|
GetTypeDefinitionResponse get_type_definition_response = 35;
|
||||||
|
|
||||||
@ -696,12 +698,23 @@ message GetDefinition {
|
|||||||
uint64 buffer_id = 2;
|
uint64 buffer_id = 2;
|
||||||
Anchor position = 3;
|
Anchor position = 3;
|
||||||
repeated VectorClockEntry version = 4;
|
repeated VectorClockEntry version = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetDefinitionResponse {
|
message GetDefinitionResponse {
|
||||||
repeated LocationLink links = 1;
|
repeated LocationLink links = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message GetDeclaration {
|
||||||
|
uint64 project_id = 1;
|
||||||
|
uint64 buffer_id = 2;
|
||||||
|
Anchor position = 3;
|
||||||
|
repeated VectorClockEntry version = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message GetDeclarationResponse {
|
||||||
|
repeated LocationLink links = 1;
|
||||||
|
}
|
||||||
|
|
||||||
message GetTypeDefinition {
|
message GetTypeDefinition {
|
||||||
uint64 project_id = 1;
|
uint64 project_id = 1;
|
||||||
uint64 buffer_id = 2;
|
uint64 buffer_id = 2;
|
||||||
|
@ -239,6 +239,8 @@ messages!(
|
|||||||
(GetCompletionsResponse, Background),
|
(GetCompletionsResponse, Background),
|
||||||
(GetDefinition, Background),
|
(GetDefinition, Background),
|
||||||
(GetDefinitionResponse, Background),
|
(GetDefinitionResponse, Background),
|
||||||
|
(GetDeclaration, Background),
|
||||||
|
(GetDeclarationResponse, Background),
|
||||||
(GetDocumentHighlights, Background),
|
(GetDocumentHighlights, Background),
|
||||||
(GetDocumentHighlightsResponse, Background),
|
(GetDocumentHighlightsResponse, Background),
|
||||||
(GetHover, Background),
|
(GetHover, Background),
|
||||||
@ -437,6 +439,7 @@ request_messages!(
|
|||||||
(GetCodeActions, GetCodeActionsResponse),
|
(GetCodeActions, GetCodeActionsResponse),
|
||||||
(GetCompletions, GetCompletionsResponse),
|
(GetCompletions, GetCompletionsResponse),
|
||||||
(GetDefinition, GetDefinitionResponse),
|
(GetDefinition, GetDefinitionResponse),
|
||||||
|
(GetDeclaration, GetDeclarationResponse),
|
||||||
(GetImplementation, GetImplementationResponse),
|
(GetImplementation, GetImplementationResponse),
|
||||||
(GetDocumentHighlights, GetDocumentHighlightsResponse),
|
(GetDocumentHighlights, GetDocumentHighlightsResponse),
|
||||||
(GetHover, GetHoverResponse),
|
(GetHover, GetHoverResponse),
|
||||||
@ -551,6 +554,7 @@ entity_messages!(
|
|||||||
GetCodeActions,
|
GetCodeActions,
|
||||||
GetCompletions,
|
GetCompletions,
|
||||||
GetDefinition,
|
GetDefinition,
|
||||||
|
GetDeclaration,
|
||||||
GetImplementation,
|
GetImplementation,
|
||||||
GetDocumentHighlights,
|
GetDocumentHighlights,
|
||||||
GetHover,
|
GetHover,
|
||||||
|
@ -145,6 +145,7 @@ pub fn app_menus() -> Vec<Menu> {
|
|||||||
MenuItem::action("Go to Line/Column...", editor::actions::ToggleGoToLine),
|
MenuItem::action("Go to Line/Column...", editor::actions::ToggleGoToLine),
|
||||||
MenuItem::separator(),
|
MenuItem::separator(),
|
||||||
MenuItem::action("Go to Definition", editor::actions::GoToDefinition),
|
MenuItem::action("Go to Definition", editor::actions::GoToDefinition),
|
||||||
|
MenuItem::action("Go to Declaration", editor::actions::GoToDeclaration),
|
||||||
MenuItem::action("Go to Type Definition", editor::actions::GoToTypeDefinition),
|
MenuItem::action("Go to Type Definition", editor::actions::GoToTypeDefinition),
|
||||||
MenuItem::action("Find All References", editor::actions::FindAllReferences),
|
MenuItem::action("Find All References", editor::actions::FindAllReferences),
|
||||||
MenuItem::separator(),
|
MenuItem::separator(),
|
||||||
|
@ -218,6 +218,8 @@ See the [tasks documentation](/docs/tasks#custom-keybindings-for-tasks) for more
|
|||||||
| Format | Editor | `⌘ + Shift + I` |
|
| Format | Editor | `⌘ + Shift + I` |
|
||||||
| Go to definition | Editor | `F12` |
|
| Go to definition | Editor | `F12` |
|
||||||
| Go to definition split | Editor | `Alt + F12` |
|
| Go to definition split | Editor | `Alt + F12` |
|
||||||
|
| Go to declaration | Editor | `Ctrl + F12` |
|
||||||
|
| Go to declaration split | Editor | `Alt + Ctrl + F12` |
|
||||||
| Go to diagnostic | Editor | `F8` |
|
| Go to diagnostic | Editor | `F8` |
|
||||||
| Go to implementation | Editor | `Shift + F12` |
|
| Go to implementation | Editor | `Shift + F12` |
|
||||||
| Go to prev diagnostic | Editor | `Shift + F8` |
|
| Go to prev diagnostic | Editor | `Shift + F8` |
|
||||||
|
@ -17,8 +17,10 @@ Vim mode has several "core Zed" key bindings, that will help you make the most o
|
|||||||
```
|
```
|
||||||
# Language server
|
# Language server
|
||||||
g d Go to definition
|
g d Go to definition
|
||||||
g D Go to type definition
|
g D Go to declaration
|
||||||
g cmd-d Go to implementation
|
g y Go to type definition
|
||||||
|
g I Go to implementation
|
||||||
|
|
||||||
c d Rename (change definition)
|
c d Rename (change definition)
|
||||||
g A Go to All references to the current word
|
g A Go to All references to the current word
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user