mirror of
https://github.com/eclipse-cdt/cdt
synced 2025-04-29 19:45:01 +02:00
Manage the removal of an include guard
This commit is contained in:
parent
333559e5ea
commit
098712b4d9
6 changed files with 214 additions and 77 deletions
|
@ -61,4 +61,10 @@ public interface IIndexFragmentFile extends IIndexFile {
|
|||
* The file 'source' must belong to the same fragment as this file.
|
||||
*/
|
||||
void transferIncluders(IIndexFragmentFile source) throws CoreException;
|
||||
|
||||
/**
|
||||
* Changes the inclusion from the context of 'source' to point to this file, instead.
|
||||
* The file 'source' must belong to the same fragment as this file.
|
||||
*/
|
||||
void transferContext(IIndexFragmentFile source) throws CoreException;
|
||||
}
|
||||
|
|
|
@ -59,6 +59,12 @@ public interface IWritableIndex extends IIndex {
|
|||
IIndexFragmentFile getWritableFile(int linkageID, IIndexFileLocation location,
|
||||
ISignificantMacros macroDictionary) throws CoreException;
|
||||
|
||||
/**
|
||||
* Returns the writable files for the given location and linkage. This method
|
||||
* returns file objects without content, also.
|
||||
*/
|
||||
IIndexFragmentFile[] getWritableFiles(int linkageID, IIndexFileLocation location) throws CoreException;
|
||||
|
||||
/**
|
||||
* Returns the writable files for the given location in any linkage. This method
|
||||
* returns file objects without content, also.
|
||||
|
@ -178,4 +184,10 @@ public interface IWritableIndex extends IIndex {
|
|||
* Both files must belong to the writable fragment.
|
||||
*/
|
||||
void transferIncluders(IIndexFragmentFile source, IIndexFragmentFile target) throws CoreException;
|
||||
|
||||
/**
|
||||
* Changes the inclusion from the context of 'source' to point to 'target', instead.
|
||||
* Both files must belong to the writable fragment.
|
||||
*/
|
||||
void transferContext(IIndexFragmentFile source, IIndexFragmentFile target) throws CoreException;
|
||||
}
|
||||
|
|
|
@ -46,7 +46,11 @@ public class WritableCIndex extends CIndex implements IWritableIndex {
|
|||
ISignificantMacros macroDictionary) throws CoreException {
|
||||
return fWritableFragment.getFile(linkageID, location, macroDictionary);
|
||||
}
|
||||
|
||||
|
||||
public IIndexFragmentFile[] getWritableFiles(int linkageID, IIndexFileLocation location) throws CoreException {
|
||||
return fWritableFragment.getFiles(linkageID, location);
|
||||
}
|
||||
|
||||
public IIndexFragmentFile[] getWritableFiles(IIndexFileLocation location) throws CoreException {
|
||||
return fWritableFragment.getFiles(location);
|
||||
}
|
||||
|
@ -185,4 +189,12 @@ public class WritableCIndex extends CIndex implements IWritableIndex {
|
|||
return;
|
||||
target.transferIncluders(source);
|
||||
}
|
||||
|
||||
public void transferContext(IIndexFragmentFile source, IIndexFragmentFile target) throws CoreException {
|
||||
if (source == null || target == null || !isWritableFile(source) || !isWritableFile(target))
|
||||
throw new IllegalArgumentException();
|
||||
if (source.equals(target))
|
||||
return;
|
||||
target.transferContext(source);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,10 +20,10 @@ import java.util.Collections;
|
|||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.cdt.core.CCorePlugin;
|
||||
import org.eclipse.cdt.core.dom.IPDOMIndexerTask;
|
||||
|
@ -802,7 +802,7 @@ public abstract class AbstractIndexerTask extends PDOMWriter {
|
|||
if (monitor.isCanceled() || hasUrgentTasks())
|
||||
return;
|
||||
parseVersionInContext(linkageID, map, ifl, versionTask, locTask.fTu,
|
||||
new HashSet<IIndexFile>(), monitor);
|
||||
new LinkedHashSet<IIndexFile>(), monitor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -857,19 +857,60 @@ public abstract class AbstractIndexerTask extends PDOMWriter {
|
|||
}
|
||||
|
||||
private void parseVersionInContext(int linkageID, LinkageTask map, IIndexFileLocation ifl,
|
||||
final FileVersionTask versionTask, Object tu, Set<IIndexFile> safeGuard,
|
||||
final FileVersionTask versionTask, Object tu, LinkedHashSet<IIndexFile> safeGuard,
|
||||
IProgressMonitor monitor) throws CoreException, InterruptedException {
|
||||
final IIndexFragmentFile headerFile = versionTask.fIndexFile;
|
||||
IIndexFragmentFile ctx= headerFile;
|
||||
|
||||
final int safeguardSize= safeGuard.size();
|
||||
for(;;) {
|
||||
IIndexInclude ctxInclude= ctx.getParsedInContext();
|
||||
if (ctxInclude == null)
|
||||
break;
|
||||
|
||||
final IIndexFragmentFile nextCtx= (IIndexFragmentFile) ctxInclude.getIncludedBy();
|
||||
if (!fIndex.isWritableFile(nextCtx) || !safeGuard.add(nextCtx))
|
||||
break;
|
||||
// Look for a context and parse the file
|
||||
IIndexFragmentFile ctxFile = findContextFile(linkageID, map, versionTask, safeGuard, monitor);
|
||||
if (ctxFile == null || ctxFile == headerFile)
|
||||
return;
|
||||
|
||||
Object contextTu= fResolver.getInputFile(ctxFile.getLocation());
|
||||
if (contextTu == null)
|
||||
return;
|
||||
|
||||
final IScannerInfo scannerInfo= fResolver.getBuildConfiguration(linkageID, contextTu);
|
||||
FileContext ctx= new FileContext(ctxFile, headerFile);
|
||||
parseFile(tu, linkageID, ifl, scannerInfo, ctx, monitor);
|
||||
if (!ctx.fLostPragmaOnceSemantics)
|
||||
return;
|
||||
|
||||
// Try the next context
|
||||
restoreSet(safeGuard, safeguardSize);
|
||||
}
|
||||
}
|
||||
|
||||
private void restoreSet(LinkedHashSet<?> set, int restoreSize) {
|
||||
for (Iterator<?> it = set.iterator(); it.hasNext();) {
|
||||
it.next();
|
||||
if (restoreSize == 0) {
|
||||
it.remove();
|
||||
} else {
|
||||
restoreSize--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IIndexFragmentFile findContextFile(int linkageID, LinkageTask map,
|
||||
final FileVersionTask versionTask, LinkedHashSet<IIndexFile> safeGuard, IProgressMonitor monitor)
|
||||
throws CoreException, InterruptedException {
|
||||
IIndexFragmentFile ctxFile= versionTask.fIndexFile;
|
||||
for(;;) {
|
||||
IIndexInclude ctxInclude= ctxFile.getParsedInContext();
|
||||
if (ctxInclude == null)
|
||||
return ctxFile;
|
||||
|
||||
IIndexFragmentFile nextCtx= (IIndexFragmentFile) ctxInclude.getIncludedBy();
|
||||
if (!fIndex.isWritableFile(nextCtx))
|
||||
return ctxFile;
|
||||
|
||||
// Found a recursion
|
||||
if (!safeGuard.add(nextCtx))
|
||||
return null;
|
||||
|
||||
final IIndexFileLocation ctxIfl = nextCtx.getLocation();
|
||||
LocationTask ctxTask= map.find(ctxIfl);
|
||||
if (ctxTask != null) {
|
||||
|
@ -880,30 +921,19 @@ public abstract class AbstractIndexerTask extends PDOMWriter {
|
|||
safeGuard, monitor);
|
||||
if (ctxVersionTask.fOutdated // This is unexpected.
|
||||
|| !versionTask.fOutdated) // Our file was parsed.
|
||||
return;
|
||||
|
||||
return null;
|
||||
|
||||
// The file is no longer a context, look for a different one.
|
||||
ctxInclude= ctx.getParsedInContext();
|
||||
continue;
|
||||
nextCtx= ctxFile;
|
||||
}
|
||||
}
|
||||
ctx= nextCtx;
|
||||
ctxFile= nextCtx;
|
||||
}
|
||||
|
||||
// See if we found a context and parse the file
|
||||
if (ctx != headerFile) {
|
||||
Object contextTu= fResolver.getInputFile(ctx.getLocation());
|
||||
if (contextTu != null) {
|
||||
final IScannerInfo scannerInfo= fResolver.getBuildConfiguration(linkageID, contextTu);
|
||||
IIndexFragmentFile[] ctx2header= {ctx, headerFile};
|
||||
parseFile(tu, linkageID, ifl, scannerInfo, ctx2header, monitor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void parseFile(Object tu, int linkageID, IIndexFileLocation ifl, IScannerInfo scanInfo,
|
||||
IIndexFragmentFile[] ctx2header, IProgressMonitor pm) throws CoreException, InterruptedException {
|
||||
FileContext ctx, IProgressMonitor pm) throws CoreException, InterruptedException {
|
||||
IPath path= getLabel(ifl);
|
||||
AbstractLanguage[] langs= fResolver.getLanguages(tu, true);
|
||||
AbstractLanguage lang= null;
|
||||
|
@ -926,11 +956,12 @@ public abstract class AbstractIndexerTask extends PDOMWriter {
|
|||
path.lastSegment(), path.removeLastSegments(1).toString()));
|
||||
long start= System.currentTimeMillis();
|
||||
FileContent codeReader= fResolver.getCodeReader(tu);
|
||||
IASTTranslationUnit ast= createAST(tu, lang, codeReader, scanInfo, fASTOptions, ctx2header, pm);
|
||||
IIndexFile[] ctxFiles = ctx == null ? null : new IIndexFile[] {ctx.fContext, ctx.fOldFile};
|
||||
|
||||
IASTTranslationUnit ast= createAST(tu, lang, codeReader, scanInfo, fASTOptions, ctxFiles, pm);
|
||||
fStatistics.fParsingTime += System.currentTimeMillis() - start;
|
||||
if (ast != null) {
|
||||
IIndexFragmentFile rewrite= ctx2header == null ? null : ctx2header[1];
|
||||
writeToIndex(linkageID, ast, codeReader.getContentsHash(), rewrite, pm);
|
||||
writeToIndex(linkageID, ast, codeReader.getContentsHash(), ctx, pm);
|
||||
}
|
||||
} catch (CoreException e) {
|
||||
th= e;
|
||||
|
@ -1055,7 +1086,7 @@ public abstract class AbstractIndexerTask extends PDOMWriter {
|
|||
}
|
||||
|
||||
private void writeToIndex(final int linkageID, IASTTranslationUnit ast, long fileContentsHash,
|
||||
IIndexFragmentFile replace, IProgressMonitor pm) throws CoreException, InterruptedException {
|
||||
FileContext ctx, IProgressMonitor pm) throws CoreException, InterruptedException {
|
||||
HashSet<FileContentKey> enteredFiles= new HashSet<FileContentKey>();
|
||||
ArrayList<FileInAST> orderedFileKeys= new ArrayList<FileInAST>();
|
||||
|
||||
|
@ -1068,13 +1099,20 @@ public abstract class AbstractIndexerTask extends PDOMWriter {
|
|||
collectOrderedFileKeys(linkageID, inclusion, enteredFiles, orderedFileKeys);
|
||||
}
|
||||
|
||||
if (replace != null || needToStoreInIndex(linkageID, topIfl, ast.getSignificantMacros())) {
|
||||
IIndexFile newFile= selectIndexFile(linkageID, topIfl, ast.getSignificantMacros());
|
||||
if (ctx != null) {
|
||||
orderedFileKeys.add(new FileInAST(null, topKey, fileContentsHash));
|
||||
if (newFile != null && fIndex.isWritableFile(newFile)) {
|
||||
// File can be reused
|
||||
ctx.fNewFile= (IIndexFragmentFile) newFile;
|
||||
}
|
||||
} else if (newFile == null) {
|
||||
orderedFileKeys.add(new FileInAST(null, topKey, fileContentsHash));
|
||||
}
|
||||
|
||||
|
||||
FileInAST[] fileKeys= orderedFileKeys.toArray(new FileInAST[orderedFileKeys.size()]);
|
||||
try {
|
||||
addSymbols(ast, fileKeys, fIndex, false, replace, fTodoTaskUpdater, pm);
|
||||
addSymbols(ast, fileKeys, fIndex, false, ctx, fTodoTaskUpdater, pm);
|
||||
} catch (CoreException e) {
|
||||
// Avoid parsing files again, that caused an exception to be thrown.
|
||||
withdrawRequests(linkageID, fileKeys);
|
||||
|
@ -1099,37 +1137,12 @@ public abstract class AbstractIndexerTask extends PDOMWriter {
|
|||
for (IASTInclusionNode element : nested) {
|
||||
collectOrderedFileKeys(linkageID, element, enteredFiles, orderedFileKeys);
|
||||
}
|
||||
if (isFirstEntry && needToStoreInIndex(linkageID, ifl, include.getSignificantMacros())) {
|
||||
if (isFirstEntry && selectIndexFile(linkageID, ifl, include.getSignificantMacros()) == null) {
|
||||
orderedFileKeys.add(new FileInAST(include, fileKey, include.getContentsHash()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean needToStoreInIndex(int linkageID, IIndexFileLocation ifl, ISignificantMacros sigMacros) throws CoreException {
|
||||
LinkageTask map = findRequestMap(linkageID);
|
||||
if (map != null) {
|
||||
LocationTask locTask= map.find(ifl);
|
||||
if (locTask != null) {
|
||||
FileVersionTask task = locTask.findVersion(sigMacros);
|
||||
if (task != null) {
|
||||
return task.fOutdated;
|
||||
}
|
||||
}
|
||||
}
|
||||
IIndexFile ifile= null;
|
||||
if (fResolver.canBePartOfSDK(ifl)) {
|
||||
// Check for a version in potentially another pdom.
|
||||
ifile = fIndex.getFile(linkageID, ifl, sigMacros);
|
||||
} else {
|
||||
// Search the writable PDOM, only.
|
||||
IIndexFragmentFile fragFile = fIndex.getWritableFile(linkageID, ifl, sigMacros);
|
||||
if (fragFile != null && fragFile.hasContent()) {
|
||||
ifile= fragFile;
|
||||
}
|
||||
}
|
||||
return ifile == null;
|
||||
}
|
||||
|
||||
private void withdrawRequests(int linkageID, FileInAST[] fileKeys) {
|
||||
LinkageTask map = findRequestMap(linkageID);
|
||||
if (map != null) {
|
||||
|
@ -1171,6 +1184,26 @@ public abstract class AbstractIndexerTask extends PDOMWriter {
|
|||
return fc;
|
||||
}
|
||||
|
||||
IIndexFile selectIndexFile(int linkageID, IIndexFileLocation ifl, ISignificantMacros sigMacros) throws CoreException {
|
||||
LinkageTask map = findRequestMap(linkageID);
|
||||
if (map != null) {
|
||||
LocationTask locTask= map.find(ifl);
|
||||
if (locTask != null) {
|
||||
FileVersionTask task = locTask.findVersion(sigMacros);
|
||||
if (task != null) {
|
||||
return task.fOutdated ? null : task.fIndexFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IIndexFile[] files = getAvailableIndexFiles(linkageID, ifl);
|
||||
for (IIndexFile file : files) {
|
||||
if (sigMacros.equals(file.getSignificantMacros()))
|
||||
return file;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public IIndexFile selectIndexFile(int linkageID, IIndexFileLocation ifl, IMacroDictionary md) throws CoreException {
|
||||
LinkageTask map = findRequestMap(linkageID);
|
||||
if (map != null) {
|
||||
|
@ -1187,11 +1220,7 @@ public abstract class AbstractIndexerTask extends PDOMWriter {
|
|||
}
|
||||
}
|
||||
|
||||
IIndexFile[] files= fIndexFilesCache.get(ifl);
|
||||
if (files == null) {
|
||||
files= fIndex.getFiles(linkageID, ifl);
|
||||
fIndexFilesCache.put(ifl, files);
|
||||
}
|
||||
IIndexFile[] files = getAvailableIndexFiles(linkageID, ifl);
|
||||
for (IIndexFile indexFile : files) {
|
||||
if (md.satisfies(indexFile.getSignificantMacros())) {
|
||||
return indexFile;
|
||||
|
@ -1199,4 +1228,33 @@ public abstract class AbstractIndexerTask extends PDOMWriter {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public IIndexFile[] getAvailableIndexFiles(int linkageID, IIndexFileLocation ifl)
|
||||
throws CoreException {
|
||||
IIndexFile[] files= fIndexFilesCache.get(ifl);
|
||||
if (files == null) {
|
||||
if (fResolver.canBePartOfSDK(ifl)) {
|
||||
// Check for a version in potentially another pdom.
|
||||
files= fIndex.getFiles(linkageID, ifl);
|
||||
} else {
|
||||
IIndexFragmentFile[] fragFiles = fIndex.getWritableFiles(linkageID, ifl);
|
||||
int j= 0;
|
||||
for (int i = 0; i < fragFiles.length; i++) {
|
||||
if (fragFiles[i].hasContent()) {
|
||||
if (j != i)
|
||||
fragFiles[j]= fragFiles[i];
|
||||
j++;
|
||||
}
|
||||
}
|
||||
if (j == fragFiles.length) {
|
||||
files= fragFiles;
|
||||
} else {
|
||||
files= new IIndexFile[j];
|
||||
System.arraycopy(fragFiles, 0, files, 0, j);
|
||||
}
|
||||
}
|
||||
fIndexFilesCache.put(ifl, files);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,6 +87,19 @@ abstract public class PDOMWriter {
|
|||
return fFileContentKey.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public static class FileContext {
|
||||
final IIndexFragmentFile fContext;
|
||||
final IIndexFragmentFile fOldFile;
|
||||
IIndexFragmentFile fNewFile;
|
||||
public boolean fLostPragmaOnceSemantics;
|
||||
|
||||
public FileContext(IIndexFragmentFile context, IIndexFragmentFile oldFile) {
|
||||
fContext= context;
|
||||
fOldFile= oldFile;
|
||||
fNewFile= null;
|
||||
}
|
||||
}
|
||||
|
||||
public static int SKIP_ALL_REFERENCES= -1;
|
||||
public static int SKIP_TYPE_REFERENCES= 1;
|
||||
|
@ -174,7 +187,7 @@ abstract public class PDOMWriter {
|
|||
* the index after your last write operation.
|
||||
*/
|
||||
final protected void addSymbols(IASTTranslationUnit ast, FileInAST[] selectedFiles,
|
||||
IWritableIndex index, boolean flushIndex, IIndexFragmentFile replaceFile,
|
||||
IWritableIndex index, boolean flushIndex, FileContext ctx,
|
||||
ITodoTaskUpdater taskUpdater, IProgressMonitor pm) throws InterruptedException,
|
||||
CoreException {
|
||||
if (fShowProblems) {
|
||||
|
@ -196,7 +209,7 @@ abstract public class PDOMWriter {
|
|||
resolveNames(data, pm);
|
||||
|
||||
// Index update
|
||||
storeSymbolsInIndex(data, replaceFile, flushIndex, pm);
|
||||
storeSymbolsInIndex(data, ctx, flushIndex, pm);
|
||||
|
||||
// Tasks update
|
||||
if (taskUpdater != null) {
|
||||
|
@ -228,8 +241,9 @@ abstract public class PDOMWriter {
|
|||
}
|
||||
}
|
||||
|
||||
private void storeSymbolsInIndex(final Data data, IIndexFragmentFile replaceFile, boolean flushIndex, IProgressMonitor pm)
|
||||
private void storeSymbolsInIndex(final Data data, FileContext ctx, boolean flushIndex, IProgressMonitor pm)
|
||||
throws InterruptedException, CoreException {
|
||||
final IIndexFragmentFile newFile= ctx == null ? null : ctx.fNewFile;
|
||||
final int linkageID= data.fAST.getLinkage().getLinkageID();
|
||||
for (int i= 0; i < data.fSelectedFiles.length; i++) {
|
||||
if (pm.isCanceled())
|
||||
|
@ -244,14 +258,27 @@ abstract public class PDOMWriter {
|
|||
YieldableIndexLock lock = new YieldableIndexLock(data.fIndex, flushIndex);
|
||||
lock.acquire();
|
||||
try {
|
||||
IIndexFragmentFile ifile= storeFileInIndex(data, fileInAST, linkageID, lock);
|
||||
if (fileInAST.fIncludeStatement == null && replaceFile != null && !replaceFile.equals(ifile)) {
|
||||
data.fIndex.transferIncluders(replaceFile, ifile);
|
||||
final boolean isReplacement= ctx != null && fileInAST.fIncludeStatement == null;
|
||||
IIndexFragmentFile ifile= null;
|
||||
if (!isReplacement || newFile == null) {
|
||||
ifile= storeFileInIndex(data, fileInAST, linkageID, lock);
|
||||
reportFileWrittenToIndex(fileInAST, ifile);
|
||||
} else {
|
||||
reportFileWrittenToIndex(fileInAST, ifile);
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
}
|
||||
|
||||
if (isReplacement) {
|
||||
if (ifile == null)
|
||||
ifile= newFile;
|
||||
if (ctx != null && !ctx.fOldFile.equals(ifile) && ifile != null) {
|
||||
if (ctx.fOldFile.hasPragmaOnceSemantics() &&
|
||||
!ifile.hasPragmaOnceSemantics()) {
|
||||
data.fIndex.transferContext(ctx.fOldFile, ifile);
|
||||
ctx.fLostPragmaOnceSemantics= true;
|
||||
} else {
|
||||
data.fIndex.transferIncluders(ctx.fOldFile, ifile);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
th= e;
|
||||
} catch (StackOverflowError e) {
|
||||
th= e;
|
||||
|
|
|
@ -219,9 +219,13 @@ public class PDOMFile implements IIndexFragmentFile {
|
|||
PDOMFile source= (PDOMFile) sourceFile;
|
||||
PDOMInclude include = source.getFirstIncludedBy();
|
||||
if (include != null) {
|
||||
// Detach the includes
|
||||
source.setFirstIncludedBy(null);
|
||||
// Adjust the includes
|
||||
for (PDOMInclude i=include; i != null; i= i.getNextInIncludedBy()) {
|
||||
i.setIncludes(this);
|
||||
}
|
||||
// Append the includes
|
||||
PDOMInclude last= getFirstIncludedBy();
|
||||
if (last == null) {
|
||||
setFirstIncludedBy(include);
|
||||
|
@ -233,7 +237,25 @@ public class PDOMFile implements IIndexFragmentFile {
|
|||
include.setPrevInIncludedBy(last);
|
||||
}
|
||||
}
|
||||
source.setFirstIncludedBy(null);
|
||||
}
|
||||
|
||||
public void transferContext(IIndexFragmentFile sourceFile) throws CoreException {
|
||||
PDOMFile source= (PDOMFile) sourceFile;
|
||||
PDOMInclude include = source.getFirstIncludedBy();
|
||||
if (include != null) {
|
||||
// Detach the include
|
||||
final PDOMInclude next = include.getNextInIncludedBy();
|
||||
include.setNextInIncludedBy(null);
|
||||
source.setFirstIncludedBy(next);
|
||||
if (next != null)
|
||||
next.setPrevInIncludedBy(null);
|
||||
|
||||
// Adjust the include
|
||||
include.setIncludes(this);
|
||||
|
||||
// Insert the include
|
||||
addIncludedBy(include, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Add table
Reference in a new issue