/*=============================================================================#
# Copyright (c) 2007-2016 Stephan Wahlbrink (WalWare.de) 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:
# Stephan Wahlbrink - initial API and implementation
#=============================================================================*/
package de.walware.ecommons.ltk.ui.sourceediting.assist;
import java.io.File;
import java.util.Arrays;
import com.ibm.icu.text.Collator;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.filesystem.URIUtil;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IResource;
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.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IInformationControlCreator;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.Region;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension3;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.jface.text.source.SourceViewer;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.statushandlers.StatusManager;
import de.walware.jcommons.lang.SystemUtils;
import de.walware.ecommons.runtime.core.util.PathUtils;
import de.walware.ecommons.ui.SharedUIResources;
import de.walware.ecommons.ltk.internal.ui.LTKUIPlugin;
import de.walware.ecommons.ltk.ui.sourceediting.ISourceEditor;
/**
* Content assist processor for completion of path for local file system resources.
*/
public abstract class PathCompletionComputor implements IContentAssistComputer {
protected class ResourceProposal extends CompletionProposalWithOverwrite implements ICompletionProposalExtension3 {
private final IFileStore fileStore;
private final boolean isDirectory;
/** The parent in the workspace, if in workspace */
private final IContainer workspaceRef;
private final String name;
/** Final completion string */
private String completion;
private IRegion selectionToSet;
/**
* Creates a new completion proposal for a resource
*
* @param offset the offset in the document where to insert the completion
* @param fileStore the EFS resource handle
* @param explicitName optional explicit name used instead of the name of the fileStore
* @param prefix optional prefix to prefix before the name
* @param workspaceRef the workspace resource handle, if the resource is in the workspace
*/
public ResourceProposal(final AssistInvocationContext context,
final int offset, final IFileStore fileStore, final String explicitName, final String prefix, final IContainer workspaceRef) {
super(context, offset);
this.fileStore= fileStore;
this.isDirectory= this.fileStore.fetchInfo().isDirectory();
this.workspaceRef= workspaceRef;
final StringBuilder name= new StringBuilder((explicitName != null) ? explicitName : this.fileStore.getName());
if (prefix != null) {
name.insert(0, prefix);
}
if (this.isDirectory) {
name.append(PathCompletionComputor.this.fileSeparator);
}
this.name= name.toString();
}
@Override
protected String getPluginId() {
return PathCompletionComputor.this.getPluginId();
}
@Override
public int getRelevance() {
return 20;
}
@Override
public String getSortingString() {
return this.name;
}
@Override
public boolean isAutoInsertable() {
return false;
}
private void createCompletion(final IDocument document) {
if (this.completion == null) {
try {
this.completion= checkPathCompletion(document, getReplacementOffset(), this.name);
}
catch (final BadLocationException e) {
StatusManager.getManager().handle(new Status(IStatus.ERROR, SharedUIResources.PLUGIN_ID, -1,
"An error occurred while creating the final path completion.", e), StatusManager.LOG);
}
}
}
@Override
protected int computeReplacementLength(final int replacementOffset, final Point selection, final int caretOffset, final boolean overwrite) throws BadLocationException {
int end= Math.max(caretOffset, selection.x + selection.y);
if (overwrite) {
final IDocument document= getInvocationContext().getSourceViewer().getDocument();
final int length= document.getLength();
while (end < length) {
final char c= document.getChar(end);
if (Character.isLetterOrDigit(c) || c == '_' || c == '.') {
end++;
}
else {
break;
}
}
if (end >= length) {
end= length;
}
}
return (end - replacementOffset);
}
/**
* @{inheritDoc}
*/
@Override
public Image getImage() {
Image image= null;
if (this.workspaceRef != null) {
final IResource member= this.workspaceRef.findMember(this.fileStore.getName(), true);
if (member != null) {
image= LTKUIPlugin.getInstance().getWorkbenchLabelProvider().getImage(member);
}
}
if (image == null) {
image= PlatformUI.getWorkbench().getSharedImages().getImage(
this.isDirectory ? ISharedImages.IMG_OBJ_FOLDER : ISharedImages.IMG_OBJ_FILE);
}
return image;
}
/**
* {@inheritDoc}
*/
@Override
public String getDisplayString() {
return this.name;
}
/**
* {@inheritDoc}
*/
@Override
public String getAdditionalProposalInfo() {
return null;
}
/**
* {@inheritDoc}
*/
@Override
public IContextInformation getContextInformation() {
return null;
}
/**
* {@inheritDoc}
*/
@Override
public IInformationControlCreator getInformationControlCreator() {
return null;
}
/**
* {@inheritDoc}
*/
@Override
public boolean validate(final IDocument document, final int offset, final DocumentEvent event) {
final int replacementOffset= getReplacementOffset();
if (offset < replacementOffset) {
return false;
}
try {
final String startsWith= document.get(replacementOffset, offset-replacementOffset);
return this.name.regionMatches(true, 0, startsWith, 0, startsWith.length());
}
catch (final BadLocationException e) {
return false;
}
}
/**
* {@inheritDoc}
*/
@Override
protected void doApply(final char trigger, final int stateMask, final int caretOffset, final int replacementOffset, final int replacementLength)
throws BadLocationException {
final AssistInvocationContext context= getInvocationContext();
final SourceViewer viewer= context.getSourceViewer();
final IDocument document= viewer.getDocument();
// final Point selectedRange= viewer.getSelectedRange();
createCompletion(document);
final Position newSelectionOffset= new Position(replacementOffset + replacementLength, 0);
try {
document.addPosition(newSelectionOffset);
document.replace(replacementOffset, newSelectionOffset.getOffset() - replacementOffset, this.completion);
this.selectionToSet= new Region(newSelectionOffset.getOffset(), 0);
}
catch (final BadLocationException e) {
StatusManager.getManager().handle(new Status(IStatus.ERROR, SharedUIResources.PLUGIN_ID, -1,
"An error occurred while inserting the path completion.", e), StatusManager.SHOW | StatusManager.LOG);
return;
}
finally {
document.removePosition(newSelectionOffset);
}
if (this.isDirectory) {
reinvokeAssist(viewer);
}
}
/**
* {@inheritDoc}
*/
@Override
public Point getSelection(final IDocument document) {
if (this.selectionToSet != null) {
return new Point(this.selectionToSet.getOffset(), this.selectionToSet.getLength());
}
return null;
}
/**
* {@inheritDoc}
*/
@Override
public int getPrefixCompletionStart(final IDocument document, final int completionOffset) {
return getReplacementOffset();
}
/**
* {@inheritDoc}
*/
@Override
public CharSequence getPrefixCompletionText(final IDocument document, final int completionOffset) {
createCompletion(document);
return this.completion;
}
}
private char fileSeparator;
private char fileSeparatorBackup;
private boolean isWindows;
private ISourceEditor editor;
public PathCompletionComputor() {
}
public abstract String getPluginId();
protected ISourceEditor getEditor() {
return this.editor;
}
/**
* {@inheritDoc}
*/
@Override
public void sessionStarted(final ISourceEditor editor, final ContentAssist assist) {
this.editor= editor;
this.isWindows= getIsWindows();
this.fileSeparator= getDefaultFileSeparator();
}
/**
* {@inheritDoc}
*/
@Override
public void sessionEnded() {
this.editor= null;
}
protected boolean getIsWindows() {
return SystemUtils.isOSWindows(System.getProperty(SystemUtils.OS_NAME_KEY));
}
protected final boolean isWindows() {
return this.isWindows;
}
protected char getDefaultFileSeparator() {
return (isWindows()) ? '\\' : '/';
}
protected char getSegmentSeparator() {
return this.fileSeparator;
}
public char[] getCompletionProposalAutoActivationCharacters() {
return new char[] { '/', '\\', ':' };
}
public char[] getContextInformationAutoActivationCharacters() {
return null;
}
/**
* {@inheritDoc}
*/
@Override
public IStatus computeCompletionProposals(final AssistInvocationContext context, final int mode,
final AssistProposalCollector proposals, final IProgressMonitor monitor) {
try {
final int offset= context.getInvocationOffset();
final IRegion partition= getContentRange(context, mode);
if (partition == null) {
return null;
}
String prefix= checkPrefix(context.getSourceViewer().getDocument().get(
partition.getOffset(), offset - partition.getOffset() ));
if (prefix == null) {
return null;
}
boolean needSeparatorBeforeStart= false; // including virtual separator
String segmentPrefix= ""; //$NON-NLS-1$
IFileStore baseStore= null;
if (prefix.length() > 0 && prefix.charAt(prefix.length() - 1) == '.') {
// prevent that path segments '.' and '..' at end are resolved by Path#canonicalize
if (prefix.equals(".") || prefix.endsWith("/.") || (isWindows() && prefix.endsWith("\\."))) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
prefix= prefix.substring(0, prefix.length() - 1);
segmentPrefix= "."; //$NON-NLS-1$
}
else if (prefix.equals("..") || prefix.endsWith("/..") || (isWindows() && prefix.endsWith("\\.."))) { // //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
prefix= prefix.substring(0, prefix.length() - 2);
segmentPrefix= ".."; //$NON-NLS-1$
}
}
IPath path= createPath(prefix);
if (path == null) {
return null;
}
if (path.segmentCount() == 0) {
if (isWindows() && path.getDevice() != null && !path.isRoot()) { // C: -> C:/
path= path.addTrailingSeparator();
needSeparatorBeforeStart= true;
}
}
else if (// path.segmentCount() > 0 &&
segmentPrefix.isEmpty() && !path.hasTrailingSeparator()) {
segmentPrefix= path.lastSegment();
path= path.removeLastSegments(1);
}
// on Windows, path starting with path separator are relative to the device of current directory
if (path.isAbsolute() && isWindows() && path.getDevice() == null && !path.isUNC()) {
final IPath basePath= getRelativeBasePath();
if (basePath != null) {
path= path.setDevice(basePath.getDevice());
}
}
baseStore= resolveStore(path);
updatePathSeparator(prefix);
String completionPrefix= (needSeparatorBeforeStart) ? Character.toString(this.fileSeparator) : null;
if (baseStore == null || !baseStore.fetchInfo().exists()) {
if (path != null) {
return tryAlternative(context, path, offset - segmentPrefix.length(), segmentPrefix,
completionPrefix, proposals );
}
return null;
}
doAddChildren(context, baseStore, offset - segmentPrefix.length(), segmentPrefix, completionPrefix,
proposals );
if (!segmentPrefix.isEmpty() && !segmentPrefix.equals(".")) { //$NON-NLS-1$
baseStore= baseStore.getChild(segmentPrefix);
if (baseStore.fetchInfo().exists()) {
final StringBuilder prefixBuilder= new StringBuilder();
if (completionPrefix != null) {
prefixBuilder.append(completionPrefix);
}
prefixBuilder.append(baseStore.getName());
prefixBuilder.append(this.fileSeparator);
completionPrefix= prefixBuilder.toString();
doAddChildren(context, baseStore, offset - segmentPrefix.length(), "", //$NON-NLS-1$
completionPrefix, proposals );
}
}
return Status.OK_STATUS;
}
catch (final BadLocationException e) {
StatusManager.getManager().handle(new Status(IStatus.ERROR, SharedUIResources.PLUGIN_ID, -1,
"An error occurred while preparing path completions.", e), StatusManager.LOG);
}
catch (final CoreException e) {
StatusManager.getManager().handle(new Status(IStatus.ERROR, SharedUIResources.PLUGIN_ID, -1,
"An error occurred while preparing path completions.", e), StatusManager.LOG);
}
restorePathSeparator();
return null;
}
@Override
public IStatus computeInformationProposals(final AssistInvocationContext context,
final AssistProposalCollector proposals, final IProgressMonitor monitor) {
return null;
}
/**
* @param prefix to check
* @return the prefix, if valid, otherwise <code>null</code>
*/
protected String checkPrefix(final String prefix) {
final char[] breakingChars= "\n\r+<>|?*\"".toCharArray(); //$NON-NLS-1$
for (int i= 0; i < breakingChars.length; i++) {
if (prefix.indexOf(breakingChars[i]) >= 0) {
return null;
}
}
return prefix;
}
private IPath createPath(String s) {
if (isWindows() && File.separatorChar == '/') {
s= s.replace('\\', '/');
}
return PathUtils.check(new Path(s));
}
private void updatePathSeparator(final String prefix) {
final int lastBack= prefix.lastIndexOf('\\');
final int lastForw= prefix.lastIndexOf('/');
if (lastBack > lastForw) {
this.fileSeparatorBackup= this.fileSeparator;
this.fileSeparator= '\\';
}
else if (lastForw > lastBack) {
this.fileSeparatorBackup= this.fileSeparator;
this.fileSeparator= '/';
}
// else -1 == -1
}
private void restorePathSeparator() {
if (this.fileSeparatorBackup != 0) {
this.fileSeparator= this.fileSeparatorBackup;
this.fileSeparatorBackup= 0;
}
}
protected void doAddChildren(final AssistInvocationContext context,
final IFileStore baseStore,
final int startOffset, final String segmentPrefix, final String completionPrefix,
final AssistProposalCollector proposals) throws CoreException {
final IContainer[] workspaceRefs= ResourcesPlugin.getWorkspace().getRoot().findContainersForLocationURI(baseStore.toURI());
final IContainer workspaceRef= (workspaceRefs.length > 0) ? workspaceRefs[0] : null;
final String[] names= baseStore.childNames(EFS.NONE, new NullProgressMonitor());
Arrays.sort(names, Collator.getInstance());
for (final String name : names) {
if (segmentPrefix.isEmpty()
|| name.regionMatches(true, 0, segmentPrefix, 0, segmentPrefix.length()) ) {
proposals.add(new ResourceProposal(context, startOffset,
baseStore.getChild(name), null, completionPrefix,
workspaceRef ));
}
}
}
protected abstract IRegion getContentRange(AssistInvocationContext context, int mode)
throws BadLocationException;
protected IPath getRelativeBasePath() {
return null;
}
protected IFileStore getRelativeBaseStore() {
return null;
}
protected IFileStore resolveStore(IPath path) throws CoreException {
if (!path.isAbsolute()) {
if (!isWindows() && path.getDevice() == null && "~".equals(path.segment(0))) { //$NON-NLS-1$
final IPath homePath= new Path(System.getProperty(SystemUtils.USER_HOME_KEY));
path= PathUtils.check(homePath.append(path.removeFirstSegments(1)));
}
else {
final IFileStore base= getRelativeBaseStore();
if (base != null) {
return base.getFileStore(path);
}
return null;
}
}
return EFS.getStore(URIUtil.toURI(path));
}
protected IStatus tryAlternative(final AssistInvocationContext context,
final IPath path,
final int startOffset, final String segmentPrefix, final String completionPrefix,
final AssistProposalCollector proposals) throws CoreException {
return null;
}
/**
* Final check of completion string.
*
* E.g. to escape special chars.
*
* @param document
* @param completionOffset
* @param completion
*
* @return the checked completion string
* @throws BadLocationException
*/
protected String checkPathCompletion(final IDocument document, final int completionOffset, final String completion)
throws BadLocationException {
return completion;
}
}