| /*=============================================================================# |
| # Copyright (c) 2009, 2021 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 org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.jface.bindings.keys.KeySequence; |
| import org.eclipse.jface.text.ITextViewer; |
| import org.eclipse.jface.text.contentassist.ContentAssistEvent; |
| import org.eclipse.jface.text.contentassist.ContentAssistant; |
| import org.eclipse.jface.text.contentassist.ICompletionListener; |
| import org.eclipse.jface.text.contentassist.ICompletionListenerExtension; |
| import org.eclipse.jface.text.contentassist.ICompletionProposal; |
| import org.eclipse.jface.text.contentassist.IContentAssistProcessor; |
| import org.eclipse.jface.text.contentassist.IContextInformation; |
| import org.eclipse.jface.text.contentassist.IContextInformationValidator; |
| import org.eclipse.osgi.util.NLS; |
| import org.eclipse.ui.statushandlers.StatusManager; |
| import org.eclipse.ui.texteditor.ITextEditorActionDefinitionIds; |
| |
| import org.eclipse.statet.jcommons.lang.NonNullByDefault; |
| import org.eclipse.statet.jcommons.lang.Nullable; |
| |
| import org.eclipse.statet.ecommons.ui.SharedMessages; |
| import org.eclipse.statet.ecommons.ui.workbench.WorkbenchUIUtils; |
| |
| import org.eclipse.statet.internal.ltk.ui.LTKUIPlugin; |
| |
| |
| /** |
| * LTK content assistant. |
| */ |
| @NonNullByDefault |
| public class ContentAssist extends ContentAssistant { |
| |
| |
| private static final int ON= 1 << 0; |
| |
| private static final int AUTO_REQUEST= 1 << 3; |
| |
| private static final int SPECIFIC_SESSION= 1 << 5; |
| |
| private static final int RELOAD_REQUEST= 1 << 6; |
| |
| |
| abstract static class Processor implements IContentAssistProcessor { |
| |
| |
| private final ContentAssist assist; |
| |
| private final String contentType; |
| |
| |
| public Processor(final ContentAssist assist, final String contentType) { |
| assert (assist != null); |
| assert (contentType != null); |
| |
| this.assist= assist; |
| |
| this.contentType= contentType; |
| } |
| |
| |
| public final ContentAssist getAssistant() { |
| return this.assist; |
| } |
| |
| public final String getContentType() { |
| return this.contentType; |
| } |
| |
| @Override |
| public final ICompletionProposal @Nullable [] computeCompletionProposals( |
| final ITextViewer viewer, final int offset) { |
| this.assist.onCompletionProposalComputeBegin(getContentType()); |
| try { |
| return doComputeCompletionProposals(viewer, offset); |
| } |
| finally { |
| this.assist.onCompletionProposalComputeEnd(getContentType()); |
| } |
| } |
| |
| protected abstract ICompletionProposal @Nullable [] doComputeCompletionProposals( |
| final ITextViewer viewer, final int offset); |
| |
| @Override |
| public final IContextInformation @Nullable [] computeContextInformation( |
| final ITextViewer viewer, final int offset) { |
| this.assist.onContextInformationComputeBegin(getContentType()); |
| try { |
| return doComputeContextInformation(viewer, offset); |
| } |
| finally { |
| this.assist.onContextInformationComputeEnd(getContentType()); |
| } |
| } |
| |
| protected abstract IContextInformation @Nullable [] doComputeContextInformation( |
| final ITextViewer viewer, final int offset); |
| |
| } |
| |
| private static class WrappedProcessor extends Processor { |
| |
| |
| private final IContentAssistProcessor processor; |
| |
| |
| public WrappedProcessor(final ContentAssist assist, final String contentType, |
| final IContentAssistProcessor processor) { |
| super(assist, contentType); |
| this.processor= processor; |
| } |
| |
| |
| @Override |
| public char @Nullable [] getCompletionProposalAutoActivationCharacters() { |
| return this.processor.getCompletionProposalAutoActivationCharacters(); |
| } |
| |
| @Override |
| public char @Nullable [] getContextInformationAutoActivationCharacters() { |
| return this.processor.getContextInformationAutoActivationCharacters(); |
| } |
| |
| @Override |
| protected ICompletionProposal @Nullable [] doComputeCompletionProposals( |
| final ITextViewer viewer, final int offset) { |
| return this.processor.computeCompletionProposals(viewer, offset); |
| } |
| |
| @Override |
| protected IContextInformation @Nullable [] doComputeContextInformation(final ITextViewer viewer, |
| final int offset) { |
| return this.processor.computeContextInformation(viewer, offset); |
| } |
| |
| @Override |
| public @Nullable String getErrorMessage() { |
| return this.processor.getErrorMessage(); |
| } |
| |
| @Override |
| public @Nullable IContextInformationValidator getContextInformationValidator() { |
| return this.processor.getContextInformationValidator(); |
| } |
| |
| } |
| |
| |
| private class SelfListener implements ICompletionListener, ICompletionListenerExtension { |
| |
| |
| @Override |
| public void assistSessionStarted(final ContentAssistEvent event) { |
| onCompletionProposalSessionBegin(event.isAutoActivated); |
| } |
| |
| @Override |
| public void assistSessionRestarted(final ContentAssistEvent event) { |
| } |
| |
| @Override |
| public void assistSessionEnded(final ContentAssistEvent event) { |
| onCompletionProposalSessionEnd(); |
| } |
| |
| @Override |
| public void selectionChanged(final @Nullable ICompletionProposal proposal, |
| final boolean smartToggle) { |
| if (proposal != ContentAssist.this.completionProposalSelection) { |
| ContentAssist.this.completionProposalSelection= proposal; |
| ContentAssist.this.completionProposalSelectionCounter++; |
| } |
| } |
| |
| } |
| |
| |
| private boolean isAutoInsertEnabled; |
| private boolean isAutoInsertOverwritten; |
| |
| private boolean isRepeatedInvocationModeEnabled; |
| |
| private int completionProposalSessionRequest; |
| private int completionProposalSession; |
| private int completionProposalSessionCounter; |
| |
| private String specificCategoryId; |
| |
| private @Nullable ICompletionProposal completionProposalSelection; |
| private int completionProposalSelectionCounter; |
| |
| private @Nullable KeySequence completionProposalKeyBinding; |
| private @Nullable String completionProposalIterationGesture; |
| |
| private int contextInformationSession; |
| |
| private boolean showSubstringMatchesEnabled; |
| |
| |
| public ContentAssist() { |
| addCompletionListener(new SelfListener()); |
| } |
| |
| |
| boolean isProposalPopupActive1() { |
| return super.isProposalPopupActive(); |
| } |
| |
| boolean isContextInfoPopupActive1() { |
| return super.isContextInfoPopupActive(); |
| } |
| |
| public void hidePopups() { |
| hide(); |
| } |
| |
| @Override |
| protected void hide() { |
| // Workaround for bug 94106 (-> loop) and bug 512251 (-> catch exception) in Platform Text |
| try { |
| super.hide(); |
| } |
| catch (final RuntimeException e) { |
| StatusManager.getManager().handle(new Status(IStatus.ERROR, LTKUIPlugin.BUNDLE_ID, |
| "An error occurred when hiding content assistant.", e )); |
| } |
| |
| for (int counter= 0; counter < 100 && isContextInfoPopupActive(); counter++) { |
| try { |
| super.hide(); |
| } |
| catch (final RuntimeException e) {} |
| } |
| } |
| |
| |
| private void onCompletionProposalSessionBegin(final boolean isAutoActivated) { |
| this.completionProposalSession= (this.completionProposalSessionRequest != 0) ? |
| ContentAssist.this.completionProposalSessionRequest : |
| (isAutoActivated) ? (ON | AUTO_REQUEST) : (ON); |
| this.completionProposalSessionRequest= 0; |
| |
| this.completionProposalSessionCounter++; |
| |
| this.completionProposalSelectionCounter= 0; |
| |
| this.completionProposalKeyBinding= WorkbenchUIUtils.getBestKeyBinding( |
| ITextEditorActionDefinitionIds.CONTENT_ASSIST_PROPOSALS ); |
| setRepeatedInvocationTrigger(this.completionProposalKeyBinding); |
| this.completionProposalIterationGesture= (this.completionProposalKeyBinding != null) ? |
| NLS.bind(SharedMessages.Affordance_Press_message, this.completionProposalKeyBinding.format()) : |
| SharedMessages.Affordance_Click_message; |
| } |
| |
| private void onCompletionProposalComputeBegin(final String contentType) { |
| this.completionProposalSessionCounter= 0; |
| this.completionProposalSelection= null; |
| } |
| |
| private void onCompletionProposalComputeEnd(final String contentType) { |
| this.completionProposalSession&= ~AUTO_REQUEST; |
| } |
| |
| private void onCompletionProposalSessionEnd() { |
| this.completionProposalSession= 0; |
| |
| this.completionProposalSelection= null; |
| |
| this.completionProposalKeyBinding= null; |
| this.completionProposalIterationGesture= null; |
| } |
| |
| private void onContextInformationComputeBegin(final String contentType) { |
| if (this.contextInformationSession == 0) { |
| this.contextInformationSession= (ON | AUTO_REQUEST); |
| } |
| } |
| |
| private void onContextInformationComputeEnd(final String contentType) { |
| this.contextInformationSession= 0; |
| } |
| |
| |
| @Override |
| public void setContentAssistProcessor(@Nullable IContentAssistProcessor processor, |
| final String contentType) { |
| if (contentType == null) { |
| throw new NullPointerException("contentType"); //$NON-NLS-1$ |
| } |
| if (processor != null && !(processor instanceof Processor)) { |
| processor= new WrappedProcessor(this, contentType, processor); |
| } |
| super.setContentAssistProcessor(processor, contentType); |
| } |
| |
| @Override |
| public void enableAutoInsert(final boolean enabled) { |
| this.isAutoInsertEnabled= enabled; |
| if (!this.isAutoInsertOverwritten) { |
| super.enableAutoInsert(enabled); |
| } |
| } |
| |
| @Override |
| public void setRepeatedInvocationMode(final boolean cycling) { |
| this.isRepeatedInvocationModeEnabled= cycling; |
| super.setRepeatedInvocationMode(cycling); |
| } |
| |
| /** |
| * Overwrites the current (user) setting temporarily and enables auto insert until it is reset |
| * by calling {@link #enableAutoInsertSetting()}. |
| * |
| * @see #enableAutoInsert(boolean) |
| */ |
| void enableAutoInsertTemporarily() { |
| this.isAutoInsertOverwritten= true; |
| super.enableAutoInsert(true); |
| } |
| |
| /** |
| * Disables the overwriting of auto insert enabled by {@link #enableAutoInsertTemporarily()} |
| * and resets it to the (user) setting. |
| * |
| * @see #enableAutoInsert(boolean) |
| */ |
| void enableAutoInsertSetting() { |
| if (this.isAutoInsertOverwritten) { |
| this.isAutoInsertOverwritten= false; |
| super.enableAutoInsert(this.isAutoInsertEnabled); |
| } |
| } |
| |
| |
| public void showPossibleCompletions(final boolean restart, final boolean autostart) { |
| class AutoAssist extends AutoAssistListener { |
| |
| public static final int SHOW_PROPOSALS= 1; |
| |
| @Override |
| public void start(final int showStyle) { |
| showAssist(showStyle); |
| } |
| |
| } |
| |
| if (restart) { |
| super.hide(); |
| } |
| if (autostart) { |
| new AutoAssist().start(AutoAssist.SHOW_PROPOSALS); |
| } |
| else { |
| super.showPossibleCompletions(); |
| } |
| } |
| |
| @Override |
| public @Nullable String showPossibleCompletions() { |
| this.completionProposalSessionRequest= (ON); |
| try { |
| return super.showPossibleCompletions(); |
| } |
| finally { |
| this.completionProposalSessionRequest= 0; |
| } |
| } |
| |
| public @Nullable String showPossibleCompletions(final String categoryId) { |
| if (categoryId == null) { |
| throw new NullPointerException("categoryId"); //$NON-NLS-1$ |
| } |
| this.completionProposalSessionRequest= (ON | SPECIFIC_SESSION); |
| this.specificCategoryId= categoryId; |
| try { |
| return super.showPossibleCompletions(); |
| } |
| finally { |
| this.completionProposalSessionRequest= 0; |
| } |
| } |
| |
| void reloadPossibleCompletions() { |
| if (this.completionProposalSession == 0 |
| || (isCompletionProposalAutoRequest() && !isProposalPopupActive1())) { |
| return; |
| } |
| |
| this.completionProposalSession|= RELOAD_REQUEST; |
| this.completionProposalSessionRequest= this.completionProposalSession; |
| super.setRepeatedInvocationMode(true); // avoid start of new session |
| try { |
| super.showPossibleCompletions(); |
| } |
| finally { |
| this.completionProposalSession&= ~RELOAD_REQUEST; |
| this.completionProposalSessionRequest= 0; |
| super.setRepeatedInvocationMode(this.isRepeatedInvocationModeEnabled); |
| } |
| } |
| |
| public final boolean isCompletionProposalAutoRequest() { |
| return ((this.completionProposalSession & AUTO_REQUEST) != 0); |
| } |
| |
| final boolean isCompletionProposalReloadRequest() { |
| return ((this.completionProposalSession & RELOAD_REQUEST) != 0); |
| } |
| |
| public final boolean isCompletionProposalSpecificSession() { |
| return ((this.completionProposalSession & SPECIFIC_SESSION) != 0); |
| } |
| |
| public final @Nullable String getSpecificMode() { |
| return (isCompletionProposalSpecificSession()) ? this.specificCategoryId : null; |
| } |
| |
| public final int getCompletionProposalSessionCounter() { |
| return this.completionProposalSessionCounter; |
| } |
| |
| public @Nullable KeySequence getCompletionProposalKeyBinding() { |
| return this.completionProposalKeyBinding; |
| } |
| |
| public @Nullable String getCompletionProposalIterationGesture() { |
| return this.completionProposalIterationGesture; |
| } |
| |
| public final int getCompletionProposalSelectionCounter() { |
| return this.completionProposalSelectionCounter; |
| } |
| |
| |
| @Override |
| public @Nullable String showContextInformation() { |
| if (isCompletionProposalSpecificSession() && isProposalPopupActive()) { |
| hide(); |
| } |
| |
| this.contextInformationSession= (ON); |
| return super.showContextInformation(); |
| } |
| |
| public final boolean isContextInformationAutoRequest() { |
| return ((this.contextInformationSession & AUTO_REQUEST) != 0); |
| } |
| |
| |
| public void setShowSubstringMatches(final boolean enabled) { |
| this.showSubstringMatchesEnabled= enabled; |
| } |
| |
| public boolean getShowSubstringMatches() { |
| return this.showSubstringMatchesEnabled; |
| } |
| |
| } |