blob: d5ef25d167cc06e4ab79c390ade8d3b81a4d2949 [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2007, 2020 Stephan Wahlbrink and others.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
# which is available at https://www.apache.org/licenses/LICENSE-2.0.
#
# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
#
# Contributors:
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
#=============================================================================*/
package org.eclipse.statet.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.IDocument;
import org.eclipse.jface.text.Position;
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 org.eclipse.statet.jcommons.lang.NonNullByDefault;
import org.eclipse.statet.jcommons.lang.Nullable;
import org.eclipse.statet.jcommons.lang.SystemUtils;
import org.eclipse.statet.jcommons.text.core.SearchPattern;
import org.eclipse.statet.jcommons.text.core.TextRegion;
import org.eclipse.statet.ecommons.runtime.core.util.PathUtils;
import org.eclipse.statet.ecommons.ui.SharedUIResources;
import org.eclipse.statet.internal.ltk.ui.LTKUIPlugin;
import org.eclipse.statet.ltk.ui.sourceediting.ISourceEditor;
import org.eclipse.statet.ltk.ui.sourceediting.assist.SourceProposal.ProposalParameters;
/**
* Content assist processor for completion of path for local file system resources.
*/
@NonNullByDefault
public abstract class PathCompletionComputor implements ContentAssistComputer {
protected class ResourceCompletionProposal extends SourceProposal<AssistInvocationContext> {
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 @Nullable String completion;
/**
* 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 ResourceCompletionProposal(final ProposalParameters<? extends AssistInvocationContext> parameters,
final IFileStore fileStore, final String explicitName, final String prefix, final IContainer workspaceRef) {
super(parameters);
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 getName() {
return this.name;
}
@Override
public String getSortingString() {
return this.name;
}
@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);
}
@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;
}
private String getCompletion() {
String completion= this.completion;
if (completion == null) {
final AssistInvocationContext context= getInvocationContext();
final IDocument document= context.getDocument();
completion= checkPathCompletion(document, getReplacementOffset(), this.name);
this.completion= completion;
}
return completion;
}
@Override
public CharSequence getPrefixCompletionText(final IDocument document, final int completionOffset) {
return getCompletion();
}
@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 IDocument document= context.getDocument();
final ApplyData applyData= getApplyData();
// final Point selectedRange= viewer.getSelectedRange();
final String replacement= getCompletion();
final Position newSelectionOffset= new Position(replacementOffset + replacementLength, 0);
try {
document.addPosition(newSelectionOffset);
document.replace(replacementOffset, newSelectionOffset.getOffset() - replacementOffset, replacement);
applyData.setSelection(newSelectionOffset.getOffset());
}
finally {
document.removePosition(newSelectionOffset);
}
if (this.isDirectory) {
reinvokeAssist();
}
}
}
private int searchMatchRules;
private char fileSeparator;
private char fileSeparatorBackup;
private boolean isWindows;
public PathCompletionComputor() {
}
@Override
public void onSessionStarted(final ISourceEditor editor, final ContentAssist assist) {
int matchRules= SearchPattern.PREFIX_MATCH;
if (assist.getShowSubstringMatches()) {
matchRules |= SearchPattern.SUBSTRING_MATCH;
}
this.searchMatchRules= matchRules;
this.isWindows= getIsWindows();
this.fileSeparator= getDefaultFileSeparator();
}
@Override
public void onSessionEnded() {
}
protected int getSearchMatchRules() {
return this.searchMatchRules;
}
protected boolean getIsWindows() {
return SystemUtils.getLocalOs() == SystemUtils.OS_WIN;
}
protected final boolean isWindows() {
return this.isWindows;
}
protected char getDefaultFileSeparator() {
return (isWindows()) ? '\\' : '/';
}
protected char getSegmentSeparator() {
return this.fileSeparator;
}
@Override
public void computeCompletionProposals(final AssistInvocationContext context, final int mode,
final AssistProposalCollector proposals, final IProgressMonitor monitor) {
try {
final int offset= context.getInvocationOffset();
final TextRegion contentRange= getContentRegion(context, mode);
if (contentRange == null
|| offset < contentRange.getStartOffset() || offset > contentRange.getEndOffset()) {
return;
}
String prefix= getPrefix(context, contentRange, offset);
if (prefix == null) {
return;
}
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;
}
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()) {
tryAlternative(context, path, offset - segmentPrefix.length(), segmentPrefix,
completionPrefix, proposals );
return;
}
{ final ProposalParameters<? extends AssistInvocationContext> parameters= createProposalParameters(
context, offset - segmentPrefix.length(), segmentPrefix);
doAddChildren(parameters, baseStore, 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();
final ProposalParameters<?> parameters= createProposalParameters(
context, offset - segmentPrefix.length(), ""); //$NON-NLS-1$
parameters.baseRelevance= 20;
doAddChildren(parameters, baseStore, completionPrefix, proposals);
}
}
return;
}
catch (final BadLocationException e) {
StatusManager.getManager().handle(new Status(IStatus.ERROR, SharedUIResources.BUNDLE_ID, -1,
"An error occurred while preparing path completions.", e), StatusManager.LOG);
}
catch (final CoreException e) {
StatusManager.getManager().handle(new Status(IStatus.ERROR, SharedUIResources.BUNDLE_ID, -1,
"An error occurred while preparing path completions.", e), StatusManager.LOG);
}
finally {
restorePathSeparator();
}
}
@Override
public void computeInformationProposals(final AssistInvocationContext context,
final AssistProposalCollector proposals, final IProgressMonitor monitor) {
}
protected @Nullable String getPrefix(final AssistInvocationContext context,
final TextRegion contentRegion, final int offset)
throws BadLocationException {
return checkPrefix(context.getSourceViewer().getDocument().get(
contentRegion.getStartOffset(), offset - contentRegion.getStartOffset() ));
}
/**
* @param prefix to check
* @return the prefix, if valid, otherwise <code>null</code>
*/
protected @Nullable String checkPrefix(final @Nullable String prefix) {
if (prefix == null) {
return null;
}
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 @Nullable 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 ProposalParameters<?> createProposalParameters(
final AssistInvocationContext context, final int replacementOffset,
final String pattern) {
final ProposalParameters<?> parameters= new ProposalParameters<>(
context, replacementOffset,
new SearchPattern(getSearchMatchRules(), pattern) );
parameters.baseRelevance= 20;
return parameters;
}
protected void doAddChildren(final ProposalParameters<? extends AssistInvocationContext> parameters,
final IFileStore baseStore,
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 (parameters.matchesNamePattern(name)) {
proposals.add(new ResourceCompletionProposal(parameters,
baseStore.getChild(name), null, completionPrefix,
workspaceRef ));
}
}
}
protected abstract @Nullable TextRegion getContentRegion(AssistInvocationContext context, int mode)
throws BadLocationException;
protected @Nullable IPath getRelativeBasePath() {
return null;
}
protected @Nullable IFileStore getRelativeBaseStore() {
return null;
}
protected @Nullable 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 void tryAlternative(final AssistInvocationContext context,
final IPath path,
final int startOffset, final String segmentPrefix, final @Nullable String completionPrefix,
final AssistProposalCollector proposals) throws CoreException {
}
/**
* 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) {
return completion;
}
}