mirror of
https://github.com/rust-lang/rust-analyzer
synced 2025-01-13 21:54:42 +00:00
Auto merge of #16384 - Veykril:smolstr, r=Veykril
minor: Make use of some new `SmolStr` improvements
This commit is contained in:
commit
2dfa9b86d0
18 changed files with 80 additions and 61 deletions
|
@ -2,7 +2,7 @@
|
|||
|
||||
use std::fmt;
|
||||
|
||||
use syntax::{ast, utils::is_raw_identifier, SmolStr};
|
||||
use syntax::{ast, format_smolstr, utils::is_raw_identifier, SmolStr};
|
||||
|
||||
/// `Name` is a wrapper around string, which is used in hir for both references
|
||||
/// and declarations. In theory, names should also carry hygiene info, but we are
|
||||
|
@ -69,8 +69,8 @@ impl Name {
|
|||
}
|
||||
|
||||
/// Shortcut to create inline plain text name. Panics if `text.len() > 22`
|
||||
const fn new_inline(text: &str) -> Name {
|
||||
Name::new_text(SmolStr::new_inline(text))
|
||||
const fn new_static(text: &'static str) -> Name {
|
||||
Name::new_text(SmolStr::new_static(text))
|
||||
}
|
||||
|
||||
/// Resolve a name from the text of token.
|
||||
|
@ -83,7 +83,7 @@ impl Name {
|
|||
// Rust, e.g. "try" in Rust 2015. Even in such cases, we keep track of them in their
|
||||
// escaped form.
|
||||
None if is_raw_identifier(raw_text) => {
|
||||
Name::new_text(SmolStr::from_iter(["r#", raw_text]))
|
||||
Name::new_text(format_smolstr!("r#{}", raw_text))
|
||||
}
|
||||
_ => Name::new_text(raw_text.into()),
|
||||
}
|
||||
|
@ -99,7 +99,7 @@ impl Name {
|
|||
/// name is equal only to itself. It's not clear how to implement this in
|
||||
/// salsa though, so we punt on that bit for a moment.
|
||||
pub const fn missing() -> Name {
|
||||
Name::new_inline("[missing name]")
|
||||
Name::new_static("[missing name]")
|
||||
}
|
||||
|
||||
/// Returns true if this is a fake name for things missing in the source code. See
|
||||
|
@ -119,7 +119,7 @@ impl Name {
|
|||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
static CNT: AtomicUsize = AtomicUsize::new(0);
|
||||
let c = CNT.fetch_add(1, Ordering::Relaxed);
|
||||
Name::new_text(format!("<ra@gennew>{c}").into())
|
||||
Name::new_text(format_smolstr!("<ra@gennew>{c}"))
|
||||
}
|
||||
|
||||
/// Returns the tuple index this name represents if it is a tuple field.
|
||||
|
@ -260,7 +260,7 @@ pub mod known {
|
|||
$(
|
||||
#[allow(bad_style)]
|
||||
pub const $ident: super::Name =
|
||||
super::Name::new_inline(stringify!($ident));
|
||||
super::Name::new_static(stringify!($ident));
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
@ -471,11 +471,11 @@ pub mod known {
|
|||
);
|
||||
|
||||
// self/Self cannot be used as an identifier
|
||||
pub const SELF_PARAM: super::Name = super::Name::new_inline("self");
|
||||
pub const SELF_TYPE: super::Name = super::Name::new_inline("Self");
|
||||
pub const SELF_PARAM: super::Name = super::Name::new_static("self");
|
||||
pub const SELF_TYPE: super::Name = super::Name::new_static("Self");
|
||||
|
||||
pub const STATIC_LIFETIME: super::Name = super::Name::new_inline("'static");
|
||||
pub const DOLLAR_CRATE: super::Name = super::Name::new_inline("$crate");
|
||||
pub const STATIC_LIFETIME: super::Name = super::Name::new_static("'static");
|
||||
pub const DOLLAR_CRATE: super::Name = super::Name::new_static("$crate");
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! name {
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
//! A simplified version of quote-crate like quasi quote macro
|
||||
|
||||
use span::Span;
|
||||
use syntax::format_smolstr;
|
||||
|
||||
use crate::name::Name;
|
||||
|
||||
pub(crate) fn dollar_crate(span: Span) -> tt::Ident<Span> {
|
||||
tt::Ident { text: syntax::SmolStr::new_inline("$crate"), span }
|
||||
pub(crate) const fn dollar_crate(span: Span) -> tt::Ident<Span> {
|
||||
tt::Ident { text: syntax::SmolStr::new_static("$crate"), span }
|
||||
}
|
||||
|
||||
// A helper macro quote macro
|
||||
|
@ -214,8 +215,8 @@ impl_to_to_tokentrees! {
|
|||
_span: crate::tt::Literal => self { self };
|
||||
_span: crate::tt::Ident => self { self };
|
||||
_span: crate::tt::Punct => self { self };
|
||||
span: &str => self { crate::tt::Literal{text: format!("\"{}\"", self.escape_default()).into(), span}};
|
||||
span: String => self { crate::tt::Literal{text: format!("\"{}\"", self.escape_default()).into(), span}};
|
||||
span: &str => self { crate::tt::Literal{text: format_smolstr!("\"{}\"", self.escape_default()), span}};
|
||||
span: String => self { crate::tt::Literal{text: format_smolstr!("\"{}\"", self.escape_default()), span}};
|
||||
span: Name => self { crate::tt::Ident{text: self.to_smol_str(), span}};
|
||||
}
|
||||
|
||||
|
|
|
@ -81,7 +81,7 @@ use rustc_hash::FxHashSet;
|
|||
use stdx::{impl_from, never};
|
||||
use syntax::{
|
||||
ast::{self, HasAttrs as _, HasName},
|
||||
AstNode, AstPtr, SmolStr, SyntaxNode, SyntaxNodePtr, TextRange, T,
|
||||
format_smolstr, AstNode, AstPtr, SmolStr, SyntaxNode, SyntaxNodePtr, TextRange, T,
|
||||
};
|
||||
use triomphe::Arc;
|
||||
|
||||
|
@ -4284,9 +4284,9 @@ impl Type {
|
|||
.filter_map(|arg| {
|
||||
// arg can be either a `Ty` or `constant`
|
||||
if let Some(ty) = arg.ty(Interner) {
|
||||
Some(SmolStr::new(ty.display(db).to_string()))
|
||||
Some(format_smolstr!("{}", ty.display(db)))
|
||||
} else if let Some(const_) = arg.constant(Interner) {
|
||||
Some(SmolStr::new_inline(&const_.display(db).to_string()))
|
||||
Some(format_smolstr!("{}", const_.display(db)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ use std::iter;
|
|||
|
||||
use hir::{known, HasAttrs, ScopeDef, Variant};
|
||||
use ide_db::{imports::import_assets::LocatedImport, RootDatabase, SymbolKind};
|
||||
use syntax::ast;
|
||||
use syntax::{ast, SmolStr};
|
||||
|
||||
use crate::{
|
||||
context::{
|
||||
|
@ -80,7 +80,11 @@ impl Completions {
|
|||
}
|
||||
|
||||
pub(crate) fn add_keyword(&mut self, ctx: &CompletionContext<'_>, keyword: &'static str) {
|
||||
let item = CompletionItem::new(CompletionItemKind::Keyword, ctx.source_range(), keyword);
|
||||
let item = CompletionItem::new(
|
||||
CompletionItemKind::Keyword,
|
||||
ctx.source_range(),
|
||||
SmolStr::new_static(keyword),
|
||||
);
|
||||
item.add_to(self, ctx.db);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
//! Completes references after dot (fields and method calls).
|
||||
|
||||
use ide_db::FxHashSet;
|
||||
use syntax::SmolStr;
|
||||
|
||||
use crate::{
|
||||
context::{CompletionContext, DotAccess, DotAccessKind, ExprCtx, PathCompletionCtx, Qualified},
|
||||
|
@ -20,8 +21,11 @@ pub(crate) fn complete_dot(
|
|||
|
||||
// Suggest .await syntax for types that implement Future trait
|
||||
if receiver_ty.impls_into_future(ctx.db) {
|
||||
let mut item =
|
||||
CompletionItem::new(CompletionItemKind::Keyword, ctx.source_range(), "await");
|
||||
let mut item = CompletionItem::new(
|
||||
CompletionItemKind::Keyword,
|
||||
ctx.source_range(),
|
||||
SmolStr::new_static("await"),
|
||||
);
|
||||
item.detail("expr.await");
|
||||
item.add_to(acc, ctx.db);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
//! Completes function abi strings.
|
||||
use syntax::{
|
||||
ast::{self, IsString},
|
||||
AstNode, AstToken,
|
||||
AstNode, AstToken, SmolStr,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
|
@ -53,7 +53,8 @@ pub(crate) fn complete_extern_abi(
|
|||
let abi_str = expanded;
|
||||
let source_range = abi_str.text_range_between_quotes()?;
|
||||
for &abi in SUPPORTED_CALLING_CONVENTIONS {
|
||||
CompletionItem::new(CompletionItemKind::Keyword, source_range, abi).add_to(acc, ctx.db);
|
||||
CompletionItem::new(CompletionItemKind::Keyword, source_range, SmolStr::new_static(abi))
|
||||
.add_to(acc, ctx.db);
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ use ide_db::{
|
|||
};
|
||||
use syntax::{
|
||||
ast::{self, edit_in_place::AttrsOwnerEdit, HasTypeBounds},
|
||||
AstNode, SyntaxElement, SyntaxKind, TextRange, T,
|
||||
format_smolstr, AstNode, SmolStr, SyntaxElement, SyntaxKind, TextRange, T,
|
||||
};
|
||||
use text_edit::TextEdit;
|
||||
|
||||
|
@ -180,7 +180,7 @@ fn add_function_impl(
|
|||
) {
|
||||
let fn_name = func.name(ctx.db);
|
||||
|
||||
let label = format!(
|
||||
let label = format_smolstr!(
|
||||
"fn {}({})",
|
||||
fn_name.display(ctx.db),
|
||||
if func.assoc_fn_params(ctx.db).is_empty() { "" } else { ".." }
|
||||
|
@ -254,7 +254,7 @@ fn add_type_alias_impl(
|
|||
) {
|
||||
let alias_name = type_alias.name(ctx.db).unescaped().to_smol_str();
|
||||
|
||||
let label = format!("type {alias_name} =");
|
||||
let label = format_smolstr!("type {alias_name} =");
|
||||
|
||||
let mut item = CompletionItem::new(SymbolKind::TypeAlias, replacement_range, label);
|
||||
item.lookup_by(format!("type {alias_name}"))
|
||||
|
@ -329,7 +329,7 @@ fn add_const_impl(
|
|||
let replacement = format!("{label} ");
|
||||
|
||||
let mut item = CompletionItem::new(SymbolKind::Const, replacement_range, label);
|
||||
item.lookup_by(format!("const {const_name}"))
|
||||
item.lookup_by(format_smolstr!("const {const_name}"))
|
||||
.set_documentation(const_.docs(ctx.db))
|
||||
.set_relevance(CompletionRelevance {
|
||||
is_item_from_trait: true,
|
||||
|
@ -348,7 +348,7 @@ fn add_const_impl(
|
|||
}
|
||||
}
|
||||
|
||||
fn make_const_compl_syntax(const_: &ast::Const, needs_whitespace: bool) -> String {
|
||||
fn make_const_compl_syntax(const_: &ast::Const, needs_whitespace: bool) -> SmolStr {
|
||||
let const_ = if needs_whitespace {
|
||||
insert_whitespace_into_node::insert_ws_into(const_.syntax().clone())
|
||||
} else {
|
||||
|
@ -368,7 +368,7 @@ fn make_const_compl_syntax(const_: &ast::Const, needs_whitespace: bool) -> Strin
|
|||
|
||||
let syntax = const_.text().slice(range).to_string();
|
||||
|
||||
format!("{} =", syntax.trim_end())
|
||||
format_smolstr!("{} =", syntax.trim_end())
|
||||
}
|
||||
|
||||
fn function_declaration(node: &ast::Fn, needs_whitespace: bool) -> String {
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
//! Complete fields in record literals and patterns.
|
||||
use ide_db::SymbolKind;
|
||||
use syntax::ast::{self, Expr};
|
||||
use syntax::{
|
||||
ast::{self, Expr},
|
||||
SmolStr,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
context::{DotAccess, DotAccessKind, PatternContext},
|
||||
|
@ -66,8 +69,11 @@ pub(crate) fn complete_record_expr_fields(
|
|||
}
|
||||
if dot_prefix {
|
||||
cov_mark::hit!(functional_update_one_dot);
|
||||
let mut item =
|
||||
CompletionItem::new(CompletionItemKind::Snippet, ctx.source_range(), "..");
|
||||
let mut item = CompletionItem::new(
|
||||
CompletionItemKind::Snippet,
|
||||
ctx.source_range(),
|
||||
SmolStr::new_static(".."),
|
||||
);
|
||||
item.insert_text(".");
|
||||
item.add_to(acc, ctx.db);
|
||||
return;
|
||||
|
@ -91,7 +97,11 @@ pub(crate) fn add_default_update(
|
|||
// FIXME: This should make use of scope_def like completions so we get all the other goodies
|
||||
// that is we should handle this like actually completing the default function
|
||||
let completion_text = "..Default::default()";
|
||||
let mut item = CompletionItem::new(SymbolKind::Field, ctx.source_range(), completion_text);
|
||||
let mut item = CompletionItem::new(
|
||||
SymbolKind::Field,
|
||||
ctx.source_range(),
|
||||
SmolStr::new_static(completion_text),
|
||||
);
|
||||
let completion_text =
|
||||
completion_text.strip_prefix(ctx.token.text()).unwrap_or(completion_text);
|
||||
item.insert_text(completion_text).set_relevance(CompletionRelevance {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
use hir::ScopeDef;
|
||||
use ide_db::{FxHashSet, SymbolKind};
|
||||
use syntax::{ast, AstNode};
|
||||
use syntax::{ast, format_smolstr, AstNode};
|
||||
|
||||
use crate::{
|
||||
context::{CompletionContext, PathCompletionCtx, Qualified},
|
||||
|
@ -108,7 +108,7 @@ pub(crate) fn complete_use_path(
|
|||
let item = CompletionItem::new(
|
||||
CompletionItemKind::SymbolKind(SymbolKind::Enum),
|
||||
ctx.source_range(),
|
||||
format!("{}::", e.name(ctx.db).display(ctx.db)),
|
||||
format_smolstr!("{}::", e.name(ctx.db).display(ctx.db)),
|
||||
);
|
||||
acc.add(item.build(ctx.db));
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ use ide_db::{
|
|||
use itertools::Itertools;
|
||||
use smallvec::SmallVec;
|
||||
use stdx::{impl_from, never};
|
||||
use syntax::{SmolStr, TextRange, TextSize};
|
||||
use syntax::{format_smolstr, SmolStr, TextRange, TextSize};
|
||||
use text_edit::TextEdit;
|
||||
|
||||
use crate::{
|
||||
|
@ -442,7 +442,7 @@ impl Builder {
|
|||
|
||||
if !self.doc_aliases.is_empty() {
|
||||
let doc_aliases = self.doc_aliases.iter().join(", ");
|
||||
label_detail.replace(SmolStr::from(format!(" (alias {doc_aliases})")));
|
||||
label_detail.replace(format_smolstr!(" (alias {doc_aliases})"));
|
||||
let lookup_doc_aliases = self
|
||||
.doc_aliases
|
||||
.iter()
|
||||
|
@ -459,21 +459,21 @@ impl Builder {
|
|||
// after typing a comma or space.
|
||||
.join("");
|
||||
if !lookup_doc_aliases.is_empty() {
|
||||
lookup = SmolStr::from(format!("{lookup}{lookup_doc_aliases}"));
|
||||
lookup = format_smolstr!("{lookup}{lookup_doc_aliases}");
|
||||
}
|
||||
}
|
||||
if let [import_edit] = &*self.imports_to_add {
|
||||
// snippets can have multiple imports, but normal completions only have up to one
|
||||
label_detail.replace(SmolStr::from(format!(
|
||||
label_detail.replace(format_smolstr!(
|
||||
"{} (use {})",
|
||||
label_detail.as_deref().unwrap_or_default(),
|
||||
import_edit.import_path.display(db)
|
||||
)));
|
||||
));
|
||||
} else if let Some(trait_name) = self.trait_name {
|
||||
label_detail.replace(SmolStr::from(format!(
|
||||
label_detail.replace(format_smolstr!(
|
||||
"{} (as {trait_name})",
|
||||
label_detail.as_deref().unwrap_or_default(),
|
||||
)));
|
||||
));
|
||||
}
|
||||
|
||||
let text_edit = match self.text_edit {
|
||||
|
|
|
@ -17,7 +17,7 @@ use ide_db::{
|
|||
imports::import_assets::LocatedImport,
|
||||
RootDatabase, SnippetCap, SymbolKind,
|
||||
};
|
||||
use syntax::{AstNode, SmolStr, SyntaxKind, TextRange};
|
||||
use syntax::{format_smolstr, AstNode, SmolStr, SyntaxKind, TextRange};
|
||||
use text_edit::TextEdit;
|
||||
|
||||
use crate::{
|
||||
|
@ -202,7 +202,7 @@ fn field_with_receiver(
|
|||
) -> SmolStr {
|
||||
receiver.map_or_else(
|
||||
|| field_name.into(),
|
||||
|receiver| format!("{}.{field_name}", receiver.display(db)).into(),
|
||||
|receiver| format_smolstr!("{}.{field_name}", receiver.display(db)),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ use hir::{db::HirDatabase, AsAssocItem, HirDisplay};
|
|||
use ide_db::{SnippetCap, SymbolKind};
|
||||
use itertools::Itertools;
|
||||
use stdx::{format_to, to_lower_snake_case};
|
||||
use syntax::{AstNode, SmolStr};
|
||||
use syntax::{format_smolstr, AstNode, SmolStr};
|
||||
|
||||
use crate::{
|
||||
context::{CompletionContext, DotAccess, DotAccessKind, PathCompletionCtx, PathKind},
|
||||
|
@ -52,13 +52,12 @@ fn render(
|
|||
|
||||
let (call, escaped_call) = match &func_kind {
|
||||
FuncKind::Method(_, Some(receiver)) => (
|
||||
format!(
|
||||
format_smolstr!(
|
||||
"{}.{}",
|
||||
receiver.unescaped().display(ctx.db()),
|
||||
name.unescaped().display(ctx.db())
|
||||
)
|
||||
.into(),
|
||||
format!("{}.{}", receiver.display(ctx.db()), name.display(ctx.db())).into(),
|
||||
),
|
||||
format_smolstr!("{}.{}", receiver.display(ctx.db()), name.display(ctx.db())),
|
||||
),
|
||||
_ => (name.unescaped().to_smol_str(), name.to_smol_str()),
|
||||
};
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
use hir::HirDisplay;
|
||||
use ide_db::{documentation::Documentation, SymbolKind};
|
||||
use syntax::SmolStr;
|
||||
use syntax::{format_smolstr, SmolStr};
|
||||
|
||||
use crate::{
|
||||
context::{PathCompletionCtx, PathKind, PatternContext},
|
||||
|
@ -94,7 +94,7 @@ fn label(
|
|||
) -> SmolStr {
|
||||
if needs_bang {
|
||||
if ctx.snippet_cap().is_some() {
|
||||
SmolStr::from_iter([&*name, "!", bra, "…", ket])
|
||||
format_smolstr!("{name}!{bra}…{ket}",)
|
||||
} else {
|
||||
banged_name(name)
|
||||
}
|
||||
|
|
|
@ -74,7 +74,7 @@ fn variant_hints(
|
|||
},
|
||||
Some(InlayTooltip::String(match &d {
|
||||
Ok(_) => "enum variant discriminant".into(),
|
||||
Err(e) => format!("{e:?}").into(),
|
||||
Err(e) => format!("{e:?}"),
|
||||
})),
|
||||
None,
|
||||
);
|
||||
|
|
|
@ -4,11 +4,11 @@
|
|||
//! ```
|
||||
use ide_db::{syntax_helpers::node_ext::walk_ty, FxHashMap};
|
||||
use itertools::Itertools;
|
||||
use syntax::SmolStr;
|
||||
use syntax::{
|
||||
ast::{self, AstNode, HasGenericParams, HasName},
|
||||
SyntaxToken,
|
||||
};
|
||||
use syntax::{format_smolstr, SmolStr};
|
||||
|
||||
use crate::{InlayHint, InlayHintPosition, InlayHintsConfig, InlayKind, LifetimeElisionHints};
|
||||
|
||||
|
@ -80,7 +80,7 @@ pub(super) fn hints(
|
|||
let mut gen_idx_name = {
|
||||
let mut gen = (0u8..).map(|idx| match idx {
|
||||
idx if idx < 10 => SmolStr::from_iter(['\'', (idx + 48) as char]),
|
||||
idx => format!("'{idx}").into(),
|
||||
idx => format_smolstr!("'{idx}"),
|
||||
});
|
||||
move || gen.next().unwrap_or_default()
|
||||
};
|
||||
|
|
|
@ -17,7 +17,7 @@ use ide_db::{
|
|||
use stdx::never;
|
||||
use syntax::{
|
||||
ast::{self, HasName},
|
||||
AstNode, SmolStr, SyntaxNode, TextRange,
|
||||
format_smolstr, AstNode, SmolStr, SyntaxNode, TextRange,
|
||||
};
|
||||
|
||||
/// `NavigationTarget` represents an element in the editor's UI which you can
|
||||
|
@ -457,7 +457,7 @@ impl TryToNav for hir::Field {
|
|||
|(FileRange { file_id, range: full_range }, focus_range)| {
|
||||
NavigationTarget::from_syntax(
|
||||
file_id,
|
||||
format!("{}", self.index()).into(),
|
||||
format_smolstr!("{}", self.index()),
|
||||
focus_range,
|
||||
full_range,
|
||||
SymbolKind::Field,
|
||||
|
|
|
@ -96,19 +96,19 @@ impl<S: Span> Bindings<S> {
|
|||
| MetaVarKind::Expr
|
||||
| MetaVarKind::Ident => {
|
||||
Fragment::Tokens(tt::TokenTree::Leaf(tt::Leaf::Ident(tt::Ident {
|
||||
text: SmolStr::new_inline("missing"),
|
||||
text: SmolStr::new_static("missing"),
|
||||
span,
|
||||
})))
|
||||
}
|
||||
MetaVarKind::Lifetime => {
|
||||
Fragment::Tokens(tt::TokenTree::Leaf(tt::Leaf::Ident(tt::Ident {
|
||||
text: SmolStr::new_inline("'missing"),
|
||||
text: SmolStr::new_static("'missing"),
|
||||
span,
|
||||
})))
|
||||
}
|
||||
MetaVarKind::Literal => {
|
||||
Fragment::Tokens(tt::TokenTree::Leaf(tt::Leaf::Ident(tt::Ident {
|
||||
text: SmolStr::new_inline("\"missing\""),
|
||||
text: SmolStr::new_static("\"missing\""),
|
||||
span,
|
||||
})))
|
||||
}
|
||||
|
|
|
@ -70,7 +70,7 @@ pub use rowan::{
|
|||
api::Preorder, Direction, GreenNode, NodeOrToken, SyntaxText, TextRange, TextSize,
|
||||
TokenAtOffset, WalkEvent,
|
||||
};
|
||||
pub use smol_str::SmolStr;
|
||||
pub use smol_str::{format_smolstr, SmolStr};
|
||||
|
||||
/// `Parse` is the result of the parsing: a syntax tree and a collection of
|
||||
/// errors.
|
||||
|
|
Loading…
Reference in a new issue