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