/*******************************************************************************
* Copyright (c) 2008, 2011 Wind River Systems, Inc. and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Anton Leherbauer (Wind River Systems) - initial API and implementation
* Jens Elmenthaler - http://bugs.eclipse.org/173458 (camel case completion)
*******************************************************************************/
package org.eclipse.cdt.internal.ui.text.contentassist;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IResourceProxy;
import org.eclipse.core.resources.IResourceProxyVisitor;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.ITypedRegion;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.swt.graphics.Image;
import org.eclipse.cdt.core.model.CoreModel;
import org.eclipse.cdt.core.model.IInclude;
import org.eclipse.cdt.core.model.ITranslationUnit;
import org.eclipse.cdt.core.parser.IExtendedScannerInfo;
import org.eclipse.cdt.core.parser.IScannerInfo;
import org.eclipse.cdt.core.parser.util.IContentAssistMatcher;
import org.eclipse.cdt.ui.CUIPlugin;
import org.eclipse.cdt.ui.text.ICPartitions;
import org.eclipse.cdt.ui.text.contentassist.ContentAssistInvocationContext;
import org.eclipse.cdt.ui.text.contentassist.ICompletionProposalComputer;
import org.eclipse.cdt.internal.core.parser.util.ContentAssistMatcherFactory;
import org.eclipse.cdt.internal.ui.viewsupport.CElementImageProvider;
/**
* A proposal computer for include directives.
*
* @since 5.0
*/
public class InclusionProposalComputer implements ICompletionProposalComputer {
private String fErrorMessage;
public List<ICompletionProposal> computeCompletionProposals(ContentAssistInvocationContext context, IProgressMonitor monitor) {
List<ICompletionProposal> proposals= Collections.emptyList();
fErrorMessage= null;
if (context instanceof CContentAssistInvocationContext) {
CContentAssistInvocationContext cContext= (CContentAssistInvocationContext) context;
if (inIncludeDirective(cContext)) {
// add include file proposals
proposals= new ArrayList<ICompletionProposal>();
try {
addInclusionProposals(cContext, proposals);
} catch (Exception exc) {
fErrorMessage= exc.getMessage();
CUIPlugin.log(exc);
}
}
}
return proposals;
}
public List<IContextInformation> computeContextInformation(ContentAssistInvocationContext context, IProgressMonitor monitor) {
return null;
}
public String getErrorMessage() {
return fErrorMessage;
}
public void sessionEnded() {
}
public void sessionStarted() {
}
/**
* Test whether the invocation offset is inside the file name part if an include directive.
*
* @param context the invocation context
* @return <code>true</code> if the invocation offset is inside or before the directive keyword
*/
private boolean inIncludeDirective(CContentAssistInvocationContext context) {
IDocument doc = context.getDocument();
int offset = context.getInvocationOffset();
try {
final ITypedRegion partition= TextUtilities.getPartition(doc, ICPartitions.C_PARTITIONING, offset, true);
if (ICPartitions.C_PREPROCESSOR.equals(partition.getType())) {
String ppPrefix= doc.get(partition.getOffset(), offset - partition.getOffset());
if (ppPrefix.matches("\\s*#\\s*include\\s*[\"<][^\">]*")) { //$NON-NLS-1$
// we are inside the file name part of the include directive
return true;
}
}
} catch (BadLocationException exc) {
}
return false;
}
private void addInclusionProposals(CContentAssistInvocationContext context, List<ICompletionProposal> proposals) throws Exception {
if (context.isContextInformationStyle()) {
return;
}
final ITranslationUnit tu= context.getTranslationUnit();
if (tu == null) {
return;
}
String prefix;
boolean angleBrackets= false;
prefix = computeIncludePrefix(context);
if (prefix.length() > 0) {
angleBrackets= prefix.charAt(0) == '<';
prefix= prefix.substring(1);
}
IPath prefixPath= new Path(prefix);
String[] potentialIncludes= collectIncludeFiles(tu, prefixPath, angleBrackets);
if (potentialIncludes.length > 0) {
IInclude[] includes= tu.getIncludes();
Set<String> alreadyIncluded= new HashSet<String>();
for (IInclude includeDirective : includes) {
alreadyIncluded.add(includeDirective.getElementName());
}
Image image = getImage(CElementImageProvider.getIncludeImageDescriptor());
for (String include : potentialIncludes) {
if (alreadyIncluded.add(include)) {
final char openingBracket= angleBrackets ? '<' : '"';
final char closingBracket= angleBrackets ? '>' : '"';
String repString= openingBracket + include;
final String dispString= repString + closingBracket;
int repLength = prefix.length() + 1;
int repOffset= context.getInvocationOffset() - repLength;
final boolean needClosingBracket= context.getDocument().getChar(repOffset + repLength) != closingBracket;
if (needClosingBracket) {
repString += closingBracket;
}
final boolean isDir= include.endsWith("/"); //$NON-NLS-1$
final int relevance= computeRelevance(prefix, include) + (isDir ? 0 : 1);
final CCompletionProposal proposal= createProposal(repOffset, repLength, repString, dispString, image, relevance, context);
if (!isDir && !needClosingBracket) {
// put cursor behind closing bracket
proposal.setCursorPosition(repString.length() + 1);
}
proposals.add(proposal);
}
}
}
}
/**
* Collect potential include files for the given translation unit.
*
* @param tu the translation unit to include the file
* @param prefixPath the path part to match the sub-directory and file name
* @param angleBrackets whether angle brackets enclose the include file name
* @return an array of incude file names
* @throws CoreException
*/
private String[] collectIncludeFiles(final ITranslationUnit tu, IPath prefixPath, boolean angleBrackets) throws CoreException {
final List<String> includeFiles= new ArrayList<String>();
if (!angleBrackets) {
// search in current directory
IResource resource= tu.getResource();
if (resource != null) {
IContainer parent= resource.getParent();
collectIncludeFilesFromContainer(tu, parent, prefixPath, includeFiles);
} else {
IPath location= tu.getLocation();
if (location != null) {
collectIncludeFilesFromDirectory(tu, location.removeLastSegments(1), prefixPath, includeFiles);
}
}
}
IScannerInfo info= tu.getScannerInfo(true);
if (info != null) {
collectIncludeFilesFromScannerInfo(tu, info, prefixPath, angleBrackets, includeFiles);
}
return includeFiles.toArray(new String[includeFiles.size()]);
}
/**
* @param tu the translation unit to include the file
* @param info the scanner info for this translation unit
* @param prefixPath the path part to match the sub-directory and file name
* @param angleBrackets whether angle brackets enclose the include file name
* @param includeFiles the result list
*/
private void collectIncludeFilesFromScannerInfo(ITranslationUnit tu, IScannerInfo info, IPath prefixPath, boolean angleBrackets, List<String> includeFiles) {
if (!angleBrackets && info instanceof IExtendedScannerInfo) {
IExtendedScannerInfo extendedInfo= (IExtendedScannerInfo) info;
String[] quoteIncludes= extendedInfo.getLocalIncludePath();
if (quoteIncludes != null) {
for (String quoteInclude : quoteIncludes) {
IPath includeDir= new Path(quoteInclude);
collectIncludeFilesFromDirectory(tu, includeDir, prefixPath, includeFiles);
}
}
}
String[] allIncludes= info.getIncludePaths();
for (String allInclude : allIncludes) {
IPath includeDir= new Path(allInclude);
collectIncludeFilesFromDirectory(tu, includeDir, prefixPath, includeFiles);
}
}
/**
* Collect include files from the given file system directory.
*
* @param tu the translation unit to include the file
* @param directory the file system path of the directory
* @param prefixPath the path part to match the sub-directory and file name
* @param includeFiles the result list
*/
private void collectIncludeFilesFromDirectory(ITranslationUnit tu, IPath directory, IPath prefixPath, List<String> includeFiles) {
final String namePrefix;
if (prefixPath.segmentCount() == 0) {
namePrefix= ""; //$NON-NLS-1$
} else if (prefixPath.hasTrailingSeparator()) {
namePrefix= ""; //$NON-NLS-1$
prefixPath= prefixPath.removeTrailingSeparator();
directory= directory.append(prefixPath);
} else {
namePrefix= prefixPath.lastSegment();
prefixPath= prefixPath.removeLastSegments(1);
if (prefixPath.segmentCount() > 0) {
directory= directory.append(prefixPath);
}
}
final File fileDir = directory.toFile();
if (!fileDir.exists()) {
return;
}
final int prefixLength = namePrefix.length();
final IProject project= tu.getCProject().getProject();
File[] files= fileDir.listFiles();
IContentAssistMatcher matcher = ContentAssistMatcherFactory.getInstance().createMatcher(namePrefix);
for (File file : files) {
final String name= file.getName();
if (name.length() >= prefixLength && matcher.match(name.toCharArray())) {
if (file.isFile()) {
if (CoreModel.isValidCXXHeaderUnitName(project, name) || CoreModel.isValidCHeaderUnitName(project, name)) {
includeFiles.add(prefixPath.append(name).toString());
}
} else if (file.isDirectory()) {
includeFiles.add(prefixPath.append(name).addTrailingSeparator().toString());
}
}
}
}
/**
* Collect include files from the given resource container.
*
* @param tu the translation unit to include the file
* @param parent the resource container
* @param prefixPath the path part to match the sub-directory and file name
* @param includeFiles the result list
* @throws CoreException
*/
private void collectIncludeFilesFromContainer(final ITranslationUnit tu, IContainer parent, IPath prefixPath, final List<String> includeFiles) throws CoreException {
final String namePrefix;
if (prefixPath.segmentCount() == 0) {
namePrefix= ""; //$NON-NLS-1$
} else if (prefixPath.hasTrailingSeparator()) {
namePrefix= ""; //$NON-NLS-1$
prefixPath= prefixPath.removeTrailingSeparator();
} else {
namePrefix= prefixPath.lastSegment();
prefixPath= prefixPath.removeLastSegments(1);
}
if (prefixPath.segmentCount() > 0) {
IPath parentPath = parent.getFullPath().append(prefixPath);
if (parentPath.segmentCount() > 1) {
parent = parent.getFolder(prefixPath);
} else if (parentPath.segmentCount() == 1) {
parent = ResourcesPlugin.getWorkspace().getRoot().getProject(parentPath.lastSegment());
} else {
return;
}
}
if (!parent.exists()) {
return;
}
final IPath cPrefixPath= prefixPath;
final int prefixLength = namePrefix.length();
final IContentAssistMatcher matcher = ContentAssistMatcherFactory.getInstance().createMatcher(namePrefix);
final IProject project= tu.getCProject().getProject();
parent.accept(new IResourceProxyVisitor() {
boolean fFirstVisit= true;
public boolean visit(IResourceProxy proxy) throws CoreException {
final int type= proxy.getType();
final String name= proxy.getName();
if (fFirstVisit) {
fFirstVisit= false;
return true;
}
if (name.length() >= prefixLength && matcher.match(name.toCharArray())) {
if (type == IResource.FILE) {
if (CoreModel.isValidCXXHeaderUnitName(project, name) || CoreModel.isValidCHeaderUnitName(project, name)) {
includeFiles.add(cPrefixPath.append(name).toString());
}
} else if (type == IResource.FOLDER) {
includeFiles.add(cPrefixPath.append(name).addTrailingSeparator().toString());
}
}
return false;
}}, IResource.DEPTH_ONE);
}
/**
* Compute the file name portion in an incomplete include directive.
*
* @param context
* @return the file name portion including the opening bracket or quote
* @throws BadLocationException
*/
private String computeIncludePrefix(CContentAssistInvocationContext context) throws BadLocationException {
IDocument document= context.getDocument();
if (document == null)
return null;
int end= context.getInvocationOffset();
int start= end;
while (--start >= 0) {
final char ch= document.getChar(start);
if (ch == '"' || ch == '<')
break;
}
return document.get(start, end - start);
}
/**
* Compute base relevance depending on quality of name / prefix match.
*
* @param prefix the completion pefix
* @param match the matching identifier
* @return a relevance value inidicating the quality of the name match
*/
protected int computeRelevance(String prefix, String match) {
int baseRelevance= 0;
boolean caseMatch= prefix.length() > 0 && match.startsWith(prefix);
if (caseMatch) {
baseRelevance += RelevanceConstants.CASE_MATCH_RELEVANCE;
}
return baseRelevance;
}
private CCompletionProposal createProposal(int repOffset, int repLength, String repString, String dispString, Image image, int relevance, CContentAssistInvocationContext context) {
return new CCompletionProposal(repString, repOffset, repLength, image, dispString, dispString, relevance, context.getViewer());
}
private Image getImage(ImageDescriptor desc) {
return desc != null ? CUIPlugin.getImageDescriptorRegistry().get(desc) : null;
}
}