Compare commits
7 commits
main
...
dynamic-ru
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e0a20690fb | ||
![]() |
e9613e552a | ||
![]() |
91b011a2c6 | ||
![]() |
24677b11ed | ||
![]() |
c70cb5a911 | ||
![]() |
b884658e49 | ||
![]() |
f0f49db969 |
17 changed files with 388 additions and 112 deletions
5
Cargo.lock
generated
5
Cargo.lock
generated
|
@ -9101,6 +9101,7 @@ dependencies = [
|
||||||
"collections",
|
"collections",
|
||||||
"futures 0.3.28",
|
"futures 0.3.28",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"project_core",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json_lenient",
|
"serde_json_lenient",
|
||||||
|
@ -9112,11 +9113,15 @@ name = "tasks_ui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"editor",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"language",
|
||||||
|
"lsp",
|
||||||
"menu",
|
"menu",
|
||||||
"picker",
|
"picker",
|
||||||
"project",
|
"project",
|
||||||
|
"project_core",
|
||||||
"serde",
|
"serde",
|
||||||
"task",
|
"task",
|
||||||
"ui",
|
"ui",
|
||||||
|
|
|
@ -108,13 +108,32 @@ pub trait ToLspPosition {
|
||||||
/// A name of a language server.
|
/// A name of a language server.
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
pub struct LanguageServerName(pub Arc<str>);
|
pub struct LanguageServerName(pub Arc<str>);
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct Location {
|
pub struct Location {
|
||||||
pub buffer: Model<Buffer>,
|
pub buffer: Model<Buffer>,
|
||||||
pub range: Range<Anchor>,
|
pub range: Range<Anchor>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum LanguageTaskKind {
|
||||||
|
RunFile,
|
||||||
|
TestAtLine,
|
||||||
|
TestFile,
|
||||||
|
TestPackage,
|
||||||
|
TestWorkspace,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct LanguageTask {}
|
||||||
|
pub trait LanguageTaskBuilder: Sync + Send + 'static {
|
||||||
|
fn task_for(
|
||||||
|
&self,
|
||||||
|
kind: LanguageTaskKind,
|
||||||
|
buffer: Model<Buffer>,
|
||||||
|
path: &Path,
|
||||||
|
cx: &mut AppContext,
|
||||||
|
) -> Option<Task<LanguageTask>>;
|
||||||
|
}
|
||||||
|
|
||||||
/// Represents a Language Server, with certain cached sync properties.
|
/// Represents a Language Server, with certain cached sync properties.
|
||||||
/// Uses [`LspAdapter`] under the hood, but calls all 'static' methods
|
/// Uses [`LspAdapter`] under the hood, but calls all 'static' methods
|
||||||
/// once at startup, and caches the results.
|
/// once at startup, and caches the results.
|
||||||
|
@ -656,6 +675,7 @@ pub struct Language {
|
||||||
pub(crate) grammar: Option<Arc<Grammar>>,
|
pub(crate) grammar: Option<Arc<Grammar>>,
|
||||||
pub(crate) adapters: Vec<Arc<CachedLspAdapter>>,
|
pub(crate) adapters: Vec<Arc<CachedLspAdapter>>,
|
||||||
|
|
||||||
|
pub(crate) task_builder: Option<Arc<dyn LanguageTaskBuilder>>,
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
fake_adapter: Option<(
|
fake_adapter: Option<(
|
||||||
futures::channel::mpsc::UnboundedSender<lsp::FakeLanguageServer>,
|
futures::channel::mpsc::UnboundedSender<lsp::FakeLanguageServer>,
|
||||||
|
@ -779,6 +799,7 @@ impl Language {
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
fake_adapter: None,
|
fake_adapter: None,
|
||||||
|
task_builder: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -122,210 +122,236 @@ pub fn init(
|
||||||
("dart", tree_sitter_dart::language()),
|
("dart", tree_sitter_dart::language()),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let language = |asset_dir_name: &'static str, adapters| {
|
// let language = |asset_dir_name: &'static str, adapters| {
|
||||||
let config = load_config(asset_dir_name);
|
// let config = load_config(asset_dir_name);
|
||||||
languages.register_language(
|
// languages.register_language(
|
||||||
config.name.clone(),
|
// config.name.clone(),
|
||||||
config.grammar.clone(),
|
// config.grammar.clone(),
|
||||||
config.matcher.clone(),
|
// config.matcher.clone(),
|
||||||
adapters,
|
// adapters,
|
||||||
move || Ok((config.clone(), load_queries(asset_dir_name))),
|
// move || Ok((config.clone(), load_queries(asset_dir_name))),
|
||||||
)
|
// )
|
||||||
};
|
// };
|
||||||
|
|
||||||
language(
|
macro_rules! language {
|
||||||
|
($name:literal) => {
|
||||||
|
let config = load_config($name);
|
||||||
|
languages.register_language(
|
||||||
|
config.name.clone(),
|
||||||
|
config.grammar.clone(),
|
||||||
|
config.matcher.clone(),
|
||||||
|
vec![],
|
||||||
|
move || Ok((config.clone(), load_queries($name))),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
($name:literal, $adapters:expr) => {
|
||||||
|
let config = load_config($name);
|
||||||
|
languages.register_language(
|
||||||
|
config.name.clone(),
|
||||||
|
config.grammar.clone(),
|
||||||
|
config.matcher.clone(),
|
||||||
|
$adapters,
|
||||||
|
move || Ok((config.clone(), load_queries($name))),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
language!(
|
||||||
"astro",
|
"astro",
|
||||||
vec![
|
vec![
|
||||||
Arc::new(astro::AstroLspAdapter::new(node_runtime.clone())),
|
Arc::new(astro::AstroLspAdapter::new(node_runtime.clone())),
|
||||||
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
||||||
],
|
]
|
||||||
);
|
);
|
||||||
language("bash", vec![]);
|
language!("bash");
|
||||||
language("c", vec![Arc::new(c::CLspAdapter) as Arc<dyn LspAdapter>]);
|
language!("c", vec![Arc::new(c::CLspAdapter) as Arc<dyn LspAdapter>]);
|
||||||
language("clojure", vec![Arc::new(clojure::ClojureLspAdapter)]);
|
language!("clojure", vec![Arc::new(clojure::ClojureLspAdapter)]);
|
||||||
language("cpp", vec![Arc::new(c::CLspAdapter)]);
|
language!("cpp", vec![Arc::new(c::CLspAdapter)]);
|
||||||
language("csharp", vec![Arc::new(csharp::OmniSharpAdapter {})]);
|
language!("csharp", vec![Arc::new(csharp::OmniSharpAdapter {})]);
|
||||||
language(
|
language!(
|
||||||
"css",
|
"css",
|
||||||
vec![
|
vec![
|
||||||
Arc::new(css::CssLspAdapter::new(node_runtime.clone())),
|
Arc::new(css::CssLspAdapter::new(node_runtime.clone())),
|
||||||
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
||||||
],
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
language(
|
language!(
|
||||||
"dockerfile",
|
"dockerfile",
|
||||||
vec![Arc::new(dockerfile::DockerfileLspAdapter::new(
|
vec![Arc::new(dockerfile::DockerfileLspAdapter::new(
|
||||||
node_runtime.clone(),
|
node_runtime.clone(),
|
||||||
))],
|
))]
|
||||||
);
|
);
|
||||||
|
|
||||||
match &ElixirSettings::get(None, cx).lsp {
|
match &ElixirSettings::get(None, cx).lsp {
|
||||||
elixir::ElixirLspSetting::ElixirLs => language(
|
elixir::ElixirLspSetting::ElixirLs => {
|
||||||
"elixir",
|
language!(
|
||||||
vec![
|
"elixir",
|
||||||
Arc::new(elixir::ElixirLspAdapter),
|
vec![
|
||||||
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
Arc::new(elixir::ElixirLspAdapter),
|
||||||
],
|
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
||||||
),
|
]
|
||||||
elixir::ElixirLspSetting::NextLs => {
|
);
|
||||||
language("elixir", vec![Arc::new(elixir::NextLspAdapter)])
|
}
|
||||||
|
elixir::ElixirLspSetting::NextLs => {
|
||||||
|
language!("elixir", vec![Arc::new(elixir::NextLspAdapter)]);
|
||||||
|
}
|
||||||
|
elixir::ElixirLspSetting::Local { path, arguments } => {
|
||||||
|
language!(
|
||||||
|
"elixir",
|
||||||
|
vec![Arc::new(elixir::LocalLspAdapter {
|
||||||
|
path: path.clone(),
|
||||||
|
arguments: arguments.clone(),
|
||||||
|
})]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
elixir::ElixirLspSetting::Local { path, arguments } => language(
|
|
||||||
"elixir",
|
|
||||||
vec![Arc::new(elixir::LocalLspAdapter {
|
|
||||||
path: path.clone(),
|
|
||||||
arguments: arguments.clone(),
|
|
||||||
})],
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
language("gitcommit", vec![]);
|
language!("gitcommit");
|
||||||
language("erlang", vec![Arc::new(erlang::ErlangLspAdapter)]);
|
language!("erlang", vec![Arc::new(erlang::ErlangLspAdapter)]);
|
||||||
|
|
||||||
language("gleam", vec![Arc::new(gleam::GleamLspAdapter)]);
|
language!("gleam", vec![Arc::new(gleam::GleamLspAdapter)]);
|
||||||
language("go", vec![Arc::new(go::GoLspAdapter)]);
|
language!("go", vec![Arc::new(go::GoLspAdapter)]);
|
||||||
language("gomod", vec![]);
|
language!("gomod");
|
||||||
language("gowork", vec![]);
|
language!("gowork");
|
||||||
language("zig", vec![Arc::new(zig::ZlsAdapter)]);
|
language!("zig", vec![Arc::new(zig::ZlsAdapter)]);
|
||||||
language(
|
language!(
|
||||||
"heex",
|
"heex",
|
||||||
vec![
|
vec![
|
||||||
Arc::new(elixir::ElixirLspAdapter),
|
Arc::new(elixir::ElixirLspAdapter),
|
||||||
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
||||||
],
|
]
|
||||||
);
|
);
|
||||||
language(
|
language!(
|
||||||
"json",
|
"json",
|
||||||
vec![Arc::new(json::JsonLspAdapter::new(
|
vec![Arc::new(json::JsonLspAdapter::new(
|
||||||
node_runtime.clone(),
|
node_runtime.clone(),
|
||||||
languages.clone(),
|
languages.clone(),
|
||||||
))],
|
))]
|
||||||
);
|
);
|
||||||
language("markdown", vec![]);
|
language!("markdown");
|
||||||
language(
|
language!(
|
||||||
"python",
|
"python",
|
||||||
vec![Arc::new(python::PythonLspAdapter::new(
|
vec![Arc::new(python::PythonLspAdapter::new(
|
||||||
node_runtime.clone(),
|
node_runtime.clone(),
|
||||||
))],
|
))]
|
||||||
);
|
);
|
||||||
language("rust", vec![Arc::new(rust::RustLspAdapter)]);
|
language!("rust", vec![Arc::new(rust::RustLspAdapter)]);
|
||||||
language("toml", vec![Arc::new(toml::TaploLspAdapter)]);
|
language!("toml", vec![Arc::new(toml::TaploLspAdapter)]);
|
||||||
match &DenoSettings::get(None, cx).enable {
|
match &DenoSettings::get(None, cx).enable {
|
||||||
true => {
|
true => {
|
||||||
language(
|
language!(
|
||||||
"tsx",
|
"tsx",
|
||||||
vec![
|
vec![
|
||||||
Arc::new(deno::DenoLspAdapter::new()),
|
Arc::new(deno::DenoLspAdapter::new()),
|
||||||
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
||||||
],
|
]
|
||||||
);
|
);
|
||||||
language("typescript", vec![Arc::new(deno::DenoLspAdapter::new())]);
|
language!("typescript", vec![Arc::new(deno::DenoLspAdapter::new())]);
|
||||||
language(
|
language!(
|
||||||
"javascript",
|
"javascript",
|
||||||
vec![
|
vec![
|
||||||
Arc::new(deno::DenoLspAdapter::new()),
|
Arc::new(deno::DenoLspAdapter::new()),
|
||||||
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
||||||
],
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
false => {
|
false => {
|
||||||
language(
|
language!(
|
||||||
"tsx",
|
"tsx",
|
||||||
vec![
|
vec![
|
||||||
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
|
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
|
||||||
Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())),
|
Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())),
|
||||||
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
||||||
],
|
]
|
||||||
);
|
);
|
||||||
language(
|
language!(
|
||||||
"typescript",
|
"typescript",
|
||||||
vec![
|
vec![
|
||||||
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
|
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
|
||||||
Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())),
|
Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())),
|
||||||
],
|
]
|
||||||
);
|
);
|
||||||
language(
|
language!(
|
||||||
"javascript",
|
"javascript",
|
||||||
vec![
|
vec![
|
||||||
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
|
Arc::new(typescript::TypeScriptLspAdapter::new(node_runtime.clone())),
|
||||||
Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())),
|
Arc::new(typescript::EsLintLspAdapter::new(node_runtime.clone())),
|
||||||
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
||||||
],
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
language("haskell", vec![Arc::new(haskell::HaskellLanguageServer {})]);
|
language!("haskell", vec![Arc::new(haskell::HaskellLanguageServer {})]);
|
||||||
language(
|
language!(
|
||||||
"html",
|
"html",
|
||||||
vec![
|
vec![
|
||||||
Arc::new(html::HtmlLspAdapter::new(node_runtime.clone())),
|
Arc::new(html::HtmlLspAdapter::new(node_runtime.clone())),
|
||||||
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
||||||
],
|
]
|
||||||
);
|
);
|
||||||
language("ruby", vec![Arc::new(ruby::RubyLanguageServer)]);
|
language!("ruby", vec![Arc::new(ruby::RubyLanguageServer)]);
|
||||||
language(
|
language!(
|
||||||
"erb",
|
"erb",
|
||||||
vec![
|
vec![
|
||||||
Arc::new(ruby::RubyLanguageServer),
|
Arc::new(ruby::RubyLanguageServer),
|
||||||
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
||||||
],
|
]
|
||||||
);
|
);
|
||||||
language("scheme", vec![]);
|
language!("scheme");
|
||||||
language("racket", vec![]);
|
language!("racket");
|
||||||
language("lua", vec![Arc::new(lua::LuaLspAdapter)]);
|
language!("lua", vec![Arc::new(lua::LuaLspAdapter)]);
|
||||||
language(
|
language!(
|
||||||
"yaml",
|
"yaml",
|
||||||
vec![Arc::new(yaml::YamlLspAdapter::new(node_runtime.clone()))],
|
vec![Arc::new(yaml::YamlLspAdapter::new(node_runtime.clone()))]
|
||||||
);
|
);
|
||||||
language(
|
language!(
|
||||||
"svelte",
|
"svelte",
|
||||||
vec![
|
vec![
|
||||||
Arc::new(svelte::SvelteLspAdapter::new(node_runtime.clone())),
|
Arc::new(svelte::SvelteLspAdapter::new(node_runtime.clone())),
|
||||||
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
||||||
],
|
]
|
||||||
);
|
);
|
||||||
language(
|
language!(
|
||||||
"php",
|
"php",
|
||||||
vec![
|
vec![
|
||||||
Arc::new(php::IntelephenseLspAdapter::new(node_runtime.clone())),
|
Arc::new(php::IntelephenseLspAdapter::new(node_runtime.clone())),
|
||||||
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
|
||||||
],
|
]
|
||||||
);
|
);
|
||||||
language(
|
language!(
|
||||||
"purescript",
|
"purescript",
|
||||||
vec![Arc::new(purescript::PurescriptLspAdapter::new(
|
vec![Arc::new(purescript::PurescriptLspAdapter::new(
|
||||||
node_runtime.clone(),
|
node_runtime.clone(),
|
||||||
))],
|
))]
|
||||||
);
|
);
|
||||||
language(
|
language!(
|
||||||
"elm",
|
"elm",
|
||||||
vec![Arc::new(elm::ElmLspAdapter::new(node_runtime.clone()))],
|
vec![Arc::new(elm::ElmLspAdapter::new(node_runtime.clone()))]
|
||||||
);
|
);
|
||||||
language("glsl", vec![]);
|
language!("glsl");
|
||||||
language("nix", vec![]);
|
language!("nix");
|
||||||
language("nu", vec![Arc::new(nu::NuLanguageServer {})]);
|
language!("nu", vec![Arc::new(nu::NuLanguageServer {})]);
|
||||||
language("ocaml", vec![Arc::new(ocaml::OCamlLspAdapter)]);
|
language!("ocaml", vec![Arc::new(ocaml::OCamlLspAdapter)]);
|
||||||
language("ocaml-interface", vec![Arc::new(ocaml::OCamlLspAdapter)]);
|
language!("ocaml-interface", vec![Arc::new(ocaml::OCamlLspAdapter)]);
|
||||||
language(
|
language!(
|
||||||
"vue",
|
"vue",
|
||||||
vec![Arc::new(vue::VueLspAdapter::new(node_runtime.clone()))],
|
vec![Arc::new(vue::VueLspAdapter::new(node_runtime.clone()))]
|
||||||
);
|
);
|
||||||
language("uiua", vec![Arc::new(uiua::UiuaLanguageServer {})]);
|
language!("uiua", vec![Arc::new(uiua::UiuaLanguageServer {})]);
|
||||||
language("proto", vec![]);
|
language!("proto");
|
||||||
language("terraform", vec![Arc::new(terraform::TerraformLspAdapter)]);
|
language!("terraform", vec![Arc::new(terraform::TerraformLspAdapter)]);
|
||||||
language(
|
language!(
|
||||||
"terraform-vars",
|
"terraform-vars",
|
||||||
vec![Arc::new(terraform::TerraformLspAdapter)],
|
vec![Arc::new(terraform::TerraformLspAdapter)]
|
||||||
);
|
);
|
||||||
language("hcl", vec![]);
|
language!("hcl", vec![]);
|
||||||
language(
|
language!(
|
||||||
"prisma",
|
"prisma",
|
||||||
vec![Arc::new(prisma::PrismaLspAdapter::new(
|
vec![Arc::new(prisma::PrismaLspAdapter::new(
|
||||||
node_runtime.clone(),
|
node_runtime.clone(),
|
||||||
))],
|
))]
|
||||||
);
|
);
|
||||||
language("dart", vec![Arc::new(dart::DartLanguageServer {})]);
|
language!("dart", vec![Arc::new(dart::DartLanguageServer {})]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
|
|
@ -600,6 +600,20 @@ impl LanguageServer {
|
||||||
related_document_support: Some(true),
|
related_document_support: Some(true),
|
||||||
dynamic_registration: None,
|
dynamic_registration: None,
|
||||||
}),
|
}),
|
||||||
|
document_symbol: Some(DocumentSymbolClientCapabilities {
|
||||||
|
dynamic_registration: None,
|
||||||
|
symbol_kind: Some(SymbolKindCapability {
|
||||||
|
value_set: Some(vec![
|
||||||
|
SymbolKind::FUNCTION,
|
||||||
|
SymbolKind::PACKAGE,
|
||||||
|
SymbolKind::MODULE,
|
||||||
|
SymbolKind::FILE,
|
||||||
|
SymbolKind::NAMESPACE
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
hierarchical_document_symbol_support: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
experimental: Some(json!({
|
experimental: Some(json!({
|
||||||
|
|
|
@ -17,8 +17,8 @@ use language::{
|
||||||
Unclipped,
|
Unclipped,
|
||||||
};
|
};
|
||||||
use lsp::{
|
use lsp::{
|
||||||
CompletionListItemDefaultsEditRange, DocumentHighlightKind, LanguageServer, LanguageServerId,
|
CompletionListItemDefaultsEditRange, DocumentHighlightKind, DocumentSymbol,
|
||||||
OneOf, ServerCapabilities,
|
DocumentSymbolResponse, LanguageServer, LanguageServerId, OneOf, ServerCapabilities,
|
||||||
};
|
};
|
||||||
use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
|
use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
|
||||||
use text::{BufferId, LineEnding};
|
use text::{BufferId, LineEnding};
|
||||||
|
@ -141,6 +141,8 @@ pub(crate) struct InlayHints {
|
||||||
pub range: Range<Anchor>,
|
pub range: Range<Anchor>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) struct DocumentSymbols;
|
||||||
|
|
||||||
pub(crate) struct FormattingOptions {
|
pub(crate) struct FormattingOptions {
|
||||||
tab_size: u32,
|
tab_size: u32,
|
||||||
}
|
}
|
||||||
|
@ -2480,3 +2482,94 @@ impl LspCommand for InlayHints {
|
||||||
BufferId::new(message.buffer_id)
|
BufferId::new(message.buffer_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait(?Send)]
|
||||||
|
impl LspCommand for DocumentSymbols {
|
||||||
|
type Response = Vec<DocumentSymbol>;
|
||||||
|
type LspRequest = lsp::DocumentSymbolRequest;
|
||||||
|
// todo: add proto support
|
||||||
|
type ProtoRequest = proto::InlayHints;
|
||||||
|
fn check_capabilities(&self, server_capabilities: &lsp::ServerCapabilities) -> bool {
|
||||||
|
let Some(document_symbol_capabilities) = &server_capabilities.document_symbol_provider
|
||||||
|
else {
|
||||||
|
dbg!("Sadge");
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
match document_symbol_capabilities {
|
||||||
|
lsp::OneOf::Left(enabled) => dbg!(*enabled),
|
||||||
|
lsp::OneOf::Right(_) => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_lsp(
|
||||||
|
&self,
|
||||||
|
path: &Path,
|
||||||
|
buffer: &Buffer,
|
||||||
|
_: &Arc<LanguageServer>,
|
||||||
|
_: &AppContext,
|
||||||
|
) -> lsp::DocumentSymbolParams {
|
||||||
|
dbg!("to_lsp");
|
||||||
|
lsp::DocumentSymbolParams {
|
||||||
|
text_document: lsp::TextDocumentIdentifier {
|
||||||
|
uri: lsp::Url::from_file_path(path).unwrap(),
|
||||||
|
},
|
||||||
|
work_done_progress_params: Default::default(),
|
||||||
|
partial_result_params: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn response_from_lsp(
|
||||||
|
self,
|
||||||
|
message: Option<DocumentSymbolResponse>,
|
||||||
|
project: Model<Project>,
|
||||||
|
buffer: Model<Buffer>,
|
||||||
|
server_id: LanguageServerId,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> anyhow::Result<Vec<DocumentSymbol>> {
|
||||||
|
dbg!("response_from_lsp");
|
||||||
|
let message = message.context("Invalid DocumentSymbolResponse")?;
|
||||||
|
dbg!(message);
|
||||||
|
return Ok(vec![]);
|
||||||
|
// match message {
|
||||||
|
// DocumentSymbolResponse::Flat(contents) => {}
|
||||||
|
// DocumentSymbolResponse::Nested(contents) => {}
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::InlayHints {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn from_proto(
|
||||||
|
message: proto::InlayHints,
|
||||||
|
_: Model<Project>,
|
||||||
|
buffer: Model<Buffer>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> Result<Self> {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn response_to_proto(
|
||||||
|
response: Vec<lsp::DocumentSymbol>,
|
||||||
|
_: &mut Project,
|
||||||
|
_: PeerId,
|
||||||
|
buffer_version: &clock::Global,
|
||||||
|
_: &mut AppContext,
|
||||||
|
) -> proto::InlayHintsResponse {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn response_from_proto(
|
||||||
|
self,
|
||||||
|
message: proto::InlayHintsResponse,
|
||||||
|
_: Model<Project>,
|
||||||
|
buffer: Model<Buffer>,
|
||||||
|
mut cx: AsyncAppContext,
|
||||||
|
) -> anyhow::Result<Vec<lsp::DocumentSymbol>> {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn buffer_id_from_proto(message: &proto::InlayHints) -> Result<BufferId> {
|
||||||
|
BufferId::new(message.buffer_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@ use log::error;
|
||||||
use lsp::{
|
use lsp::{
|
||||||
DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions,
|
DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions,
|
||||||
DocumentHighlightKind, LanguageServer, LanguageServerBinary, LanguageServerId,
|
DocumentHighlightKind, LanguageServer, LanguageServerBinary, LanguageServerId,
|
||||||
MessageActionItem, OneOf,
|
MessageActionItem, OneOf, SymbolInformation,
|
||||||
};
|
};
|
||||||
use lsp_command::*;
|
use lsp_command::*;
|
||||||
use node_runtime::NodeRuntime;
|
use node_runtime::NodeRuntime;
|
||||||
|
@ -4911,6 +4911,18 @@ impl Project {
|
||||||
self.hover_impl(buffer, position, cx)
|
self.hover_impl(buffer, position, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn document_symbols(
|
||||||
|
&self,
|
||||||
|
buffer: &Model<Buffer>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Result<Vec<lsp::DocumentSymbol>>> {
|
||||||
|
self.request_lsp(
|
||||||
|
buffer.clone(),
|
||||||
|
LanguageServerToQuery::Primary,
|
||||||
|
DocumentSymbols,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}
|
||||||
#[inline(never)]
|
#[inline(never)]
|
||||||
fn completions_impl(
|
fn completions_impl(
|
||||||
&self,
|
&self,
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
use std::{any::TypeId, path::Path, sync::Arc};
|
use std::{any::TypeId, path::Path, sync::Arc};
|
||||||
|
|
||||||
use gpui::{AppContext, Context, Model, ModelContext, Subscription};
|
use gpui::{AppContext, Context, Model, ModelContext, Subscription};
|
||||||
|
use project_core::{Location, ProjectPath};
|
||||||
use task::{Source, Task, TaskId};
|
use task::{Source, Task, TaskId};
|
||||||
|
|
||||||
/// Inventory tracks available tasks for a given project.
|
/// Inventory tracks available tasks for a given project.
|
||||||
|
@ -55,7 +56,7 @@ impl Inventory {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pulls its sources to list runanbles for the path given (up to the source to decide what to return for no path).
|
/// Pulls its sources to list runanbles for the path given (up to the source to decide what to return for no path).
|
||||||
pub fn list_tasks(&self, path: Option<&Path>, cx: &mut AppContext) -> Vec<Arc<dyn Task>> {
|
pub fn list_tasks(&self, path: Option<&Location>, cx: &mut AppContext) -> Vec<Arc<dyn Task>> {
|
||||||
let mut tasks = Vec::new();
|
let mut tasks = Vec::new();
|
||||||
for source in &self.sources {
|
for source in &self.sources {
|
||||||
tasks.extend(
|
tasks.extend(
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
use std::path::Path;
|
use std::ops::Range;
|
||||||
use std::sync::atomic::AtomicUsize;
|
use std::sync::atomic::AtomicUsize;
|
||||||
use std::sync::atomic::Ordering::SeqCst;
|
use std::sync::atomic::Ordering::SeqCst;
|
||||||
|
use std::{path::Path, sync::Arc};
|
||||||
|
|
||||||
use language::DiagnosticEntry;
|
use gpui::Model;
|
||||||
|
use language::{Buffer, DiagnosticEntry};
|
||||||
use lsp::{DiagnosticSeverity, LanguageServerId};
|
use lsp::{DiagnosticSeverity, LanguageServerId};
|
||||||
use rpc::proto;
|
use rpc::proto;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
use worktree::WorktreeId;
|
||||||
|
|
||||||
mod ignore;
|
mod ignore;
|
||||||
pub mod project_settings;
|
pub mod project_settings;
|
||||||
|
@ -79,3 +82,15 @@ impl DiagnosticSummary {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||||
|
pub struct ProjectPath {
|
||||||
|
pub worktree_id: WorktreeId,
|
||||||
|
pub path: Arc<Path>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct Location {
|
||||||
|
pub buffer: Model<Buffer>,
|
||||||
|
pub range: Range<language::Anchor>,
|
||||||
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ anyhow.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
project_core.workspace = true
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json_lenient.workspace = true
|
serde_json_lenient.workspace = true
|
||||||
|
|
|
@ -6,6 +6,7 @@ pub mod static_source;
|
||||||
|
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use gpui::ModelContext;
|
use gpui::ModelContext;
|
||||||
|
use project_core::Location;
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -62,7 +63,7 @@ pub trait Source: Any {
|
||||||
/// Collects all tasks available for scheduling, for the path given.
|
/// Collects all tasks available for scheduling, for the path given.
|
||||||
fn tasks_for_path(
|
fn tasks_for_path(
|
||||||
&mut self,
|
&mut self,
|
||||||
path: Option<&Path>,
|
path: Option<&Location>,
|
||||||
cx: &mut ModelContext<Box<dyn Source>>,
|
cx: &mut ModelContext<Box<dyn Source>>,
|
||||||
) -> Vec<Arc<dyn Task>>;
|
) -> Vec<Arc<dyn Task>>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{Source, SpawnInTerminal, Task, TaskId};
|
use crate::{Source, SpawnInTerminal, Task, TaskId};
|
||||||
use gpui::{AppContext, Context, Model};
|
use gpui::{AppContext, Context, Model};
|
||||||
|
use project_core::{Location, ProjectPath};
|
||||||
|
|
||||||
/// A storage and source of tasks generated out of user command prompt inputs.
|
/// A storage and source of tasks generated out of user command prompt inputs.
|
||||||
pub struct OneshotSource {
|
pub struct OneshotSource {
|
||||||
|
@ -73,7 +74,7 @@ impl Source for OneshotSource {
|
||||||
|
|
||||||
fn tasks_for_path(
|
fn tasks_for_path(
|
||||||
&mut self,
|
&mut self,
|
||||||
_path: Option<&std::path::Path>,
|
_path: Option<&Location>,
|
||||||
_cx: &mut gpui::ModelContext<Box<dyn Source>>,
|
_cx: &mut gpui::ModelContext<Box<dyn Source>>,
|
||||||
) -> Vec<Arc<dyn Task>> {
|
) -> Vec<Arc<dyn Task>> {
|
||||||
self.tasks.clone()
|
self.tasks.clone()
|
||||||
|
|
|
@ -8,6 +8,7 @@ use std::{
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use gpui::{AppContext, Context, Model, ModelContext, Subscription};
|
use gpui::{AppContext, Context, Model, ModelContext, Subscription};
|
||||||
|
use project_core::Location;
|
||||||
use schemars::{gen::SchemaSettings, JsonSchema};
|
use schemars::{gen::SchemaSettings, JsonSchema};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
@ -184,7 +185,7 @@ impl StaticSource {
|
||||||
impl Source for StaticSource {
|
impl Source for StaticSource {
|
||||||
fn tasks_for_path(
|
fn tasks_for_path(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: Option<&Path>,
|
_: Option<&Location>,
|
||||||
_: &mut ModelContext<Box<dyn Source>>,
|
_: &mut ModelContext<Box<dyn Source>>,
|
||||||
) -> Vec<Arc<dyn Task>> {
|
) -> Vec<Arc<dyn Task>> {
|
||||||
self.tasks
|
self.tasks
|
||||||
|
|
|
@ -7,11 +7,15 @@ license = "GPL-3.0-or-later"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
editor.workspace = true
|
||||||
fuzzy.workspace = true
|
fuzzy.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
language.workspace = true
|
||||||
|
lsp.workspace = true
|
||||||
menu.workspace = true
|
menu.workspace = true
|
||||||
picker.workspace = true
|
picker.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
|
project_core.workspace = true
|
||||||
task.workspace = true
|
task.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
|
|
|
@ -7,6 +7,8 @@ use util::ResultExt;
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
mod modal;
|
mod modal;
|
||||||
|
mod tests_source;
|
||||||
|
pub use tests_source::TestSource;
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
cx.observe_new_views(
|
cx.observe_new_views(
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use editor::Anchor;
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, rems, AppContext, DismissEvent, EventEmitter, FocusableView, InteractiveElement,
|
actions, rems, AppContext, DismissEvent, EventEmitter, FocusableView, InteractiveElement,
|
||||||
|
@ -7,7 +8,8 @@ use gpui::{
|
||||||
VisualContext, WeakView,
|
VisualContext, WeakView,
|
||||||
};
|
};
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use project::Inventory;
|
use project::{Inventory, Item};
|
||||||
|
use project_core::Location;
|
||||||
use task::{oneshot_source::OneshotSource, Task};
|
use task::{oneshot_source::OneshotSource, Task};
|
||||||
use ui::{v_flex, HighlightedLabel, ListItem, ListItemSpacing, Selectable, WindowContext};
|
use ui::{v_flex, HighlightedLabel, ListItem, ListItemSpacing, Selectable, WindowContext};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
@ -129,10 +131,24 @@ impl PickerDelegate for TasksModalDelegate {
|
||||||
cx.spawn(move |picker, mut cx| async move {
|
cx.spawn(move |picker, mut cx| async move {
|
||||||
let Some(candidates) = picker
|
let Some(candidates) = picker
|
||||||
.update(&mut cx, |picker, cx| {
|
.update(&mut cx, |picker, cx| {
|
||||||
|
let editor = picker
|
||||||
|
.delegate
|
||||||
|
.workspace
|
||||||
|
.update(cx, |this, cx| this.active_item_as::<editor::Editor>(cx))
|
||||||
|
.ok()
|
||||||
|
.flatten();
|
||||||
|
let buffer =
|
||||||
|
(|| Some(editor.clone()?.read(cx).buffer().read(cx).as_singleton()?))();
|
||||||
|
let range = (|| Some(editor?.read(cx).selections.first_anchor()))();
|
||||||
|
let path = buffer.zip(range).map(|(buffer, _)| Location {
|
||||||
|
buffer,
|
||||||
|
range: language::Anchor::MIN..language::Anchor::MAX,
|
||||||
|
});
|
||||||
|
//let path = path.as_deref();
|
||||||
picker.delegate.candidates = picker
|
picker.delegate.candidates = picker
|
||||||
.delegate
|
.delegate
|
||||||
.inventory
|
.inventory
|
||||||
.update(cx, |inventory, cx| inventory.list_tasks(None, cx));
|
.update(cx, |inventory, cx| inventory.list_tasks(path.as_ref(), cx));
|
||||||
picker
|
picker
|
||||||
.delegate
|
.delegate
|
||||||
.candidates
|
.candidates
|
||||||
|
|
60
crates/tasks_ui/src/tests_source.rs
Normal file
60
crates/tasks_ui/src/tests_source.rs
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
use gpui::{AppContext, Model, Task, WeakModel};
|
||||||
|
use language::Buffer;
|
||||||
|
use project::Project;
|
||||||
|
use project_core::Location;
|
||||||
|
use task::Source;
|
||||||
|
use ui::Context;
|
||||||
|
|
||||||
|
/// Returns runnables for tests at current cursor, module and file.
|
||||||
|
pub struct TestSource {
|
||||||
|
project: WeakModel<Project>,
|
||||||
|
all_tasks_for: Option<(WeakModel<Buffer>, Task<Result<Vec<lsp::DocumentSymbol>>>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestSource {
|
||||||
|
pub fn new(project: WeakModel<Project>, cx: &mut AppContext) -> Model<Box<dyn Source>> {
|
||||||
|
cx.new_model(|_| {
|
||||||
|
Box::new(Self {
|
||||||
|
project,
|
||||||
|
all_tasks_for: None,
|
||||||
|
}) as _
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Source for TestSource {
|
||||||
|
fn as_any(&mut self) -> &mut dyn std::any::Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tasks_for_path(
|
||||||
|
&mut self,
|
||||||
|
_path: Option<&Location>,
|
||||||
|
cx: &mut gpui::ModelContext<Box<dyn Source>>,
|
||||||
|
) -> Vec<std::sync::Arc<dyn task::Task>> {
|
||||||
|
if let Some(path) = _path {
|
||||||
|
self.project.update(cx, move |_, cx| {
|
||||||
|
let p = path.buffer.clone();
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
dbg!("Heyyo");
|
||||||
|
dbg!(
|
||||||
|
this.update(&mut cx, |this, cx| this.document_symbols(&p, cx))
|
||||||
|
.ok()?
|
||||||
|
.await
|
||||||
|
);
|
||||||
|
dbg!("Hey");
|
||||||
|
Some(())
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
});
|
||||||
|
// self.all_tasks_for = Some((
|
||||||
|
// path.buffer.downgrade(),
|
||||||
|
// self.project.update(cx, |this, cx| {
|
||||||
|
// this.document_symbols(&path.buffer, cx).shared()
|
||||||
|
// }),
|
||||||
|
// ));
|
||||||
|
}
|
||||||
|
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,6 +28,7 @@ use settings::{
|
||||||
};
|
};
|
||||||
use std::{borrow::Cow, ops::Deref, path::Path, sync::Arc};
|
use std::{borrow::Cow, ops::Deref, path::Path, sync::Arc};
|
||||||
use task::{oneshot_source::OneshotSource, static_source::StaticSource};
|
use task::{oneshot_source::OneshotSource, static_source::StaticSource};
|
||||||
|
use tasks_ui::TestSource;
|
||||||
use terminal_view::terminal_panel::{self, TerminalPanel};
|
use terminal_view::terminal_panel::{self, TerminalPanel};
|
||||||
use util::{
|
use util::{
|
||||||
asset_str,
|
asset_str,
|
||||||
|
@ -160,13 +161,15 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
|
||||||
app_state.fs.clone(),
|
app_state.fs.clone(),
|
||||||
paths::TASKS.clone(),
|
paths::TASKS.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let static_source = StaticSource::new(tasks_file_rx, cx);
|
let static_source = StaticSource::new(tasks_file_rx, cx);
|
||||||
let oneshot_source = OneshotSource::new(cx);
|
let oneshot_source = OneshotSource::new(cx);
|
||||||
|
let tests_source = TestSource::new(project.downgrade(), cx);
|
||||||
project.update(cx, |project, cx| {
|
project.update(cx, |project, cx| {
|
||||||
project.task_inventory().update(cx, |inventory, cx| {
|
project.task_inventory().update(cx, |inventory, cx| {
|
||||||
inventory.add_source(oneshot_source, cx);
|
inventory.add_source(oneshot_source, cx);
|
||||||
inventory.add_source(static_source, cx);
|
inventory.add_source(static_source, cx);
|
||||||
|
inventory.add_source(tests_source, cx);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue