| /*=============================================================================# |
| # 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; |
| } |
| |
| } |