mirror of
https://github.com/ultimatepp/ultimatepp.git
synced 2026-08-28 06:12:53 -06:00
pdb: Fixed visualise issues #1306, CtrlLib: fixed multirrot TreeCtrl flag init
git-svn-id: svn://ultimatepp.org/upp/trunk@9087 f0d560ea-af0d-0410-9eb7-867de7ffcac7
This commit is contained in:
parent
0f99d35ffa
commit
480ff2baff
8 changed files with 237 additions and 219 deletions
|
|
@ -56,7 +56,7 @@ TreeCtrl::Node::Node(const Image& img, Ctrl& ctrl, int cx, int cy)
|
|||
TreeCtrl::TreeCtrl()
|
||||
{
|
||||
display = &StdDisplay();
|
||||
levelcx = 16;
|
||||
levelcx = DPI(16);
|
||||
nocursor = false;
|
||||
noroot = false;
|
||||
dirty = true;
|
||||
|
|
@ -66,6 +66,7 @@ TreeCtrl::TreeCtrl()
|
|||
mousemove = false;
|
||||
accel = false;
|
||||
treesize = Size(0, 0);
|
||||
multiroot = false;
|
||||
Clear();
|
||||
SetFrame(ViewFrame());
|
||||
AddFrame(sb);
|
||||
|
|
|
|||
|
|
@ -309,216 +309,6 @@ void Pdb::Data()
|
|||
}
|
||||
}
|
||||
|
||||
void Pdb::TreeNode(int parent, const String& name, Pdb::Val val)
|
||||
{
|
||||
NamedVal nv;
|
||||
nv.name = name;
|
||||
nv.val = val;
|
||||
tree.Add(parent, Null, RawToValue(nv), name + "=" + Visualise(val).GetString(), val.type >= 0 || val.ref > 0);
|
||||
}
|
||||
|
||||
void Pdb::TreeExpand(int node)
|
||||
{
|
||||
if(tree.GetChildCount(node))
|
||||
return;
|
||||
Value v = tree.Get(node);
|
||||
if(!v.Is<NamedVal>())
|
||||
return;
|
||||
const NamedVal& nv = ValueTo<NamedVal>(tree.Get(node));
|
||||
Val val = nv.val;
|
||||
if(nv.val.ref > 0) {
|
||||
val = DeRef(val);
|
||||
if(val.type < 0 || val.ref > 0) {
|
||||
TreeNode(node, '*' + nv.name, val);
|
||||
SaveTree();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(val.type < 0) {
|
||||
SaveTree();
|
||||
return;
|
||||
}
|
||||
const Type& t = GetType(val.type);
|
||||
if(t.vtbl_typeindex == -2) {
|
||||
int count = GetSymInfo(t.modbase, type.GetKey(val.type), TI_GET_COUNT);
|
||||
Val prtti;
|
||||
prtti.ref = 1;
|
||||
prtti.type = UINT4;
|
||||
prtti.address = val.address - 4;
|
||||
Val rtti = GetRVal(prtti);
|
||||
FnInfo rtf = GetFnInfo(rtti.address);
|
||||
TreeNode(node, rtf.name, prtti);
|
||||
for(int i = 0; i < count; i++) {
|
||||
Val ventry;
|
||||
ventry.type = PFUNC;
|
||||
ventry.address = val.address + 4 * i;
|
||||
TreeNode(node, NFormat("[%d]", i), ventry);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(t.vtbl_typeindex >= 0) {
|
||||
Val vtbl;
|
||||
vtbl.ref = 1;
|
||||
vtbl.address = val.address + t.vtbl_offset;
|
||||
vtbl.type = t.vtbl_typeindex;
|
||||
TreeNode(node, "virtual", vtbl);
|
||||
/*
|
||||
Val vloc = GetRVal(vtbl);
|
||||
FnInfo vtbl_type = GetFnInfo(vloc.address);
|
||||
const char *p = vtbl_type.name, *e = p;
|
||||
while(*e && !(*e == ':' && e[1] == ':'))
|
||||
e++;
|
||||
String fullsym(p, e);
|
||||
FnInfo ffs = GetFnInfo(fullsym);
|
||||
if(ffs.pdbtype) {
|
||||
Val typeval;
|
||||
typeval.address = val.address;
|
||||
TypeVal(typeval, ffs.pdbtype, t.modbase);
|
||||
TreeNode(node, String().Cat() << '[' << ffs.name << ']', typeval);
|
||||
}
|
||||
*/
|
||||
}
|
||||
for(int i = 0; i < t.base.GetCount(); i++) {
|
||||
Val r = t.base[i];
|
||||
r.address += val.address;
|
||||
if(r.type >= 0) {
|
||||
const Type& bt = GetType(r.type);
|
||||
TreeNode(node, bt.name, r);
|
||||
}
|
||||
}
|
||||
for(int i = 0; i < t.member.GetCount(); i++) {
|
||||
Val r = t.member[i];
|
||||
r.address += val.address;
|
||||
TreeNode(node, t.member.GetKey(i), r);
|
||||
}
|
||||
for(int i = 0; i < t.static_member.GetCount(); i++) {
|
||||
Val r = t.static_member[i];
|
||||
TreeNode(node, t.static_member.GetKey(i), r);
|
||||
}
|
||||
}
|
||||
|
||||
String Pdb::StoreTree(int parent)
|
||||
{
|
||||
String result;
|
||||
int n = tree.GetChildCount(parent);
|
||||
for(int i = 0; i < n; i++) {
|
||||
int child = tree.GetChild(parent, i);
|
||||
if(tree.IsOpen(child)) {
|
||||
const NamedVal& nv = ValueTo<NamedVal>(tree.Get(child));
|
||||
if(!IsNull(result))
|
||||
result << ';';
|
||||
result << nv.name;
|
||||
String w = StoreTree(child);
|
||||
if(!IsNull(w))
|
||||
result << '{' << w << '}';
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void Pdb::SaveTree()
|
||||
{
|
||||
Value v = tree.Get(0);
|
||||
if(!IsType<NamedVal>(v))
|
||||
return;
|
||||
const NamedVal& nv = ValueTo<NamedVal>(v);
|
||||
if(nv.val.type < 0)
|
||||
return;
|
||||
String w;
|
||||
Point p = tree.GetScroll();
|
||||
w << p.x << ' ' << p.y << ' ' << tree.GetCursor() << ' ' << StoreTree(0);
|
||||
treetype.GetAdd(GetType(nv.val.type).name) = w;
|
||||
LOG("SaveTree " << GetType(nv.val.type).name << ' ' << w);
|
||||
}
|
||||
|
||||
void Pdb::ExpandTreeType(int parent, CParser& p)
|
||||
{
|
||||
for(;;) {
|
||||
String id;
|
||||
if(p.Char('*'))
|
||||
id = "*";
|
||||
if(p.IsId())
|
||||
id << p.ReadId();
|
||||
else
|
||||
break;
|
||||
for(;;)
|
||||
if(p.Char('<'))
|
||||
id << '<';
|
||||
else
|
||||
if(p.Char('>'))
|
||||
id << '>';
|
||||
else
|
||||
if(p.Char(','))
|
||||
id << ',';
|
||||
else
|
||||
if(p.IsInt())
|
||||
id << AsString(p.ReadInt());
|
||||
else
|
||||
if(p.IsString())
|
||||
id << '\"' << AsCString(p.ReadString()) << '\"';
|
||||
else
|
||||
if(p.IsId())
|
||||
id << p.ReadId();
|
||||
else
|
||||
break;
|
||||
int n = tree.GetChildCount(parent);
|
||||
for(int i = 0; i < n; i++) {
|
||||
int child = tree.GetChild(parent, i);
|
||||
const NamedVal& nv = ValueTo<NamedVal>(tree.Get(child));
|
||||
if(nv.name == id) {
|
||||
tree.Open(child);
|
||||
if(p.Char('{')) {
|
||||
ExpandTreeType(child, p);
|
||||
p.PassChar('}');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
p.Char(';');
|
||||
}
|
||||
}
|
||||
|
||||
void Pdb::SetTree(const String& exp)
|
||||
{
|
||||
SaveTree();
|
||||
tree.Clear();
|
||||
NamedVal nv;
|
||||
try {
|
||||
CParser p(exp);
|
||||
nv.val = Exp(p);
|
||||
}
|
||||
catch(CParser::Error) {
|
||||
return;
|
||||
}
|
||||
nv.name = exp;
|
||||
String n = exp;
|
||||
if(nv.val.type >= 0)
|
||||
n = GetType(nv.val.type).name;
|
||||
tree.SetRoot(Null, RawToValue(nv), n + '=' + Visualise(nv.val).GetString());
|
||||
if(nv.val.type >= 0) {
|
||||
String w = treetype.Get(n, Null);
|
||||
LOG("SetTree " << n << ' ' << w);
|
||||
tree.Open(0);
|
||||
CParser p(w);
|
||||
try {
|
||||
Point sc;
|
||||
sc.x = p.ReadInt();
|
||||
sc.y = p.ReadInt();
|
||||
int cursor = p.ReadInt();
|
||||
ExpandTreeType(0, p);
|
||||
tree.ScrollTo(sc);
|
||||
if(cursor >= 0)
|
||||
tree.SetCursor(cursor);
|
||||
}
|
||||
catch(CParser::Error) {}
|
||||
}
|
||||
}
|
||||
|
||||
void Pdb::SetTreeA(ArrayCtrl *array)
|
||||
{
|
||||
SetTree(array->Get(0));
|
||||
}
|
||||
|
||||
void Pdb::MemoryGoto(const String& exp)
|
||||
{
|
||||
CParser p(exp);
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ file
|
|||
Exp.cpp,
|
||||
Visualise.cpp,
|
||||
Data.cpp,
|
||||
Tree.cpp,
|
||||
Stack.cpp,
|
||||
Code.cpp,
|
||||
Pdb.cpp,
|
||||
|
|
|
|||
|
|
@ -208,6 +208,7 @@ Pdb::Pdb()
|
|||
locals.WhenEnterRow = THISBACK1(SetTreeA, &locals);
|
||||
locals.WhenBar = THISBACK(LocalsMenu);
|
||||
locals.WhenLeftDouble = THISBACK1(ExploreKey, &locals);
|
||||
locals.EvenRowColor();
|
||||
|
||||
self.NoHeader();
|
||||
self.AddColumn("", 1);
|
||||
|
|
@ -215,6 +216,7 @@ Pdb::Pdb()
|
|||
self.WhenEnterRow = THISBACK1(SetTreeA, &self);
|
||||
self.WhenBar = THISBACK(LocalsMenu);
|
||||
self.WhenLeftDouble = THISBACK1(ExploreKey, &self);
|
||||
self.EvenRowColor();
|
||||
|
||||
watches.NoHeader();
|
||||
watches.AddColumn("", 1).Edit(watchedit);
|
||||
|
|
@ -225,6 +227,7 @@ Pdb::Pdb()
|
|||
watches.WhenLeftDouble = THISBACK1(ExploreKey, &watches);
|
||||
watches.WhenAcceptEdit = THISBACK(Data);
|
||||
watches.WhenDrop = THISBACK(DropWatch);
|
||||
watches.EvenRowColor();
|
||||
|
||||
autos.NoHeader();
|
||||
autos.AddColumn("", 1);
|
||||
|
|
@ -232,6 +235,7 @@ Pdb::Pdb()
|
|||
autos.WhenEnterRow = THISBACK1(SetTreeA, &autos);
|
||||
autos.WhenBar = THISBACK(AutosMenu);
|
||||
autos.WhenLeftDouble = THISBACK1(ExploreKey, &autos);
|
||||
autos.EvenRowColor();
|
||||
|
||||
int c = EditField::GetStdHeight();
|
||||
explorer.AddColumn("", 1);
|
||||
|
|
@ -259,7 +263,7 @@ Pdb::Pdb()
|
|||
tab.Add(explorer_pane.SizePos(), "Explorer");
|
||||
tab.Add(cpu.SizePos(), "CPU");
|
||||
tab.Add(memory.SizePos(), "Memory");
|
||||
|
||||
|
||||
cpu.Columns(4);
|
||||
cpu.ItemHeight(Courier(Ctrl::HorzLayoutZoom(12)).GetCy());
|
||||
cpu.SetDisplay(Single<CpuRegisterDisplay>());
|
||||
|
|
|
|||
|
|
@ -328,6 +328,7 @@ struct Pdb : Debugger, ParentCtrl {
|
|||
Val Exp0(CParser& p);
|
||||
Val Exp(CParser& p);
|
||||
void CatInt(Visual& result, int64 val);
|
||||
void BaseFields(Visual& result, const Type& t, Pdb::Val val, int expandptr, int slen, bool& cm, int depth);
|
||||
void Visualise(Visual& result, Pdb::Val val, int expandptr, int slen);
|
||||
void Visualise(Visual& result, Pdb::Val val, int expandptr);
|
||||
Visual Visualise(Val v);
|
||||
|
|
|
|||
217
uppsrc/ide/Debuggers/Tree.cpp
Normal file
217
uppsrc/ide/Debuggers/Tree.cpp
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
#include "Debuggers.h"
|
||||
|
||||
#ifdef COMPILER_MSC
|
||||
|
||||
#define LLOG(x) // LOG(x)
|
||||
|
||||
void Pdb::TreeNode(int parent, const String& name, Pdb::Val val)
|
||||
{
|
||||
NamedVal nv;
|
||||
nv.name = name;
|
||||
nv.val = val;
|
||||
tree.Add(parent, Null, RawToValue(nv), name + "=" + Visualise(val).GetString(), val.type >= 0 || val.ref > 0);
|
||||
}
|
||||
|
||||
void Pdb::TreeExpand(int node)
|
||||
{
|
||||
if(tree.GetChildCount(node))
|
||||
return;
|
||||
Value v = tree.Get(node);
|
||||
if(!v.Is<NamedVal>())
|
||||
return;
|
||||
const NamedVal& nv = ValueTo<NamedVal>(tree.Get(node));
|
||||
Val val = nv.val;
|
||||
if(nv.val.ref > 0) {
|
||||
val = DeRef(val);
|
||||
if(val.type < 0 || val.ref > 0) {
|
||||
TreeNode(node, '*' + nv.name, val);
|
||||
SaveTree();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if(val.type < 0) {
|
||||
SaveTree();
|
||||
return;
|
||||
}
|
||||
const Type& t = GetType(val.type);
|
||||
if(t.vtbl_typeindex == -2) {
|
||||
int count = GetSymInfo(t.modbase, type.GetKey(val.type), TI_GET_COUNT);
|
||||
Val prtti;
|
||||
prtti.ref = 1;
|
||||
prtti.type = UINT4;
|
||||
prtti.address = val.address - 4;
|
||||
Val rtti = GetRVal(prtti);
|
||||
FnInfo rtf = GetFnInfo(rtti.address);
|
||||
TreeNode(node, rtf.name, prtti);
|
||||
for(int i = 0; i < count; i++) {
|
||||
Val ventry;
|
||||
ventry.type = PFUNC;
|
||||
ventry.address = val.address + 4 * i;
|
||||
TreeNode(node, NFormat("[%d]", i), ventry);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if(t.vtbl_typeindex >= 0) {
|
||||
Val vtbl;
|
||||
vtbl.ref = 1;
|
||||
vtbl.address = val.address + t.vtbl_offset;
|
||||
vtbl.type = t.vtbl_typeindex;
|
||||
TreeNode(node, "virtual", vtbl);
|
||||
/*
|
||||
Val vloc = GetRVal(vtbl);
|
||||
FnInfo vtbl_type = GetFnInfo(vloc.address);
|
||||
const char *p = vtbl_type.name, *e = p;
|
||||
while(*e && !(*e == ':' && e[1] == ':'))
|
||||
e++;
|
||||
String fullsym(p, e);
|
||||
FnInfo ffs = GetFnInfo(fullsym);
|
||||
if(ffs.pdbtype) {
|
||||
Val typeval;
|
||||
typeval.address = val.address;
|
||||
TypeVal(typeval, ffs.pdbtype, t.modbase);
|
||||
TreeNode(node, String().Cat() << '[' << ffs.name << ']', typeval);
|
||||
}
|
||||
*/
|
||||
}
|
||||
for(int i = 0; i < t.base.GetCount(); i++) {
|
||||
Val r = t.base[i];
|
||||
r.address += val.address;
|
||||
if(r.type >= 0) {
|
||||
const Type& bt = GetType(r.type);
|
||||
TreeNode(node, bt.name, r);
|
||||
}
|
||||
}
|
||||
for(int i = 0; i < t.member.GetCount(); i++) {
|
||||
Val r = t.member[i];
|
||||
r.address += val.address;
|
||||
TreeNode(node, t.member.GetKey(i), r);
|
||||
}
|
||||
for(int i = 0; i < t.static_member.GetCount(); i++) {
|
||||
Val r = t.static_member[i];
|
||||
TreeNode(node, t.static_member.GetKey(i), r);
|
||||
}
|
||||
}
|
||||
|
||||
String Pdb::StoreTree(int parent)
|
||||
{
|
||||
String result;
|
||||
int n = tree.GetChildCount(parent);
|
||||
for(int i = 0; i < n; i++) {
|
||||
int child = tree.GetChild(parent, i);
|
||||
if(tree.IsOpen(child)) {
|
||||
const NamedVal& nv = ValueTo<NamedVal>(tree.Get(child));
|
||||
if(!IsNull(result))
|
||||
result << ';';
|
||||
result << nv.name;
|
||||
String w = StoreTree(child);
|
||||
if(!IsNull(w))
|
||||
result << '{' << w << '}';
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void Pdb::SaveTree()
|
||||
{
|
||||
Value v = tree.Get(0);
|
||||
if(!IsType<NamedVal>(v))
|
||||
return;
|
||||
const NamedVal& nv = ValueTo<NamedVal>(v);
|
||||
if(nv.val.type < 0)
|
||||
return;
|
||||
String w;
|
||||
Point p = tree.GetScroll();
|
||||
w << p.x << ' ' << p.y << ' ' << tree.GetCursor() << ' ' << StoreTree(0);
|
||||
treetype.GetAdd(GetType(nv.val.type).name) = w;
|
||||
LOG("SaveTree " << GetType(nv.val.type).name << ' ' << w);
|
||||
}
|
||||
|
||||
void Pdb::ExpandTreeType(int parent, CParser& p)
|
||||
{
|
||||
for(;;) {
|
||||
String id;
|
||||
if(p.Char('*'))
|
||||
id = "*";
|
||||
if(p.IsId())
|
||||
id << p.ReadId();
|
||||
else
|
||||
break;
|
||||
for(;;)
|
||||
if(p.Char('<'))
|
||||
id << '<';
|
||||
else
|
||||
if(p.Char('>'))
|
||||
id << '>';
|
||||
else
|
||||
if(p.Char(','))
|
||||
id << ',';
|
||||
else
|
||||
if(p.IsInt())
|
||||
id << AsString(p.ReadInt());
|
||||
else
|
||||
if(p.IsString())
|
||||
id << '\"' << AsCString(p.ReadString()) << '\"';
|
||||
else
|
||||
if(p.IsId())
|
||||
id << p.ReadId();
|
||||
else
|
||||
break;
|
||||
int n = tree.GetChildCount(parent);
|
||||
for(int i = 0; i < n; i++) {
|
||||
int child = tree.GetChild(parent, i);
|
||||
const NamedVal& nv = ValueTo<NamedVal>(tree.Get(child));
|
||||
if(nv.name == id) {
|
||||
tree.Open(child);
|
||||
if(p.Char('{')) {
|
||||
ExpandTreeType(child, p);
|
||||
p.PassChar('}');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
p.Char(';');
|
||||
}
|
||||
}
|
||||
|
||||
void Pdb::SetTree(const String& exp)
|
||||
{
|
||||
SaveTree();
|
||||
tree.Clear();
|
||||
NamedVal nv;
|
||||
try {
|
||||
CParser p(exp);
|
||||
nv.val = Exp(p);
|
||||
}
|
||||
catch(CParser::Error) {
|
||||
return;
|
||||
}
|
||||
nv.name = exp;
|
||||
String n = exp;
|
||||
if(nv.val.type >= 0)
|
||||
n = GetType(nv.val.type).name;
|
||||
tree.SetRoot(Null, RawToValue(nv), n + '=' + Visualise(nv.val).GetString());
|
||||
if(nv.val.type >= 0) {
|
||||
String w = treetype.Get(n, Null);
|
||||
LOG("SetTree " << n << ' ' << w);
|
||||
tree.Open(0);
|
||||
CParser p(w);
|
||||
try {
|
||||
Point sc;
|
||||
sc.x = p.ReadInt();
|
||||
sc.y = p.ReadInt();
|
||||
int cursor = p.ReadInt();
|
||||
ExpandTreeType(0, p);
|
||||
tree.ScrollTo(sc);
|
||||
if(cursor >= 0)
|
||||
tree.SetCursor(cursor);
|
||||
}
|
||||
catch(CParser::Error) {}
|
||||
}
|
||||
}
|
||||
|
||||
void Pdb::SetTreeA(ArrayCtrl *array)
|
||||
{
|
||||
SetTree(array->Get(0));
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -161,6 +161,12 @@ void Pdb::Visualise(Visual& result, Pdb::Val val, int expandptr, int slen)
|
|||
result.Cat(e, SColorDisabled);
|
||||
}
|
||||
}
|
||||
BaseFields(result, t, val, expandptr, slen, cm, 0);
|
||||
result.Cat(" }", SColorMark);
|
||||
}
|
||||
|
||||
void Pdb::BaseFields(Visual& result, const Type& t, Pdb::Val val, int expandptr, int slen, bool& cm, int depth)
|
||||
{
|
||||
for(int i = 0; i < t.base.GetCount(); i++) {
|
||||
const Val& b = t.base[i];
|
||||
if(b.type >= 0) {
|
||||
|
|
@ -170,7 +176,7 @@ void Pdb::Visualise(Visual& result, Pdb::Val val, int expandptr, int slen)
|
|||
if(cm)
|
||||
result.Cat(", ");
|
||||
cm = true;
|
||||
if(result.length > maxlen) {
|
||||
if(result.length > 300) {
|
||||
result.Cat("..");
|
||||
break;
|
||||
}
|
||||
|
|
@ -185,9 +191,10 @@ void Pdb::Visualise(Visual& result, Pdb::Val val, int expandptr, int slen)
|
|||
result.Cat(e, SColorDisabled);
|
||||
}
|
||||
}
|
||||
if(depth < 30)
|
||||
BaseFields(result, t, val, expandptr, slen, cm, depth + 1);
|
||||
}
|
||||
}
|
||||
result.Cat(" }", SColorMark);
|
||||
}
|
||||
|
||||
Size Pdb::Visual::GetSize() const
|
||||
|
|
@ -205,8 +212,7 @@ void Pdb::Visualise(Visual& result, Pdb::Val val, int expandptr)
|
|||
int h = 300;
|
||||
for(int i = 0; i < 8; i++) {
|
||||
int slen = (l + h) / 2;
|
||||
result.length = 0;
|
||||
result.part.Clear();
|
||||
result.Clear();
|
||||
Visualise(result, val, expandptr, slen);
|
||||
int x = result.GetSize().cx;
|
||||
if(x < cx)
|
||||
|
|
@ -215,7 +221,7 @@ void Pdb::Visualise(Visual& result, Pdb::Val val, int expandptr)
|
|||
h = slen;
|
||||
if(l + 1 >= h)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Pdb::Visual Pdb::Visualise(Val v)
|
||||
|
|
|
|||
|
|
@ -763,9 +763,7 @@ void AppMain___()
|
|||
return;
|
||||
String upp_dir = GetFileFolder(GetExeFilePath());
|
||||
String cf = GetExeDirFile("setup-path");
|
||||
#ifndef _DEBUG
|
||||
if(upp_dir != LoadFile(cf))
|
||||
#endif
|
||||
{
|
||||
InstantSetup();
|
||||
SaveFile(cf, upp_dir);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue