blob: 9b4022807174ffc963df8c0f41376f8f76eb7af2 [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2008, 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 static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullAssert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
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.ITextViewer;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension5;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.jface.text.link.LinkedModeModel;
import org.eclipse.jface.text.link.LinkedModeUI;
import org.eclipse.jface.text.link.LinkedModeUI.ExitFlags;
import org.eclipse.jface.text.link.LinkedModeUI.IExitPolicy;
import org.eclipse.jface.text.link.LinkedPosition;
import org.eclipse.jface.text.link.LinkedPositionGroup;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.ui.statushandlers.StatusManager;
import org.eclipse.ui.texteditor.link.EditorLinkedModeUI;
import org.eclipse.statet.jcommons.lang.NonNullByDefault;
import org.eclipse.statet.jcommons.lang.Nullable;
import org.eclipse.statet.jcommons.text.core.TextRegion;
import org.eclipse.statet.ecommons.text.ui.DefaultBrowserInformationInput;
import org.eclipse.statet.internal.ltk.ui.LTKUIPlugin;
import org.eclipse.statet.ltk.ui.LTKUI;
import org.eclipse.statet.ltk.ui.sourceediting.SourceEditor;
import org.eclipse.statet.ltk.ui.sourceediting.TextEditToolSynchronizer;
@NonNullByDefault
public abstract class LinkedNamesAssistProposal implements AssistProposal,
ICompletionProposalExtension5 {
/**
* An exit policy that skips Backspace and Delete at the beginning and at the end
* of a linked position, respectively.
*/
public static class DeleteBlockingExitPolicy implements IExitPolicy {
private final IDocument document;
public DeleteBlockingExitPolicy(final IDocument document) {
this.document= document;
}
@Override
public @Nullable ExitFlags doExit(final LinkedModeModel model, final VerifyEvent event,
final int offset, final int length) {
switch (event.character) {
case SWT.BS:
{ //skip backspace at beginning of linked position
final LinkedPosition position= model.findPosition(new LinkedPosition(
this.document, offset, 0, LinkedPositionGroup.NO_STOP));
if (position != null && offset <= position.getOffset() && length == 0) {
event.doit= false;
}
return null;
}
case SWT.DEL:
{ //skip delete at end of linked position
final LinkedPosition position= model.findPosition(new LinkedPosition(
this.document, offset, 0, LinkedPositionGroup.NO_STOP));
if (position != null && offset >= position.getOffset()+position.getLength() && length == 0) {
event.doit= false;
}
return null;
}
}
return null;
}
}
private final AssistInvocationContext context;
private String label;
private @Nullable String description;
private int relevance;
private @Nullable String valueSuggestion;
@SuppressWarnings("null")
public LinkedNamesAssistProposal(final AssistInvocationContext invocationContext) {
this.context= nonNullAssert(invocationContext);
}
protected void init(final String label, final @Nullable String description, final int relevance) {
this.label= nonNullAssert(label);
this.description= description;
this.relevance= relevance;
}
@Override
public boolean validate(final IDocument document, final int offset,
final @Nullable DocumentEvent event) {
return false;
}
@Override
public void apply(final ITextViewer viewer, final char trigger, final int stateMask, final int offset) {
try {
// by default full word is selected by linked model ui
// instead we want to keep the original selection by default
int selectionStartOffset;
int selectionEndOffset;
{ final Point selection= viewer.getSelectedRange();
selectionStartOffset= selection.x;
selectionEndOffset= selection.x + selection.y;
}
final IDocument document= viewer.getDocument();
if (document == null) {
return;
}
final LinkedModeModel model= new LinkedModeModel();
final LinkedPositionGroup group= new LinkedPositionGroup();
collectPositions(document, group);
if (group.isEmpty()) {
return;
}
model.addGroup(group);
model.forceInstall();
{ final SourceEditor editor= this.context.getEditor();
final TextEditToolSynchronizer synchronizer;
if (editor != null && (synchronizer= editor.getTextEditToolSynchronizer()) != null) {
synchronizer.install(model);
}
}
final LinkedModeUI ui= new EditorLinkedModeUI(model, viewer);
ui.setExitPolicy(new DeleteBlockingExitPolicy(document));
ui.setExitPosition(viewer, offset, 0, LinkedPositionGroup.NO_STOP);
ui.enter();
final String valueSuggestion= this.valueSuggestion;
final Position position0= group.getPositions()[0];
if (valueSuggestion != null) {
document.replace(position0.getOffset(), position0.getLength(), valueSuggestion);
selectionStartOffset= position0.getOffset();
selectionEndOffset= selectionStartOffset + valueSuggestion.length();
}
else {
// correct selection if larger/outside of the initial position
final int positionEndOffset= position0.getOffset() + position0.getLength();
if (selectionStartOffset < position0.getOffset()) {
selectionStartOffset= position0.getOffset();
}
else if (selectionStartOffset > positionEndOffset) {
selectionStartOffset= positionEndOffset;
}
if (selectionEndOffset < selectionStartOffset) {
selectionEndOffset= selectionStartOffset;
}
else if (selectionEndOffset > positionEndOffset) {
selectionEndOffset= positionEndOffset;
}
}
viewer.setSelectedRange(selectionStartOffset, selectionStartOffset + selectionEndOffset);
}
catch (final BadLocationException e) {
StatusManager.getManager().handle(new Status(IStatus.ERROR, LTKUIPlugin.BUNDLE_ID, -1,
"Error initializing linked rename.", e )); //$NON-NLS-1$
}
}
protected abstract void collectPositions(final IDocument document, final LinkedPositionGroup group)
throws BadLocationException;
protected int addPosition(final LinkedPositionGroup group, final IDocument document,
final @Nullable Position position, final int idx) throws BadLocationException {
if (position != null) {
group.addPosition(new LinkedPosition(document, position.getOffset(), position.getLength(), idx));
return idx+1;
}
return idx;
}
protected int addPosition(final LinkedPositionGroup group, final IDocument document,
final @Nullable TextRegion position, final int idx) throws BadLocationException {
if (position != null) {
group.addPosition(new LinkedPosition(document, position.getStartOffset(), position.getLength(), idx));
return idx+1;
}
return idx;
}
@Override
public void apply(final IDocument document) {
throw new UnsupportedOperationException();
}
@Override
public @Nullable Point getSelection(final IDocument document) {
return null;
}
@Override
public int getRelevance() {
return this.relevance;
}
@Override
public String getSortingString() {
return this.label;
}
@Override
public String getDisplayString() {
return this.label;
}
@Override
public Image getImage() {
return LTKUI.getImages().get(LTKUI.OBJ_TEXT_LINKEDRENAME_IMAGE_ID);
}
@Override
public @Nullable String getAdditionalProposalInfo() {
return this.description;
}
@Override
public @Nullable Object getAdditionalProposalInfo(final IProgressMonitor monitor) {
final var description= this.description;
if (description == null) {
return null;
}
return new DefaultBrowserInformationInput(getDisplayString(),
description, DefaultBrowserInformationInput.FORMAT_TEXT_INPUT );
}
@Override
public @Nullable IContextInformation getContextInformation() {
return null;
}
}