blob: 6a2091d2d7d3a31e902de11701816093deb4823f [file] [log] [blame]
/*=============================================================================#
# 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;
}
}