diff --git a/bundles/org.eclipse.text.quicksearch/META-INF/MANIFEST.MF b/bundles/org.eclipse.text.quicksearch/META-INF/MANIFEST.MF index f158e0beb15..cc7a899b4bf 100644 --- a/bundles/org.eclipse.text.quicksearch/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.text.quicksearch/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: %pluginName Bundle-SymbolicName: org.eclipse.text.quicksearch;singleton:=true -Bundle-Version: 1.3.0.qualifier +Bundle-Version: 1.4.0.qualifier Bundle-Activator: org.eclipse.text.quicksearch.internal.ui.QuickSearchActivator Require-Bundle: org.eclipse.ui;bundle-version="[3.113.0,4.0.0)", org.eclipse.core.resources;bundle-version="[3.13.0,4.0.0)", @@ -12,12 +12,14 @@ Require-Bundle: org.eclipse.ui;bundle-version="[3.113.0,4.0.0)", org.eclipse.ui.editors;bundle-version="[3.11.0,4.0.0)", org.eclipse.jface;bundle-version="[3.17.0,4.0.0)", org.eclipse.jface.text;bundle-version="[3.15.0,4.0.0)", - org.apache.ant;bundle-version="[1.10.0,2.0.0)" + org.apache.ant;bundle-version="[1.10.0,2.0.0)", + org.eclipse.core.contenttype;bundle-version="[3.9.0,4.0.0)" Bundle-ActivationPolicy: lazy Bundle-RequiredExecutionEnvironment: JavaSE-21 Bundle-Vendor: %providerName Bundle-Localization: plugin -Export-Package: org.eclipse.text.quicksearch.internal.core;x-internal:=true, +Export-Package: org.eclipse.text.quicksearch, + org.eclipse.text.quicksearch.internal.core;x-internal:=true, org.eclipse.text.quicksearch.internal.core.pathmatch;x-internal:=true, org.eclipse.text.quicksearch.internal.core.preferences;x-internal:=true, org.eclipse.text.quicksearch.internal.core.priority;x-friends:="org.eclipse.text.quicksearch.tests", diff --git a/bundles/org.eclipse.text.quicksearch/plugin.properties b/bundles/org.eclipse.text.quicksearch/plugin.properties index 1e5d5466c06..2e2ab285f50 100644 --- a/bundles/org.eclipse.text.quicksearch/plugin.properties +++ b/bundles/org.eclipse.text.quicksearch/plugin.properties @@ -9,6 +9,7 @@ # # Contributors: # Pivotal Inc - initial API and implementation +# Jozef Tomek - text viewers extension ############################################################################### pluginName= Quick Search @@ -18,3 +19,10 @@ quickSearch.label= Quick Search quickSearch.ellipsis= &Quick Search... quickSearch.tooltip=Search for a text pattern in the workspace quickAccess.category.label=File content +textViewers=Text Viewer +defaultSourceViewer=Text Viewer + +QuickSearchActivator.targetIdAttributeMissing=target id attribute ''{0}'' missing +QuickSearchActivator.contentTypeNotFound=content type ''{0}'' not found +QuickSearchActivator.targetNotFound=target ''{0}'' not found +QuickSearchActivator.unexpectedTag=expected tag ''{1}'', got ''{0}'' \ No newline at end of file diff --git a/bundles/org.eclipse.text.quicksearch/plugin.xml b/bundles/org.eclipse.text.quicksearch/plugin.xml index 40f76acfad9..27c4a77e27c 100644 --- a/bundles/org.eclipse.text.quicksearch/plugin.xml +++ b/bundles/org.eclipse.text.quicksearch/plugin.xml @@ -10,10 +10,13 @@ Contributors: Pivotal Inc - Initial API and implementation + Jozef Tomek - text viewers extension --> + + + + + + + + diff --git a/bundles/org.eclipse.text.quicksearch/schema/textViewers.exsd b/bundles/org.eclipse.text.quicksearch/schema/textViewers.exsd new file mode 100644 index 00000000000..bdd7839a997 --- /dev/null +++ b/bundles/org.eclipse.text.quicksearch/schema/textViewers.exsd @@ -0,0 +1,226 @@ + + + + + + + + + This extension point allows a plug-in to register text viewers for specific types of content in which quick search matches were found. +Since viewers don't have a default constructor, the extension point must implement the factory interface for viewers <samp>org.eclipse.text.quicksearch.ITextViewerCreator</samp> that then creates viewers wrapped by implementation of <samp>org.eclipse.text.quicksearch.ITextViewerCreator.ITextViewerHandle</samp> providing methods necessary for presenting quick search results. + + + + + + + + + + + + + + + + + + a fully qualified identifier of the target extension point + + + + + + + an optional identifier of the extension instance + + + + + + + an optional name of the extension instance + + + + + + + + + + + + + + + + + + + + a unique identifier that can be used to reference the viewer + + + + + + + a fully qualified name of a class that implements a factory for the +text viewer handle and implements <samp>org.eclipse.text.quicksearch.ITextViewerCreator</samp> + + + + + + + + + + an optional comma separated list of file extensions e.g. "java, html". If not defined, some <code>contentTypeBinding</code> must be defined + + + + + + + a translatable label that will be used in the UI for this viewer. + + + + + + + + + + Editor id to consider while searching for a viewer matching a content type. +If the specified "linked" editor has content type associations, they will be also considered as possible bindings for this viewer. This is useful in cases where viewer supports same content types as "linked" editor (like in generic editor). + + + + + + + + + + + + + + + + A <code>contentTypeBinding</code> binds a text viewer to a content type. If no binding is defined, <code>viewer</code> needs to declare <code>extensions</code> + + + + + + + The id of a content type defined using the <code>org.eclipse.core.contenttype.contentTypes</code> extension point. + + + + + + + + + + The id of a text viewer defined using the <code>viewer</code> element of this extension point (i.e. <code>org.eclipse.text.quicksearch.DefaultSourceViewer</code>) + + + + + + + + + + + + + + + 1.3 + + + + + + + + + The following is an example of a text viewer for text content type: +<p> +<pre> +<extension point="org.eclipse.text.quicksearch.textViewers"> + <viewer + id="org.eclipse.text.quicksearch.DefaultSourceViewer" + class="org.eclipse.text.quicksearch.internal.ui.DefaultSourceViewerCreator"/> + <contentTypeBinding + contentTypeId="org.eclipse.core.runtime.text" + sourceViewerId="org.eclipse.text.quicksearch.DefaultSourceViewer"> + </contentTypeBinding> +</extension> +</pre> +</p> +The following is an example of a text viewer for text content type & content types for which referenced editor is registered: +<p> +<pre> +<extension point="org.eclipse.text.quicksearch.textViewers"> + <viewer + id="org.eclipse.ui.genericeditor.quicksearch.GenericEditorViewer" + class="org.eclipse.ui.internal.genericeditor.quicksearch.GenericEditorViewerCreator" + linkedEditor="org.eclipse.ui.genericeditor.GenericEditor"/> + <contentTypeBinding + contentTypeId="org.eclipse.core.runtime.text" + sourceViewerId="org.eclipse.ui.genericeditor.quicksearch.GenericEditorViewer"> + </contentTypeBinding> +</extension> +</pre> +</p> + + + + + + + + + The contributed class must implement <code>org.eclipse.text.quicksearch.ITextViewerCreator</code> + + + + + + + + + The Quick Search plugin provides <code>org.eclipse.text.quicksearch.internal.ui.DefaultSourceViewerCreator</code> supplying default text viewer with no syntax coloring. + + + + + + + + + Copyright (c) 2025 Contributors to the Eclipse Foundation<br> + +See the NOTICE file(s) distributed with this work for additional +information regarding copyright ownership. + +This program and the accompanying materials are made available under the +terms of the Eclipse Public License 2.0 which is available at +<a href="https://www.eclipse.org/legal/epl-2.0">https://www.eclipse.org/legal/epl-2.0.html</a> + +SPDX-License-Identifier: EPL-2.0 + +Contributors: + Jozef Tomek - initial API and implementation + + + + diff --git a/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/ISourceViewerConfigurer.java b/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/ISourceViewerConfigurer.java new file mode 100644 index 00000000000..d9f36a8905d --- /dev/null +++ b/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/ISourceViewerConfigurer.java @@ -0,0 +1,45 @@ +package org.eclipse.text.quicksearch; + +import org.eclipse.jface.text.source.IChangeRulerColumn; +import org.eclipse.jface.text.source.SourceViewer; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.text.quicksearch.SourceViewerConfigurer.ISourceViewerCreator; +import org.eclipse.text.quicksearch.SourceViewerHandle.FixedLineHighlighter; + +/** + * Factory used by {@link SourceViewerHandle} responsible for creation and necessary setup of source viewers so that + * they provide common aspects of quicksearch text viewers: + * + * + * Actual source viewer instance creation is delegated to provided {@link ISourceViewerCreator}. + * @since 1.3 + * @see SourceViewerConfigurer + */ +public interface ISourceViewerConfigurer { + + /** + * Creates, configures and returns source viewer that provides common aspects of quicksearch text viewers. Delegates + * source viewer creation to {@link ISourceViewerCreator} provided on initialization. + * @param parent the parent SWT control for the viewer + * @return configured source viewer + * @see ISourceViewerCreator + */ + T getSourceViewer(Composite parent); + + /** + * Returns change ruler column installed to the viewer. + * @return viewer's change ruler column + */ + IChangeRulerColumn getChangeRulerColumn(); + + /** + * Returns fixed line highlighter installed to the viewer. + * @return viewer's fixed line highlighter + */ + FixedLineHighlighter getMatchLineHighlighter(); +} diff --git a/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/ITextViewerCreator.java b/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/ITextViewerCreator.java new file mode 100644 index 00000000000..63e7bd1cb26 --- /dev/null +++ b/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/ITextViewerCreator.java @@ -0,0 +1,108 @@ +/******************************************************************************* + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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 + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Jozef Tomek - initial API and implementation + *******************************************************************************/ +package org.eclipse.text.quicksearch; + +import org.eclipse.core.resources.IFile; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IRegion; +import org.eclipse.swt.custom.StackLayout; +import org.eclipse.swt.custom.StyleRange; +import org.eclipse.swt.widgets.Composite; + +/** + * A factory object for instances of ITextViewerHandle that wraps text viewer used for presenting + * quicksearch matches within content of their containing document. + * + * @see ITextViewerHandle + * @since 1.4 + */ +public interface ITextViewerCreator { + + /** + * Creates a new text viewer under the given SWT parent control and returns handle for it. This method must + * always create new text viewer & handle when called since each opened quick search dialog will call this method + * passing unique parent to display the viewer in the dialog. Dialog then re-uses returned handle + * (viewer) when this ITextViewerCreator is again chosen to present some (possibly other) quicksearch + * match. If other ITextViewerCreator contributor is chosen to present a match in the same dialog, + * this viewer is just made not visible (by means of using {@link StackLayout} in the parent composites hierarchy). + *

+ * It's recommended to use {@link SourceViewerHandle} configured by {@link SourceViewerConfigurer} since they + * implement common aspects expected from quicksearch text viewers. + * + * @param parent the SWT parent control under which to create the viewer's SWT control + * @return a new text viewer handle + */ + ITextViewerHandle createTextViewer(Composite parent); + + /** + * Text viewer handle is a wrapper for a text viewer displayed in quicksearch dialog to present matches. + * + * @see ITextViewerCreator#createTextViewer(Composite) + */ + public interface ITextViewerHandle { + + /** + * Returns number of lines of text the client area of this handle's text viewer would display with its current + * size. Value is used to calculate regions subsequently passed to + * {@link #focusMatch(IRegion, IRegion, int, IRegion) focusMatch()}. + * + * @return number of visible text lines + * @see #focusMatch(IRegion, IRegion, int, IRegion) + */ + int getVisibleLines(); + + /** + * Sets input to the text viewer of this handle. Only if different document is to be presented in this handle's + * viewer, this method is called with new input document. If different match in the already displayed document + * is to be presented, only {@link #focusMatch(IRegion, IRegion, int, IRegion) focusMatch()} is called. + * + * @param document the document to present in the viewer (content of the file) + * @param allMatchesStyles common text styles to apply in order to highlight all found matches in the document + * @param file the file contents of which is the document + * @see #focusMatch(IRegion, IRegion, int, IRegion) + */ + void setViewerInput(IDocument document, StyleRange[] allMatchesStyles, IFile file); + + /** + * Focuses on the specific match (selected amongst quicksearch results) within content of its containing + * document, which was previously set as viewer's input by call to + * {@link #setViewerInput(IDocument, StyleRange[], IFile) setViewerInput()}. This is expected to mean:
+ *

+ * + * Passed regions are adjusted to have selected match vertically centered in the viewer of this handle since + * they are derived from the number of lines presented by the viewer (previous call to + * {@link #getVisibleLines()}). + *

+ * All parameters are coordinates within the input document previously set by call to + * {@link #setViewerInput(IDocument, StyleRange[], IFile) setViewerInput()}. + * + * @param visibleRegion bounds of the portion of input document to present in the viewer + * @param revealedRange bounds of the part of input document to reveal (for vertical scroll) + * @param matchLine line number in the input document where match is located + * @param matchRange bounds of the focused match in the input document (for horizontal scroll) + * @see #getVisibleLines() + * @see #setViewerInput(IDocument, StyleRange[], IFile) + */ + void focusMatch(IRegion visibleRegion, IRegion revealedRange, int matchLine, IRegion matchRange); + } + +} diff --git a/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/ReconcilingAwareSourceViewerHandle.java b/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/ReconcilingAwareSourceViewerHandle.java new file mode 100644 index 00000000000..0e5c8751084 --- /dev/null +++ b/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/ReconcilingAwareSourceViewerHandle.java @@ -0,0 +1,162 @@ +package org.eclipse.text.quicksearch; + +import java.util.Iterator; + +import org.eclipse.core.resources.IFile; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.ITextPresentationListener; +import org.eclipse.jface.text.TextPresentation; +import org.eclipse.jface.text.source.SourceViewer; +import org.eclipse.swt.custom.StyleRange; +import org.eclipse.swt.events.DisposeEvent; +import org.eclipse.swt.events.DisposeListener; +import org.eclipse.swt.widgets.Composite; + +/** + * Extension of {@link SourceViewerHandle} which by means of registering itself as a {@link ITextPresentationListener} + * observes styles applied to source viewer's text from presentation reconcilers and merges quick search matches + * highlighting styles into them. It also collects all these styles to be later able to apply them when the handle + * is asked to focus different match in the document. This is to overcome shortcomings of those source viewers that use + * multiple presentation reconcilers but trigger only some of them on when viewer's visible area is changed on match + * focus. + * + * @since 1.4 + */ +public class ReconcilingAwareSourceViewerHandle extends SourceViewerHandle + implements ITextPresentationListener, DisposeListener{ + + private final boolean fCollectStylesInSetInput; + private final boolean fCollectStylesInFocusMatch; + + private boolean fFirstFocusMatch; + private boolean fDoCollectStyles; + /** + * Helps with applying {@link SourceViewerHandle#fMatchRanges} to some text presentation considering its extent. + */ + private TextPresentation fMatchStylesPresentation; + /** + * Union of styles collected from text presentations previously applied to the viewer. Already have match styles + * merged in. + */ + private TextPresentation fCollectedStylesPresentation; + + /** + * Creates new instance which, depending on collectStylesInSetInput and + * collectStylesInFocusMatch, will also collect applied styles while executing + * {@link #setViewerInput(IDocument, StyleRange[], IFile) setViewerInput()} and + * {@link #focusMatch(IRegion, IRegion, int, IRegion) focusMatch()} respectively. + * + * @param sourceViewerConfigurer the viewer configurer responsible for creation & setup of the viewer + * @param parent the parent SWT control for the viewer + * @param collectStylesInSetInput whether to collect styles also during execution of + * {@link #setViewerInput(IDocument, StyleRange[], IFile) setViewerInput()} method + * @param collectStylesInFocusMatch whether to collect styles also during execution of + * {@link #focusMatch(IRegion, IRegion, int, IRegion) focusMatch()} + */ + public ReconcilingAwareSourceViewerHandle(ISourceViewerConfigurer sourceViewerConfigurer, Composite parent, + boolean collectStylesInSetInput, boolean collectStylesInFocusMatch) { + super(sourceViewerConfigurer, parent); + fCollectStylesInSetInput = collectStylesInSetInput; + fCollectStylesInFocusMatch = collectStylesInFocusMatch; + fSourceViewer.addTextPresentationListener(this); + parent.addDisposeListener(this); + } + + @Override + public void setViewerInput(IDocument document, StyleRange[] matchRangers, IFile file) { + fFirstFocusMatch = true; + fDoCollectStyles = fCollectStylesInSetInput; + if (matchRangers != null && matchRangers.length > 0) { + fMatchStylesPresentation = new TextPresentation(matchRangers.length); + for (StyleRange styleRange : matchRangers) { + fMatchStylesPresentation.addStyleRange((StyleRange) styleRange.clone()); + } + } else { + // should never happen + fMatchStylesPresentation = null; + } + fCollectedStylesPresentation = new TextPresentation(1024); + super.setViewerInput(document, matchRangers, file); + fDoCollectStyles = true; + } + + @Override + public void focusMatch(IRegion visibleRegion, IRegion revealedRange, int matchLine, IRegion matchRange) { + fDoCollectStyles = fCollectStylesInFocusMatch; + try { + super.focusMatch(visibleRegion, revealedRange, matchLine, matchRange); + if (fFirstFocusMatch) { + // on 1st focus we don't need to apply collected styles yet + fFirstFocusMatch = false; + } else if (fCollectedStylesPresentation != null) { // should never be NULL here + fDoCollectStyles = false; + int mergedStyles = fCollectedStylesPresentation.getDenumerableRanges(); + if (mergedStyles > 0) { + var presentation = new TextPresentation(fCollectedStylesPresentation.getCoverage(), mergedStyles); + Iterator iter = fCollectedStylesPresentation.getAllStyleRangeIterator(); + while (iter.hasNext()) { + presentation.addStyleRange((StyleRange) iter.next().clone()); + } + fSourceViewer.changeTextPresentation(presentation, false); + } + } + } finally { + fDoCollectStyles = true; + } + } + + @Override + protected void postFocusMatch() { + // no-op to avoid replacing styles directly on text widget + } + + @Override + public void applyTextPresentation(TextPresentation textPresentation) { + applyMatchStyles(textPresentation, true); // always enrich with match styles + if (fDoCollectStyles && fCollectedStylesPresentation != null) { // should never be NULL here + for (Iterator iter = textPresentation.getAllStyleRangeIterator(); iter.hasNext();) { + // viewer's text widget also replaces style ranges with those arriving last + fCollectedStylesPresentation.replaceStyleRange((StyleRange) iter.next().clone()); + } + } + } + + /** + * Applies all matches highlighting styles previously passed to + * {@link #setViewerInput(IDocument, StyleRange[], IFile) setViewerInput()} method to presentation + * considering presentation's extent. Styles either replace ({@link TextPresentation#replaceStyleRange(StyleRange)}) + * or are merged ({@link TextPresentation#mergeStyleRange(StyleRange)}) with text presentation's styles in the + * particular ranges depending on mergeStyles parameter. + * + * @param mergeStyles true if the styles should be merged, false if they should replace + * text presentation styles in the same ranges + * @see #setViewerInput(IDocument, StyleRange[], IFile) + */ + protected void applyMatchStyles(TextPresentation presentation, boolean mergeStyles) { + if (fMatchStylesPresentation == null) { + // should never happen + return; + } + var extent = presentation.getExtent(); + int extentStart = extent.getOffset(); + fMatchStylesPresentation.setResultWindow(extent); + for (Iterator iter = fMatchStylesPresentation.getAllStyleRangeIterator(); iter.hasNext();) { + var style = iter.next(); + style.start += extentStart; + if (mergeStyles) { + presentation.mergeStyleRange(style); + } else { + presentation.replaceStyleRange(style); + } + } + } + + @Override + public void widgetDisposed(DisposeEvent e) { + fCollectedStylesPresentation = null; + fMatchStylesPresentation = null; + fSourceViewer.removeTextPresentationListener(this); + } + +} diff --git a/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/SourceViewerConfigurer.java b/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/SourceViewerConfigurer.java new file mode 100644 index 00000000000..a0d3dddfa2c --- /dev/null +++ b/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/SourceViewerConfigurer.java @@ -0,0 +1,436 @@ +/******************************************************************************* + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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 + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Jozef Tomek - initial API and implementation + *******************************************************************************/ +package org.eclipse.text.quicksearch; + +import static org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants.EDITOR_CURRENT_LINE; +import static org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants.EDITOR_CURRENT_LINE_COLOR; +import static org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants.EDITOR_LINE_NUMBER_RULER_COLOR; +import static org.eclipse.ui.texteditor.AbstractTextEditor.PREFERENCE_COLOR_BACKGROUND; +import static org.eclipse.ui.texteditor.AbstractTextEditor.PREFERENCE_COLOR_BACKGROUND_SYSTEM_DEFAULT; +import static org.eclipse.ui.texteditor.AbstractTextEditor.PREFERENCE_COLOR_FOREGROUND; +import static org.eclipse.ui.texteditor.AbstractTextEditor.PREFERENCE_COLOR_FOREGROUND_SYSTEM_DEFAULT; +import static org.eclipse.ui.texteditor.AbstractTextEditor.PREFERENCE_COLOR_SELECTION_BACKGROUND; +import static org.eclipse.ui.texteditor.AbstractTextEditor.PREFERENCE_COLOR_SELECTION_BACKGROUND_SYSTEM_DEFAULT; +import static org.eclipse.ui.texteditor.AbstractTextEditor.PREFERENCE_COLOR_SELECTION_FOREGROUND; +import static org.eclipse.ui.texteditor.AbstractTextEditor.PREFERENCE_COLOR_SELECTION_FOREGROUND_SYSTEM_DEFAULT; + +import org.eclipse.core.runtime.Assert; +import org.eclipse.jface.internal.text.source.DiffPainter; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.preference.PreferenceConverter; +import org.eclipse.jface.resource.JFaceResources; +import org.eclipse.jface.text.source.CompositeRuler; +import org.eclipse.jface.text.source.IChangeRulerColumn; +import org.eclipse.jface.text.source.ISharedTextColors; +import org.eclipse.jface.text.source.LineNumberChangeRulerColumn; +import org.eclipse.jface.text.source.SourceViewer; +import org.eclipse.jface.util.IPropertyChangeListener; +import org.eclipse.jface.util.PropertyChangeEvent; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.ISelectionProvider; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.text.quicksearch.SourceViewerHandle.FixedLineHighlighter; +import org.eclipse.ui.editors.text.EditorsUI; +import org.eclipse.ui.texteditor.SourceViewerDecorationSupport; + +/** + * Implementation of source viewer factory for {@link SourceViewerHandle} that creates and does necessary setup of + * source viewer so that it provides common aspects of quicksearch text viewers: + *

+ * + * Actual source viewer instance creation is delegated to provided {@link ISourceViewerCreator}. + * @since 1.3 + */ +public class SourceViewerConfigurer implements ISourceViewerConfigurer { + + public static final int VIEWER_STYLES = SWT.H_SCROLL | SWT.V_SCROLL | SWT.MULTI | SWT.BORDER | SWT.FULL_SELECTION | SWT.READ_ONLY; + private static final String DISABLE_CSS = "org.eclipse.e4.ui.css.disabled"; //$NON-NLS-1$ + + private final ISourceViewerCreator fViewerCreator; + private final IPropertyChangeListener fPropertyChangeListener = this::handlePreferenceStoreChanged; + private final LineNumberChangeRulerColumn fLineNumberRulerColumn = new LineNumberChangeRulerColumn(EditorsUI.getSharedTextColors()); + private final FixedLineHighlighter fMatchLineHighlighter = new FixedLineHighlighter(); + + protected final CompositeRuler fVerticalRuler = new CompositeRuler(); + protected final IPreferenceStore fPreferenceStore; + protected T fSourceViewer; + private Font fFont; + + /** + * Creates new instance that will use viewerCreator as a source viewer factory and common store + * to get preferences from for the viewer configuration. + * @param viewerCreator the factory for actual source viewer + */ + public SourceViewerConfigurer(ISourceViewerCreator viewerCreator) { + this(viewerCreator, EditorsUI.getPreferenceStore()); + } + + /** + * Creates new instance that will use viewerCreator as a source viewer factory and store + * to get preferences from for the viewer configuration. + * @param viewerCreator the factory for actual source viewer + * @param store the preference store to use for configuration + */ + public SourceViewerConfigurer(ISourceViewerCreator viewerCreator, IPreferenceStore store) { + Assert.isNotNull(viewerCreator); + Assert.isNotNull(store); + fViewerCreator = viewerCreator; + fPreferenceStore = store; + } + + @Override + public T getSourceViewer(Composite parent) { + fSourceViewer = fViewerCreator.createSourceViewer(parent, fVerticalRuler, VIEWER_STYLES); + Assert.isNotNull(fSourceViewer); + initialize(); + return fSourceViewer; + } + + @Override + public IChangeRulerColumn getChangeRulerColumn() { + return fLineNumberRulerColumn; + } + + @Override + public FixedLineHighlighter getMatchLineHighlighter() { + return fMatchLineHighlighter; + } + + /** + * Initializes created source viewer to provide common aspects of quicksearch text viewers. + */ + protected void initialize() { + fSourceViewer.getTextWidget().setData(DISABLE_CSS, Boolean.TRUE); + fPreferenceStore.addPropertyChangeListener(fPropertyChangeListener); + + fSourceViewer.addVerticalRulerColumn(fLineNumberRulerColumn); + + initializeColors(); + initializeFont(); + + fSourceViewer.getTextWidget().addLineBackgroundListener(fMatchLineHighlighter); + + var currentLineDecorations = getSourceViewerDecorationSupport(); + currentLineDecorations.install(fPreferenceStore); + + updateContributedRulerColumns((CompositeRuler) fVerticalRuler); + + fSourceViewer.getControl().addDisposeListener(e -> { + currentLineDecorations.uninstall(); + fPreferenceStore.removePropertyChangeListener(fPropertyChangeListener); + }); + } + + /** + * Creates decoration support for the created source viewer. Default implementation returns {@link SourceViewerDecorationSupport}. + * @return decoration support for the source viewer + */ + protected SourceViewerDecorationSupport getSourceViewerDecorationSupport() { + var support = new SourceViewerDecorationSupport(fSourceViewer, null, null, EditorsUI.getSharedTextColors()); + support.setCursorLinePainterPreferenceKeys(EDITOR_CURRENT_LINE, EDITOR_CURRENT_LINE_COLOR); + return support; + } + + /** + * Initializes the fore- and background colors of the created source viewer for both normal and selected text, color + * for line numbers (in vertical ruler) and color for line highlighting. + */ + protected void initializeColors() { + + IPreferenceStore store= getPreferenceStore(); + if (store != null) { + ISharedTextColors sharedColors = EditorsUI.getSharedTextColors(); + var textWidget = fSourceViewer.getTextWidget(); + + // ----------- foreground color -------------------- + Color color= store.getBoolean(PREFERENCE_COLOR_FOREGROUND_SYSTEM_DEFAULT) + ? null + : sharedColors.getColor(getColorFromStore(store, PREFERENCE_COLOR_FOREGROUND)); + textWidget.setForeground(color); + + // ---------- background color ---------------------- + color= store.getBoolean(PREFERENCE_COLOR_BACKGROUND_SYSTEM_DEFAULT) + ? null + : sharedColors.getColor(getColorFromStore(store, PREFERENCE_COLOR_BACKGROUND)); + textWidget.setBackground(color); + + // ----------- selection foreground color -------------------- + color= store.getBoolean(PREFERENCE_COLOR_SELECTION_FOREGROUND_SYSTEM_DEFAULT) + ? null + : sharedColors.getColor(getColorFromStore(store, PREFERENCE_COLOR_SELECTION_FOREGROUND)); + textWidget.setSelectionForeground(color); + + // ---------- selection background color ---------------------- + color= store.getBoolean(PREFERENCE_COLOR_SELECTION_BACKGROUND_SYSTEM_DEFAULT) + ? null + : sharedColors.getColor(getColorFromStore(store, PREFERENCE_COLOR_SELECTION_BACKGROUND)); + textWidget.setSelectionBackground(color); + + fLineNumberRulerColumn.setBackground(textWidget.getBackground()); + + // ----------- line numbers color -------------------- + var lineNumbersColor = getColorFromStore(store, EDITOR_LINE_NUMBER_RULER_COLOR); + if (lineNumbersColor == null) { + lineNumbersColor = new RGB(0, 0, 0); + } + fLineNumberRulerColumn.setForeground(sharedColors.getColor(lineNumbersColor)); + + // ----------- line highlight (background) color -------------------- + color = sharedColors.getColor(getColorFromStore(store, EDITOR_CURRENT_LINE_COLOR)); + fLineNumberRulerColumn.setChangedColor(sharedColors.getColor(reverseInterpolateDiffPainterColor(textWidget.getBackground(), color))); + if (fMatchLineHighlighter != null) { + fMatchLineHighlighter.setHighlightColor(color); + } + } + } + + /** + * Returns color that when set to {@link DiffPainter} makes it to paint quick diff annotation in a change ruler + * column with background color backgroundColor with color equal to finalColor. + * @param backgroundColor background color of the change ruler column + * @param finalColor final desired color of the diff annotation + * @return color to set to diff painter to make it to draw annotation with desired color + */ + @SuppressWarnings("restriction") + public static RGB reverseInterpolateDiffPainterColor(Color backgroundColor, Color finalColor) { + RGB baseRGB= finalColor.getRGB(); + RGB background= backgroundColor.getRGB(); + + boolean darkBase= isDark(baseRGB); + boolean darkBackground= isDark(background); + if (darkBase && darkBackground) + background= new RGB(255, 255, 255); + else if (!darkBase && !darkBackground) + background= new RGB(0, 0, 0); + + // reverse interpolate + double scale = 0.6; + double scaleInv = 1.0 - scale; + return new RGB((int) ((baseRGB.red - scale * background.red) / scaleInv), (int) ((baseRGB.green - scale * background.green) / scaleInv), (int) ((baseRGB.blue - scale * background.blue) / scaleInv)); + } + + + // copy-paste of org.eclipse.jface.internal.text.source.DiffPainter.isDark(RGB) + private static boolean isDark(RGB rgb) { + return greyLevel(rgb) > 128; + } + + // copy-paste of org.eclipse.jface.internal.text.source.DiffPainter.greyLevel(RGB) + private static double greyLevel(RGB rgb) { + if (rgb.red == rgb.green && rgb.green == rgb.blue) + return rgb.red; + return (0.299 * rgb.red + 0.587 * rgb.green + 0.114 * rgb.blue + 0.5); + } + + /** + * Gets color preference configured in store under key. + * @param store preference store + * @param key color preference key + * @return color preference + */ + protected RGB getColorFromStore(IPreferenceStore store, String key) { + RGB rgb = null; + if (store.contains(key)) { + if (store.isDefault(key)) { + rgb = PreferenceConverter.getDefaultColor(store, key); + } else { + rgb = PreferenceConverter.getColor(store, key); + } + } + return rgb; + } + + private void initializeFont() { + + boolean isSharedFont= true; + Font font= null; + String symbolicFontName= getSymbolicFontName(); + + if (symbolicFontName != null) + font= JFaceResources.getFont(symbolicFontName); + else if (fPreferenceStore != null) { + // Backward compatibility + if (fPreferenceStore.contains(JFaceResources.TEXT_FONT) && !fPreferenceStore.isDefault(JFaceResources.TEXT_FONT)) { + FontData data= PreferenceConverter.getFontData(fPreferenceStore, JFaceResources.TEXT_FONT); + + if (data != null) { + isSharedFont= false; + font= new Font(fSourceViewer.getTextWidget().getDisplay(), data); + } + } + } + if (font == null) + font= JFaceResources.getTextFont(); + + if (!font.equals(fSourceViewer.getTextWidget().getFont())) { + setFont(font); + + disposeFont(); + if (!isSharedFont) + fFont= font; + } else if (!isSharedFont) { + font.dispose(); + } + } + + // Sets the font for the source viewer sustaining selection and scroll position. + private void setFont(Font font) { + if (fSourceViewer.getDocument() != null) { + + ISelectionProvider provider= fSourceViewer.getSelectionProvider(); + ISelection selection= provider.getSelection(); + int topIndex= fSourceViewer.getTopIndex(); + + Control parent= fSourceViewer.getControl(); + parent.setRedraw(false); + + fSourceViewer.getTextWidget().setFont(font); + + fVerticalRuler.setFont(font); + + provider.setSelection(selection); + fSourceViewer.setTopIndex(topIndex); + + if (parent instanceof Composite composite) { + composite.layout(true); + } + + parent.setRedraw(true); + } else { + fSourceViewer.getTextWidget().setFont(font); + fVerticalRuler.setFont(font); + } + } + + private void disposeFont() { + if (fFont != null) { + fFont.dispose(); + fFont= null; + } + } + + /** + * Returns the property preference key for the viewer font. + *

+ * If this configurer provides non-null {@link #getSymbolicFontName() symbolicFontName} then this name + * is returned, otherwise {@link JFaceResources#TEXT_FONT} is returned. + *

+ * + * @return key in the preference store for the font used in the source viewer + */ + private final String getFontPropertyPreferenceKey() { + String symbolicFontName= getSymbolicFontName(); + if (symbolicFontName != null) + return symbolicFontName; + return JFaceResources.TEXT_FONT; + } + + /** + * Returns custom symbolic name for the font to be used in the source viewer. By default returns null + * in which case default text viewer font is used. + * @return custom symbolic font name or null for default font use + */ + protected String getSymbolicFontName() { + return null; + } + + /** + * Returns preference store used for source viewer configuration. + * + * @return this configurer's preference store which may be null + */ + protected final IPreferenceStore getPreferenceStore() { + return fPreferenceStore; + } + + /** + * Adds additional ruler contributions to the vertical ruler. + *

+ * Default implementation does nothing, clients may replace.

+ * + * @param ruler the composite ruler to add contributions to + */ + protected void updateContributedRulerColumns(CompositeRuler ruler) { + // no-op in default implementation + } + + /** + * Handles a property change event describing a change of the preference store and updates the preference related + * source viewer properties. + *

+ * Subclasses may extend. + *

+ * + * @param event the property change event + */ + protected void handlePreferenceStoreChanged(PropertyChangeEvent event) { + String property= event.getProperty(); + + if (getFontPropertyPreferenceKey().equals(property)) { + initializeFont(); + return; + } + + if (property != null) { + switch (property) { + case PREFERENCE_COLOR_FOREGROUND: + case PREFERENCE_COLOR_FOREGROUND_SYSTEM_DEFAULT: + case PREFERENCE_COLOR_BACKGROUND: + case PREFERENCE_COLOR_BACKGROUND_SYSTEM_DEFAULT: + case PREFERENCE_COLOR_SELECTION_FOREGROUND: + case PREFERENCE_COLOR_SELECTION_FOREGROUND_SYSTEM_DEFAULT: + case PREFERENCE_COLOR_SELECTION_BACKGROUND: + case PREFERENCE_COLOR_SELECTION_BACKGROUND_SYSTEM_DEFAULT: + case EDITOR_LINE_NUMBER_RULER_COLOR: + initializeColors(); + return; + default: + break; + } + } + } + + /** + * Factory creating actual source viewer subsequently configured by {@link SourceViewerConfigurer} to provide + * common aspects of quicksearch text viewers. + * @see SourceViewerConfigurer + * @since 1.3 + */ + public interface ISourceViewerCreator { + + /** + * Creates new source viewer with verticalRuler and styles under passed + * parent. + * @param parent the parent SWT control for the viewer + * @param verticalRuler the vertical ruler to add to the created viewer + * @param styles the SWT styles for the viewer + * @return + */ + T createSourceViewer(Composite parent, CompositeRuler verticalRuler, int styles); + } + +} diff --git a/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/SourceViewerHandle.java b/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/SourceViewerHandle.java new file mode 100644 index 00000000000..ec850e6bde4 --- /dev/null +++ b/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/SourceViewerHandle.java @@ -0,0 +1,346 @@ +/******************************************************************************* + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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 + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Jozef Tomek - initial API and implementation + *******************************************************************************/ +package org.eclipse.text.quicksearch; + +import java.util.Collections; +import java.util.Iterator; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.Assert; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.CursorLinePainter; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.Position; +import org.eclipse.jface.text.Region; +import org.eclipse.jface.text.TextPresentation; +import org.eclipse.jface.text.source.Annotation; +import org.eclipse.jface.text.source.IAnnotationModel; +import org.eclipse.jface.text.source.IAnnotationModelListener; +import org.eclipse.jface.text.source.IChangeRulerColumn; +import org.eclipse.jface.text.source.ILineDiffInfo; +import org.eclipse.jface.text.source.ILineDiffer; +import org.eclipse.jface.text.source.SourceViewer; +import org.eclipse.swt.custom.LineBackgroundEvent; +import org.eclipse.swt.custom.LineBackgroundListener; +import org.eclipse.swt.custom.StyleRange; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.text.quicksearch.ITextViewerCreator.ITextViewerHandle; +import org.eclipse.text.quicksearch.internal.ui.QuickSearchActivator; + +/** + * Implementation of {@link ITextViewerHandle} that provides common aspects expected from quicksearch text viewers: + *
    + *
  • focusing on selected quicksearch match as described in the documentation of + * {@link ITextViewerHandle#focusMatch(IRegion, IRegion, int, IRegion) focusMatch()} + *
  • highlighting all matches in the presented content by applying common text style through + * {@link StyledText#setStyleRange(StyleRange)} on the viewert's text widget. + *
+ *

+ * Highlighting the selected match line is achieved by: + *

    + *
  • highlighting line number in the line numbers vertical ruler column by announcing it as a changed line for the + * quick diff feature (if a viewer provides {@link IChangeRulerColumn}) + *
  • highlighting whole line by using {@link FixedLineHighlighter} (if a viewer provides one) + *
+ *

+ * This class uses {@link ISourceViewerConfigurer} that does viewer creation and setup necessary for + * aforementioned aspects. + * + * @since 1.4 + */ +public class SourceViewerHandle implements ITextViewerHandle { + + /** + * Source viewer created by {@link ISourceViewerConfigurer}. + */ + protected final T fSourceViewer; + /** + * Optional change ruler column provided by {@link ISourceViewerConfigurer}. + */ + protected final IChangeRulerColumn fChangeRulerColumn; + /** + * Optional fixed line change annotation model for {@link #fChangeRulerColumn} in case it is provided. + */ + protected final FixedLineChangedAnnotationModel fFixedLineChangeModel; + /** + * Optional fixed line highlighter provided by {@link ISourceViewerConfigurer}. + */ + protected final FixedLineHighlighter fMatchLineHighlighter; + /** + * Highlighting styles for all matches in the input document previously passed to + * {@link #setViewerInput(IDocument, StyleRange[], IFile) setViewerInput()}. Styles can be subsequently applied to + * source viewer's text widget using {@link #applyMatchesStyles()} (called from default implementation of + * {@link #focusMatch(IRegion, IRegion, int, IRegion) focusMatch()}) or {@link #applyMatchStyles(TextPresentation)}. + */ + protected StyleRange[] fMatchRanges = null; + + /** + * Creates new instance that will use sourceViewerConfigurer to create viewer under the + * parent. + * + * @param sourceViewerConfigurer the viewer configurer responsible for creation & setup of the viewer + * @param parent the parent SWT control for the viewer + */ + public SourceViewerHandle(ISourceViewerConfigurer sourceViewerConfigurer, Composite parent) { + Assert.isNotNull(sourceViewerConfigurer); + fSourceViewer = sourceViewerConfigurer.getSourceViewer(parent); + Assert.isNotNull(fSourceViewer); + fChangeRulerColumn = sourceViewerConfigurer.getChangeRulerColumn(); + fMatchLineHighlighter = sourceViewerConfigurer.getMatchLineHighlighter(); + if (fChangeRulerColumn != null) { + fFixedLineChangeModel = new FixedLineChangedAnnotationModel(); + fChangeRulerColumn.setModel(fFixedLineChangeModel); + } else { + fFixedLineChangeModel = null; + } + } + + @Override + public int getVisibleLines() { + StyledText details = fSourceViewer.getTextWidget(); + if (details != null && !details.isDisposed()) { + int lineHeight = details.getLineHeight(); + int areaHeight = details.getClientArea().height; + return (areaHeight + lineHeight - 1) / lineHeight; + } + return 0; + } + + @Override + public void setViewerInput(IDocument document, StyleRange[] allMatchesStyles, IFile file) { + fMatchRanges = allMatchesStyles; + fSourceViewer.setInput(document); + } + + /** + * Called from {@link #setViewerInput(IDocument, StyleRange[], IFile) setViewerInput()} to remember highlighting + * styles for all matches in the input document. Styles can be subsequently applied to source viewer's text widget + * using {@link #applyMatchesStyles()} (called from default implementation of + * {@link #focusMatch(IRegion, IRegion, int, IRegion) focusMatch()}) or {@link #applyMatchStyles(TextPresentation)}. + * @param allMatchesStyles common text styles to apply in order to highlight all found matches in the document file + * the file contents of which is the document + */ + protected void setMatchesStyles(StyleRange[] allMatchesStyles) { + } + + @Override + public void focusMatch(IRegion visibleRegion, IRegion revealedRange, int matchLine, IRegion matchRange) { + // limit content of the document that we can scroll to + if (!fSourceViewer.getVisibleRegion().equals(visibleRegion)) { + fSourceViewer.setVisibleRegion(visibleRegion.getOffset(), visibleRegion.getLength()); + } + // scroll to range to be presented + fSourceViewer.revealRange(revealedRange.getOffset(), revealedRange.getLength()); + // sets caret position + fSourceViewer.setSelectedRange(matchRange.getOffset(), 0); + // does horizontal scrolling if necessary to reveal 1st occurrence in target line + fSourceViewer.revealRange(matchRange.getOffset(), matchRange.getLength()); + + if (fMatchLineHighlighter != null) { + try { + fMatchLineHighlighter.setTargetLineOffset(fSourceViewer.getDocument().getLineOffset(matchLine) - visibleRegion.getOffset()); + } catch (BadLocationException e) { + QuickSearchActivator.log(e); + } + } + + if (fFixedLineChangeModel != null) { + fFixedLineChangeModel.selectedMatchLine = matchLine; + fChangeRulerColumn.redraw(); + } + postFocusMatch(); + } + + /** + * Called at the end of {@link #focusMatch(IRegion, IRegion, int, IRegion) focusMatch()} execution. Applies matches styles via + * {@link #applyMatchesStyles()} by default. + */ + protected void postFocusMatch() { + applyMatchesStyles(); + + } + + /** + * Applies all matches highlighting styles previously passed to + * {@link #setViewerInput(IDocument, StyleRange[], IFile) setViewerInput()} method to source viewer's text widget + * considering projection of model (document) ranges to text widget ranges. Styles are set using + * {@link StyledText#setStyleRange(StyleRange)} thus replacing existing styles in the range. + * + * @see #setViewerInput(IDocument, StyleRange[], IFile) + */ + protected void applyMatchesStyles() { + if (fMatchRanges == null || fMatchRanges.length == 0) { + return; + } + for (StyleRange styleRange : fMatchRanges) { + styleRange = modelRange2WidgetStyleRange(styleRange); + if (styleRange != null) { + fSourceViewer.getTextWidget().setStyleRange(styleRange); + } + } + } + + private StyleRange modelRange2WidgetStyleRange(StyleRange range) { + IRegion region= fSourceViewer.modelRange2WidgetRange(new Region(range.start, range.length)); + if (region != null) { + StyleRange result= (StyleRange) range.clone(); + result.start= region.getOffset(); + result.length= region.getLength(); + return result; + } + return null; + } + + /** + * Annotation model implementation that announces single configured line as {@link ILineDiffInfo#CHANGED} for + * quick diff feature of the viewer's {@link IChangeRulerColumn}. + */ + public static class FixedLineChangedAnnotationModel implements IAnnotationModel, ILineDiffer { + + protected int selectedMatchLine; + + @Override + public void addAnnotationModelListener(IAnnotationModelListener listener) { + // no-op + } + + @Override + public void removeAnnotationModelListener(IAnnotationModelListener listener) { + // no-op + } + + @Override + public void connect(IDocument document) { + // no-op + } + + @Override + public void disconnect(IDocument document) { + // no-op + } + + @Override + public void addAnnotation(Annotation annotation, Position position) { + // no-op + } + + @Override + public void removeAnnotation(Annotation annotation) { + // no-op + } + + @Override + public Iterator getAnnotationIterator() { + return Collections.emptyIterator(); + } + + @Override + public Position getPosition(Annotation annotation) { + return null; + } + + @Override + public ILineDiffInfo getLineInfo(int line) { + return line == selectedMatchLine ? FixedLineChangedDiffInfo.INSTANCE : null; + } + + @Override + public void revertLine(int line) throws BadLocationException { + // no-op + } + + @Override + public void revertBlock(int line) throws BadLocationException { + // no-op + } + + @Override + public void revertSelection(int line, int nLines) throws BadLocationException { + // no-op + } + + @Override + public int restoreAfterLine(int line) throws BadLocationException { + // no-op + return 0; + } + + } + + private static class FixedLineChangedDiffInfo implements ILineDiffInfo { + + static final FixedLineChangedDiffInfo INSTANCE = new FixedLineChangedDiffInfo(); + + @Override + public int getRemovedLinesBelow() { + return 0; + } + + @Override + public int getRemovedLinesAbove() { + return 0; + } + + @Override + public int getChangeType() { + return CHANGED; + } + + @Override + public boolean hasChanges() { + return true; + } + + @Override + public String[] getOriginalText() { + return new String[0]; + } + + } + + /** + * A line background listener that provides the color that is used for current line highlighting (what + * {@link CursorLinePainter} does) but for single fixed line only and does so always regardless of show current + * line highlighting on/off preference. + * + * @see CursorLinePainter + */ + public static class FixedLineHighlighter implements LineBackgroundListener { + + private int lineOffset = -1; + private Color highlightColor; + + public void setHighlightColor(Color highlightColor) { + this.highlightColor = highlightColor; + } + + public void setTargetLineOffset(int lineOffset) { + this.lineOffset = lineOffset; + } + + @Override + public void lineGetBackground(LineBackgroundEvent event) { + if (lineOffset == event.lineOffset) { + event.lineBackground = highlightColor; + } + } + + } + +} diff --git a/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/core/QuickTextQuery.java b/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/core/QuickTextQuery.java index 552912ab95c..a0ec3f370dd 100644 --- a/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/core/QuickTextQuery.java +++ b/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/core/QuickTextQuery.java @@ -165,7 +165,7 @@ public boolean isTrivial() { @Override public String toString() { - return "QTQuery("+orgPattern+", "+(caseInsensitive?"caseSens":"caseInSens")+")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ + return "QTQuery("+orgPattern+", "+(caseInsensitive?"caseInSens":"caseSens")+")"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ } public List findAll(String text) { diff --git a/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/ui/DefaultSourceViewerCreator.java b/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/ui/DefaultSourceViewerCreator.java new file mode 100644 index 00000000000..062fa0c9c8a --- /dev/null +++ b/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/ui/DefaultSourceViewerCreator.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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 + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Jozef Tomek - initial API and implementation + *******************************************************************************/ +package org.eclipse.text.quicksearch.internal.ui; +import org.eclipse.jface.text.source.SourceViewer; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.text.quicksearch.ITextViewerCreator; +import org.eclipse.text.quicksearch.SourceViewerConfigurer; +import org.eclipse.text.quicksearch.SourceViewerHandle; + +/** + * Creates quicksearch text viewer handles that use {@link DefaultSourceViewer}. + * Used as a fallback by Quick Search plugin-in to display text file content if no better viewer is available. + * + * @see ITextViewerCreator + */ +public class DefaultSourceViewerCreator implements ITextViewerCreator { + + @Override + public ITextViewerHandle createTextViewer(Composite parent) { + return new SourceViewerHandle<>(new SourceViewerConfigurer<>(SourceViewer::new), parent); + } + +} diff --git a/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/ui/ExtensionsRegistry.java b/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/ui/ExtensionsRegistry.java new file mode 100644 index 00000000000..41c09e3050d --- /dev/null +++ b/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/ui/ExtensionsRegistry.java @@ -0,0 +1,104 @@ +package org.eclipse.text.quicksearch.internal.ui; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.StringTokenizer; + +import org.eclipse.core.runtime.IConfigurationElement; +import org.eclipse.core.runtime.content.IContentType; + +class ExtensionsRegistry { + private final static String ID_ATTRIBUTE = "id"; //$NON-NLS-1$ + private final static String EXTENSIONS_ATTRIBUTE = "extensions"; //$NON-NLS-1$ + private final static String CONTENT_TYPE_ID_ATTRIBUTE = "contentTypeId"; //$NON-NLS-1$ + + private HashMap fIdMap; // maps ids to data + private HashMap> fExtensionMap; // multimap: maps extensions to list of data + private HashMap> fContentTypeBindings; // multimap: maps content type bindings to list of data + + void register(IConfigurationElement element, T data) { + String id = element.getAttribute(ID_ATTRIBUTE); + if (id != null) { + if (fIdMap == null) + fIdMap = new HashMap<>(); + fIdMap.put(id, data); + } + + String types = element.getAttribute(EXTENSIONS_ATTRIBUTE); + if (types != null) { + if (fExtensionMap == null) + fExtensionMap = new HashMap<>(); + StringTokenizer tokenizer = new StringTokenizer(types, ","); //$NON-NLS-1$ + while (tokenizer.hasMoreElements()) { + String extension = tokenizer.nextToken().trim(); + List l = fExtensionMap.get(normalizeCase(extension)); + if (l == null) + fExtensionMap.put(normalizeCase(extension), l = new ArrayList<>()); + l.add(data); + } + } + } + + void createBinding(IConfigurationElement element, String idAttributeName) { + String type = element.getAttribute(CONTENT_TYPE_ID_ATTRIBUTE); + String id = element.getAttribute(idAttributeName); + if (id == null) + QuickSearchActivator.log(QuickSearchActivator.getFormattedString("QuickSearchActivator.targetIdAttributeMissing", idAttributeName)); //$NON-NLS-1$ + if (type != null && id != null && fIdMap != null) { + T o = fIdMap.get(id); + if (o != null) { + IContentType ct = QuickSearchActivator.getDefault().getContentType(type); + if (ct != null) { + if (fContentTypeBindings == null) + fContentTypeBindings = new HashMap<>(); + List l = fContentTypeBindings.get(ct); + if (l == null) + fContentTypeBindings.put(ct, l = new ArrayList<>()); + l.add(o); + } else { + QuickSearchActivator.log(QuickSearchActivator.getFormattedString("QuickSearchActivator.contentTypeNotFound", type)); //$NON-NLS-1$ + } + } else { + QuickSearchActivator.log(QuickSearchActivator.getFormattedString("QuickSearchActivator.targetNotFound", id)); //$NON-NLS-1$ + } + } + } + + T search(IContentType type) { + List list = searchAll(type); + return list != null ? list.get(0) : null; + } + + List searchAll(IContentType type) { + if (fContentTypeBindings != null) { + for (; type != null; type = type.getBaseType()) { + List data = fContentTypeBindings.get(type); + if (data != null) + return data; + } + } + return null; + } + + T search(String extension) { + List list = searchAll(extension); + return list != null ? list.get(0) : null; + } + + List searchAll(String extension) { + if (fExtensionMap != null) + return fExtensionMap.get(normalizeCase(extension)); + return null; + } + + Collection getAll() { + return fIdMap == null ? Collections.emptySet() : fIdMap.values(); + } + + private static String normalizeCase(String s) { + return s == null ? null : s.toUpperCase(); + } +} \ No newline at end of file diff --git a/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/ui/IViewerDescriptor.java b/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/ui/IViewerDescriptor.java new file mode 100644 index 00000000000..a49a0a9cf05 --- /dev/null +++ b/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/ui/IViewerDescriptor.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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 + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Jozef Tomek - initial API and implementation + *******************************************************************************/ +package org.eclipse.text.quicksearch.internal.ui; + +import org.eclipse.text.quicksearch.ITextViewerCreator; + +interface IViewerDescriptor { + + /** + * Creates a new viewer creator from this descriptor. + * + * @return a new viewer creator. + */ + ITextViewerCreator getViewerCreator(); + + /** + * Returns label for viewer created by provided viewer creator that will be used in UI. + * + * @return label for provided viewer used in UI + */ + String getLabel(); + +} diff --git a/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/ui/QuickSearchActivator.java b/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/ui/QuickSearchActivator.java index c862c05edf7..4c839bf8ff6 100644 --- a/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/ui/QuickSearchActivator.java +++ b/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/ui/QuickSearchActivator.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2013-2019 Pivotal Software, Inc. + * Copyright (c) 2013-2025 Pivotal Software, Inc. and others * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v2.0 * which accompanies this distribution, and is available at @@ -9,13 +9,44 @@ * * Contributors: * Pivotal Software, Inc. - initial API and implementation + * Jozef Tomek - text viewers extension *******************************************************************************/ package org.eclipse.text.quicksearch.internal.ui; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Hashtable; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.MissingResourceException; +import java.util.Optional; +import java.util.ResourceBundle; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.Adapters; +import org.eclipse.core.runtime.Assert; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IAdaptable; +import org.eclipse.core.runtime.IConfigurationElement; +import org.eclipse.core.runtime.IExtensionRegistry; import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.content.IContentDescription; +import org.eclipse.core.runtime.content.IContentType; +import org.eclipse.core.runtime.content.IContentTypeManager; import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.graphics.Image; import org.eclipse.text.quicksearch.internal.core.preferences.QuickSearchPreferences; +import org.eclipse.ui.IEditorDescriptor; +import org.eclipse.ui.IEditorRegistry; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.model.IWorkbenchAdapter; import org.eclipse.ui.plugin.AbstractUIPlugin; import org.osgi.framework.BundleContext; @@ -30,7 +61,22 @@ public class QuickSearchActivator extends AbstractUIPlugin { // The shared instance private static QuickSearchActivator plugin; - private QuickSearchPreferences prefs = null; //Lazy initialized + private static final String TEXT_VIEWERS_EXTENSION_POINT= "textViewers"; //$NON-NLS-1$ + private static final String CONTENT_TYPE_BINDING = "contentTypeBinding"; //$NON-NLS-1$ + private static final String VIEWER_TAG = "viewer"; //$NON-NLS-1$ + private static final String VIEWER_ID_ATTRIBUTE = "viewerId"; //$NON-NLS-1$ + private static final String DEFAULT_CREATOR_CLASS = DefaultSourceViewerCreator.class.getName(); + + private final ExtensionsRegistry fTextViewersRegistry = new ExtensionsRegistry<>(); + private final Map fgImages= new Hashtable<>(10); + private final List fgDisposeOnShutdownImages= new ArrayList<>(); + + // Lazy initialized + private QuickSearchPreferences prefs = null; + private IContentTypeManager contentTypeManager; + private ResourceBundle resourceBundle; + private boolean registryInitialized; + private IViewerDescriptor defaultViewerDescriptor; /** * The constructor @@ -46,6 +92,7 @@ public QuickSearchActivator() { public void start(BundleContext context) throws Exception { super.start(context); plugin = this; + contentTypeManager = Platform.getContentTypeManager(); } /* @@ -55,6 +102,11 @@ public void start(BundleContext context) throws Exception { @Override public void stop(BundleContext context) throws Exception { plugin = null; + for (Image img : fgDisposeOnShutdownImages) { + if (!img.isDisposed()) { + img.dispose(); + } + } super.stop(context); } @@ -82,6 +134,10 @@ public static void log(Throwable exception) { log(createErrorStatus(exception)); } + public static void log(String message) { + log(new Status(IStatus.ERROR, PLUGIN_ID, 0, message, null)); + } + public static void log(IStatus status) { // if (logger == null) { getDefault().getLog().log(status); @@ -102,4 +158,202 @@ public QuickSearchPreferences getPreferences() { return prefs; } + private ResourceBundle getResourceBundle() { + if (resourceBundle == null) + resourceBundle = Platform.getResourceBundle(getBundle()); + return resourceBundle; + } + + static String getFormattedString(String key, String arg) { + try { + return MessageFormat.format(getDefault().getResourceBundle().getString(key), arg); + } catch (MissingResourceException e) { + return "!" + key + "!"; //$NON-NLS-2$ //$NON-NLS-1$ + } + } + + private static String getFormattedString(String key, String arg0, String arg1) { + try { + return MessageFormat.format(getDefault().getResourceBundle().getString(key), arg0, arg1); + } catch (MissingResourceException e) { + return "!" + key + "!";//$NON-NLS-2$ //$NON-NLS-1$ + } + } + + private void initializeExtensionsRegistry() { + if (!registryInitialized) { + registerExtensions(); + Assert.isNotNull(defaultViewerDescriptor); + registryInitialized = true; + } + } + + private void registerExtensions() { + IExtensionRegistry registry = Platform.getExtensionRegistry(); + + // collect all descriptors which define the source viewer extension point + IConfigurationElement[] elements = registry.getConfigurationElementsFor(PLUGIN_ID, TEXT_VIEWERS_EXTENSION_POINT); + for (IConfigurationElement element : elements) { + String name = element.getName(); + if (!CONTENT_TYPE_BINDING.equals(name)) { + if (!VIEWER_TAG.equals(name)) + log(getFormattedString("QuickSearchActivator.unexpectedTag", name, VIEWER_TAG)); //$NON-NLS-1$ + var viewerDescriptor = new ViewerDescriptor(element); + fTextViewersRegistry.register(element, viewerDescriptor); + if (DEFAULT_CREATOR_CLASS.equals(viewerDescriptor.getViewerClass())) { + defaultViewerDescriptor = viewerDescriptor; + } + } + } + for (IConfigurationElement element : elements) { + if (CONTENT_TYPE_BINDING.equals(element.getName())) + fTextViewersRegistry.createBinding(element, VIEWER_ID_ATTRIBUTE); + } + } + + IContentType getContentType(String contentTypeIdentifier) { + return contentTypeManager.getContentType(contentTypeIdentifier); + } + + IViewerDescriptor getDefaultViewer() { + initializeExtensionsRegistry(); + return defaultViewerDescriptor; + } + + List getViewers(IFile file) { + initializeExtensionsRegistry(); + if (file == null) { + return List.of(getDefaultViewer()); + } + return findContentViewerDescriptor(file); + } + + private List findContentViewerDescriptor(IFile input) { + LinkedHashSet result = new LinkedHashSet<>(); + + String name = input.getName(); + + IContentDescription cDescr = null; + try { + cDescr = input.getContentDescription(); + } catch (CoreException e) { + log(e); + } + IContentType ctype = cDescr == null ? null : cDescr.getContentType(); + if (ctype == null) { + ctype = contentTypeManager.findContentTypeFor(name); + } + if (ctype != null) { + List list = fTextViewersRegistry.searchAll(ctype); + if (list != null) { + result.addAll(list); + } + } + + String type = input.getFileExtension(); + if (type != null) { + List list = fTextViewersRegistry.searchAll(type); + if (list != null) + result.addAll(list); + } + + Set editorLinkedDescriptors = findEditorLinkedDescriptors(name, ctype, false); + result.addAll(editorLinkedDescriptors); + + if (result.isEmpty() || result.size() == 1) { + // single candidate should always be the default viewer, but in case it's not, add default viewer as well + result.add(defaultViewerDescriptor); + } else { + // more than 1 candidate, make sure default viewer is the last one + result.remove(defaultViewerDescriptor); + result.add(defaultViewerDescriptor); + } + return new ArrayList<>(result); + } + + /** + * @param fileName file name for content in search match preview panel + * @param contentType possible content type for content in search match preview panel, may be null + * @param firstIsEnough stop searching once first match is found + * @return set of descriptors which could be found for given content type via "linked" editor + */ + private Set findEditorLinkedDescriptors(String fileName, IContentType contentType, + boolean firstIsEnough) { + if (fileName == null && contentType == null) { + return Collections.emptySet(); + } + if (contentType == null) { + contentType = contentTypeManager.findContentTypeFor(fileName); + } + + LinkedHashSet viewers = fTextViewersRegistry.getAll().stream() + .filter(vd -> vd.getLinkedEditorId() != null).collect(Collectors.toCollection(LinkedHashSet::new)); + if (viewers.isEmpty()) { + return Collections.emptySet(); + } + + IEditorRegistry editorReg = PlatformUI.getWorkbench().getEditorRegistry(); + LinkedHashSet result = new LinkedHashSet<>(); + IEditorDescriptor[] editors = editorReg.getEditors(fileName, contentType); + for (IEditorDescriptor ed : editors) { + addLinkedEditorContentTypes(viewers, firstIsEnough, ed.getId(), result); + if (firstIsEnough && !result.isEmpty()) { + return result; + } + } + return result; + } + + private void addLinkedEditorContentTypes(LinkedHashSet viewers, boolean firstIsEnough, + String editorId, Set result) { + Stream stream = viewers.stream().filter(vd -> editorId.equals(vd.getLinkedEditorId())); + if (firstIsEnough) { + Optional first = stream.findFirst(); + if (first.isPresent()) { + result.add(first.get()); + } + } else { + stream.collect(Collectors.toCollection(() -> result)); + } + } + + /** + * Returns a shared image for the given adaptable. + * This convenience method queries the given adaptable + * for its IWorkbenchAdapter.getImageDescriptor, which it + * uses to create an image if it does not already have one. + *

+ * Note: Images returned from this method will be automatically disposed + * of when this plug-in shuts down. Callers must not dispose of these + * images themselves. + *

+ * + * @param adaptable the adaptable for which to find an image + * @return an image + */ + public Image getImage(IAdaptable adaptable) { + if (adaptable != null) { + IWorkbenchAdapter o= Adapters.adapt(adaptable, IWorkbenchAdapter.class); + if (o == null) { + return null; + } + ImageDescriptor id= o.getImageDescriptor(adaptable); + if (id != null) { + Image image= fgImages.get(id); + if (image == null) { + image= id.createImage(); + try { + fgImages.put(id, image); + } catch (NullPointerException e) { + // NeedWork + } + fgDisposeOnShutdownImages.add(image); + + } + return image; + } + } + return null; + } + } diff --git a/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/ui/QuickSearchDialog.java b/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/ui/QuickSearchDialog.java index 0cce0ad5b65..883304e22d6 100644 --- a/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/ui/QuickSearchDialog.java +++ b/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/ui/QuickSearchDialog.java @@ -17,59 +17,68 @@ * Simon Muschel - bug 258493 * Kris De Volder Copied and modified from org.eclipse.ui.dialogs.FilteredResourcesSelectionDialog * to create QuickSearchDialog + * Jozef Tomek - text viewers extension *******************************************************************************/ package org.eclipse.text.quicksearch.internal.ui; -import static org.eclipse.jface.resource.JFaceResources.TEXT_FONT; - import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Deque; +import java.util.HashMap; +import java.util.IdentityHashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; import org.eclipse.core.commands.AbstractHandler; import org.eclipse.core.commands.ExecutionEvent; import org.eclipse.core.commands.IHandler; import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.ICoreRunnable; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.GroupMarker; import org.eclipse.jface.action.IAction; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.LegacyActionTools; import org.eclipse.jface.action.MenuManager; +import org.eclipse.jface.action.Separator; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jface.dialogs.IDialogSettings; import org.eclipse.jface.layout.GridDataFactory; -import org.eclipse.jface.resource.JFaceResources; +import org.eclipse.jface.layout.GridLayoutFactory; +import org.eclipse.jface.layout.RowLayoutFactory; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.Region; import org.eclipse.jface.viewers.ILazyContentProvider; import org.eclipse.jface.viewers.IStructuredContentProvider; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.StyledCellLabelProvider; import org.eclipse.jface.viewers.StyledString; -import org.eclipse.jface.viewers.StyledString.Styler; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.TableViewerColumn; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.viewers.ViewerCell; +import org.eclipse.jface.viewers.StyledString.Styler; import org.eclipse.osgi.util.NLS; import org.eclipse.search.internal.ui.text.EditorOpener; import org.eclipse.swt.SWT; import org.eclipse.swt.accessibility.ACC; import org.eclipse.swt.accessibility.AccessibleAdapter; import org.eclipse.swt.accessibility.AccessibleEvent; +import org.eclipse.swt.custom.CLabel; import org.eclipse.swt.custom.SashForm; +import org.eclipse.swt.custom.StackLayout; import org.eclipse.swt.custom.StyleRange; -import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.events.ControlAdapter; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.KeyAdapter; @@ -82,6 +91,7 @@ import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; @@ -89,7 +99,6 @@ import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; -import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.Table; @@ -97,14 +106,16 @@ import org.eclipse.swt.widgets.Text; import org.eclipse.swt.widgets.ToolBar; import org.eclipse.swt.widgets.ToolItem; +import org.eclipse.text.quicksearch.ITextViewerCreator.ITextViewerHandle; import org.eclipse.text.quicksearch.internal.core.LineItem; import org.eclipse.text.quicksearch.internal.core.QuickTextQuery; -import org.eclipse.text.quicksearch.internal.core.QuickTextQuery.TextRange; import org.eclipse.text.quicksearch.internal.core.QuickTextSearchRequestor; import org.eclipse.text.quicksearch.internal.core.QuickTextSearcher; +import org.eclipse.text.quicksearch.internal.core.QuickTextQuery.TextRange; import org.eclipse.text.quicksearch.internal.core.pathmatch.ResourceMatchers; import org.eclipse.text.quicksearch.internal.util.DocumentFetcher; import org.eclipse.ui.ActiveShellExpression; +import org.eclipse.ui.ISharedImages; import org.eclipse.ui.IWorkbenchCommandConstants; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.IWorkbenchWindow; @@ -332,15 +343,13 @@ public void update(ViewerCell cell) { private TableViewer list; - private MenuManager menuManager; + private MenuManager viewMenuManager; private MenuManager contextMenuManager; private boolean multi; - private ToolBar toolBar; - - private ToolItem toolItem; + private ToolBar viewMenuToolBar; private Label progressLabel; @@ -359,8 +368,6 @@ public void update(ViewerCell cell) { private QuickTextSearcher searcher; - private StyledText details; - private DocumentFetcher documents; @@ -379,6 +386,18 @@ public void update(ViewerCell cell) { private Combo searchIn; private Label listLabel; + private IDocument lastDocument; + private Composite viewersParent; + private StackLayout viewersParentLayout; + private final Map viewerWrappers = new IdentityHashMap<>(); + private TextViewerWrapper currentViewerWrapper; + private static final Map SELECTED_VIEWERS = new HashMap<>(); + + private List currentDescriptors = Collections.emptyList(); + private CLabel viewerSelectionLabel; + private MenuManager viewersSelectionMenuManager; + private ToolBar viewersSelectionToolBar; + /** * Creates a new instance of the class. * @@ -541,10 +560,12 @@ public boolean close() { showViewHandler.getHandler().dispose(); showViewHandler = null; } - if (menuManager != null) - menuManager.dispose(); + if (viewMenuManager != null) + viewMenuManager.dispose(); if (contextMenuManager != null) contextMenuManager.dispose(); + if (viewersSelectionMenuManager != null) + viewersSelectionMenuManager.dispose(); storeDialog(getDialogSettings()); if (searcher!=null) { searcher.cancel(); @@ -664,14 +685,14 @@ private Label createLabels(Composite parent) { } private void createViewMenu(Composite parent) { - toolBar = new ToolBar(parent, SWT.FLAT); - toolItem = new ToolItem(toolBar, SWT.PUSH, 0); + viewMenuToolBar = new ToolBar(parent, SWT.FLAT); + ToolItem toolItem = new ToolItem(viewMenuToolBar, SWT.PUSH, 0); GridData data = new GridData(); data.horizontalAlignment = GridData.END; - toolBar.setLayoutData(data); + viewMenuToolBar.setLayoutData(data); - toolBar.addMouseListener(new MouseAdapter() { + viewMenuToolBar.addMouseListener(new MouseAdapter() { @Override public void mouseDown(MouseEvent e) { showViewMenu(); @@ -689,9 +710,9 @@ public void widgetSelected(SelectionEvent e) { } }); - menuManager = new MenuManager(); + viewMenuManager = new MenuManager(); - fillViewMenu(menuManager); + fillViewMenu(viewMenuManager); IHandlerService service = PlatformUI.getWorkbench() .getService(IHandlerService.class); @@ -722,10 +743,10 @@ protected void fillViewMenu(IMenuManager menuManager) { } private void showViewMenu() { - Menu menu = menuManager.createContextMenu(getShell()); - Rectangle bounds = toolItem.getBounds(); + Menu menu = viewMenuManager.createContextMenu(getShell()); + Rectangle bounds = viewMenuToolBar.getItem(0).getBounds(); Point topLeft = new Point(bounds.x, bounds.y + bounds.height); - topLeft = toolBar.toDisplay(topLeft); + topLeft = viewMenuToolBar.toDisplay(topLeft); menu.setLocation(topLeft.x, topLeft.y); menu.setVisible(true); } @@ -870,36 +891,6 @@ public void keyPressed(KeyEvent e) { list.addDoubleClickListener(event -> handleDoubleClick()); - list.getTable().addKeyListener(new KeyAdapter() { - @Override - public void keyPressed(KeyEvent e) { - - if (e.keyCode == SWT.ARROW_UP && (e.stateMask & SWT.SHIFT) == 0 - && (e.stateMask & SWT.CTRL) == 0) { - StructuredSelection selection = (StructuredSelection) list - .getSelection(); - - if (selection.size() == 1) { - Object element = selection.getFirstElement(); - if (element.equals(list.getElementAt(0))) { - pattern.setFocus(); - } - list.getTable().notifyListeners(SWT.Selection, - new Event()); - - } - } - - if (e.keyCode == SWT.ARROW_DOWN - && (e.stateMask & SWT.SHIFT) != 0 - && (e.stateMask & SWT.CTRL) != 0) { - - list.getTable().notifyListeners(SWT.Selection, new Event()); - } - - } - }); - createDetailsArea(sashForm); sashForm.setWeights(new int[] {5,2}); @@ -946,77 +937,208 @@ protected void dispose() { blankImage.dispose(); blankImage = null; } + if (documents != null) { + documents.destroy(); + } } private void createDetailsArea(Composite parent) { - details = new StyledText(parent, SWT.MULTI+SWT.READ_ONLY+SWT.BORDER+SWT.H_SCROLL+SWT.V_SCROLL); - details.setFont(JFaceResources.getFont(TEXT_FONT)); + Composite detailsComposite = new Composite(parent, SWT.NONE); + detailsComposite.setLayout(GridLayoutFactory.fillDefaults().equalWidth(true).spacing(0, 0).create()); - list.addSelectionChangedListener(event -> refreshDetails()); - details.addControlListener(new ControlAdapter() { + final Composite toolbarComposite = new Composite(detailsComposite, SWT.NONE); + toolbarComposite.setLayoutData(GridDataFactory.fillDefaults().hint(SWT.DEFAULT, 24).grab(true, false).create()); + toolbarComposite.setLayout(RowLayoutFactory.fillDefaults().create()); + + viewerSelectionLabel = new CLabel(toolbarComposite, SWT.NONE); + + viewersSelectionToolBar = new ToolBar(toolbarComposite, SWT.FLAT); + final ToolItem toolItem = new ToolItem(viewersSelectionToolBar, SWT.PUSH, 0); + toolItem.setImage(WorkbenchImages.getImage(IWorkbenchGraphicConstants.IMG_LCL_VIEW_MENU)); + toolItem.setToolTipText(QuickSearchMessages.QuickSearchDialog_switchButtonTooltip); + toolItem.addSelectionListener(new SelectionAdapter() { @Override - public void controlResized(ControlEvent e) { - refreshDetails(); + public void widgetSelected(SelectionEvent e) { + showViewersSelectionMenu(); + } + }); + viewersSelectionToolBar.addMouseListener(new MouseAdapter() { + @Override + public void mouseDown(MouseEvent e) { + showViewersSelectionMenu(); } }); + + viewersSelectionMenuManager = new MenuManager(); + + viewersParent = new Composite(detailsComposite, SWT.NONE); + viewersParent.setLayoutData(GridDataFactory.fillDefaults().grab(true, true).create()); + viewersParentLayout = new StackLayout(); + viewersParent.setLayout(viewersParentLayout); + + replaceViewer(QuickSearchActivator.getDefault().getDefaultViewer(), null); + Assert.isNotNull(currentViewerWrapper, "Default source viewer initialization failed"); //$NON-NLS-1$ + + list.addSelectionChangedListener(event -> refreshDetails()); } + private TextViewerWrapper replaceViewer(IViewerDescriptor descriptor, IFile file) { + if (descriptor == null) { + // should never happen + descriptor = QuickSearchActivator.getDefault().getDefaultViewer(); + } + if (currentViewerWrapper != null && currentViewerWrapper.descriptor == descriptor) { + return currentViewerWrapper; + } + var wrapper = wrapViewer(descriptor); + if (currentViewerWrapper != null && wrapper != null) { + wrapper.viewerParent.setSize(currentViewerWrapper.viewerParent.getSize()); + } + currentViewerWrapper = wrapper; + currentViewerWrapper.showInViewersParent(); + + viewerSelectionLabel.setText(currentViewerWrapper.descriptor.getLabel()); + if (file == null) { + viewerSelectionLabel.setImage(ISharedImages.get().getImage(ISharedImages.IMG_OBJ_FILE)); + } else { + viewerSelectionLabel.setImage(QuickSearchActivator.getDefault().getImage(file)); + } + viewerSelectionLabel.getParent().layout(); + + return currentViewerWrapper; + } + + private void showViewersSelectionMenu() { + viewersSelectionMenuManager.removeAll(); + + // add default viewer option + LineItem item = (LineItem) ((IStructuredSelection) list.getSelection()).getFirstElement(); + var defaultDescriptor = currentDescriptors.getFirst(); + var defaultViewerItem = new DefaultViewerSelectionAction(defaultDescriptor, item); + viewersSelectionMenuManager.add(defaultViewerItem); + viewersSelectionMenuManager.add(new Separator()); + + // add options for all contributed viewers + viewersSelectionMenuManager.add(new GroupMarker("viewers")); //$NON-NLS-1$ // TODO + for (IViewerDescriptor viewerDescriptor : currentDescriptors) { + viewersSelectionMenuManager.add( + new ViewerSelectionAction(viewerDescriptor.getLabel(), viewerDescriptor, item)); + } + + Menu menu = viewersSelectionMenuManager.createContextMenu(getShell()); + + Rectangle bounds = viewersSelectionToolBar.getItem(0).getBounds(); + Point topLeft = new Point(bounds.x, bounds.y + bounds.height); + topLeft = viewersSelectionToolBar.toDisplay(topLeft); + menu.setLocation(topLeft.x, topLeft.y); + menu.setVisible(true); + } + + private TextViewerWrapper wrapViewer(IViewerDescriptor descriptor) { + TextViewerWrapper wrapper = viewerWrappers.get(descriptor); + if (wrapper == null) { + Composite viewerParent = new Composite(viewersParent, SWT.NONE); + viewerParent.setLayout(new FillLayout()); + // ask descriptor to create extension's creator and creator to provide handle for text viewer + if (descriptor.getViewerCreator().createTextViewer(viewerParent) instanceof ITextViewerHandle handle) { + var wrp = wrapper = new TextViewerWrapper(descriptor, handle, viewerParent); + viewerWrappers.put(descriptor, wrapper); + viewerParent.addControlListener(new ControlAdapter() { + @Override + public void controlResized(ControlEvent e) { + if (currentViewerWrapper == wrp && !currentViewerWrapper.shouldIgnoreResize.get()) { + LineItem item = (LineItem) ((IStructuredSelection) list.getSelection()).getFirstElement(); + if (item != null) { + showInViewer(item, item.getFile(), lastDocument); + } + } + } + }); + } else { // viewer descriptor failed to provide source viewer creator, we return null + viewerParent.dispose(); + } + } // else we're re-using viewer that was already initialized + return wrapper != null + ? wrapper + : viewerWrappers.get(QuickSearchActivator.getDefault().getDefaultViewer()); // always expected non NULL + } - // Dumber version just using the a 'raw' StyledText widget. private void refreshDetails() { - if (details!=null && list!=null && !list.getTable().isDisposed()) { + IFile file = null; + if (list!=null && !list.getTable().isDisposed()) { if (documents==null) { documents = new DocumentFetcher(); } IStructuredSelection sel = (IStructuredSelection) list.getSelection(); - if (sel==null || sel.isEmpty()) { - details.setText(EMPTY_STRING); - } else { + if (sel!=null && !sel.isEmpty()) { //Not empty selection - final int context = 100; // number of lines before and after match to include in preview - int numLines = computeLines(); - if (numLines > 0) { - LineItem item = (LineItem) sel.getFirstElement(); - IDocument document = documents.getDocument(item.getFile()); - if (document!=null) { - try { - int line = item.getLineNumber()-1; //in document lines are 0 based. In search 1 based. - int contextStartLine = Math.max(line-(numLines-1)/2 - context, 0); - int start = document.getLineOffset(contextStartLine); - int end = document.getLength(); - try { - IRegion lineInfo = document.getLineInformation(line + numLines/2 + context); - end = lineInfo.getOffset() + lineInfo.getLength(); - } catch (BadLocationException e) { - //Presumably line number is past the end of document. - //ignore. - } - - StyledString styledString = highlightMatches(document.get(start, end-start)); - details.setText(styledString.getString()); - details.setStyleRanges(styledString.getStyleRanges()); - details.setTopIndex(Math.max(line - contextStartLine - numLines/2, 0)); - return; - } catch (BadLocationException e) { + LineItem item = (LineItem) sel.getFirstElement(); + file = item.getFile(); + IDocument document = documents.getDocument(file); + if (document!=null) { + viewersSelectionToolBar.setVisible(true); + if (document != lastDocument) { + currentDescriptors = QuickSearchActivator.getDefault().getViewers(file); + var selectedDescr = SELECTED_VIEWERS.get(file); + if (selectedDescr == null || !currentDescriptors.contains(selectedDescr)) { + selectedDescr = currentDescriptors.getFirst(); + SELECTED_VIEWERS.remove(file); } + replaceViewer(selectedDescr, file); } + showInViewer(item, file, document); + return; } } - //empty selection or some error: - details.setText(EMPTY_STRING); } + //empty selection or some error: + lastDocument = null; + replaceViewer(QuickSearchActivator.getDefault().getDefaultViewer(), file).setInput(null, null, null); + viewersSelectionToolBar.setVisible(false); } - /** - * Computes how many lines of text can be displayed in the details section. - */ - private int computeLines() { - if (details!=null && !details.isDisposed()) { - int lineHeight = details.getLineHeight(); - int areaHeight = details.getClientArea().height; - return (areaHeight + lineHeight - 1) / lineHeight; + private void showInViewer(LineItem item, IFile file, IDocument document) { + int numLines = currentViewerWrapper.handle.getVisibleLines(); + try { + final int context = 100; // number of lines before and after match to include in preview + int line = item.getLineNumber()-1; //in document lines are 0 based. In search 1 based. + int contextStartLine = Math.max(line-(numLines-1)/2 - context, 0); + int start = document.getLineOffset(contextStartLine); + int displayedEndLine = line + numLines/2; + int end = document.getLength(); + if (displayedEndLine + context <= document.getNumberOfLines()) { + try { + IRegion lineInfo = document.getLineInformation(displayedEndLine + context); + end = lineInfo.getOffset() + lineInfo.getLength(); + } catch (BadLocationException e) { + //Presumably line number is past the end of document. + //ignore. + } + } + int contextLenght = end-start; + + if (document != lastDocument) { + // some matches may fall out of visible region, but we still prepare & pass ranges for all + StyledString styledString = highlightMatches(document.get()); + var ranges = styledString.getStyleRanges(); + lastDocument = document; + currentViewerWrapper.setInput(document, ranges, file); + } + + var visibleRange = new Region(start, contextLenght); + + IRegion rangeEndLineInfo = document.getLineInformation(Math.min(displayedEndLine, document.getNumberOfLines() - 1)); + int rangeStart = document.getLineOffset(Math.max(line - numLines/2, 0)); + int rangeEnd = rangeEndLineInfo.getOffset() + rangeEndLineInfo.getLength(); + var revealedRange = new Region(rangeStart, rangeEnd - rangeStart); + + var targetLineFirstMatch = getQuery().findFirst(document.get(item.getOffset(), contextLenght - (item.getOffset() - start))); + int targetLineFirstMatchStart = item.getOffset() + targetLineFirstMatch.getOffset(); + var matchRange = new Region(targetLineFirstMatchStart, targetLineFirstMatch.getLength()); + + currentViewerWrapper.handle.focusMatch(visibleRange, revealedRange, line, matchRange); + } catch (BadLocationException e) { } - return 0; } /** @@ -1034,47 +1156,6 @@ private StyledString highlightMatches(String visibleText) { return styledText; } -// Version using sourceviewer -// private void refreshDetails() { -// if (details!=null && list!=null && !list.getTable().isDisposed()) { -// if (documents==null) { -// documents = new DocumentFetcher(); -// } -// IStructuredSelection sel = (IStructuredSelection) list.getSelection(); -// if (sel!=null && !sel.isEmpty()) { -// //Not empty selection -// LineItem item = (LineItem) sel.getFirstElement(); -// IDocument document = documents.getDocument(item.getFile()); -// try { -// int line = item.getLineNumber()-1; //in document lines are 0 based. In search 1 based. -// int start = document.getLineOffset(Math.max(line-2, 0)); -// int end = document.getLength(); -// try { -// end = document.getLineOffset(line+3); -// } catch (BadLocationException e) { -// //Presumably line number is past the end of document. -// //ignore. -// } -// details.setDocument(document, start, end-start); -// -// String visibleText = document.get(start, end-start); -// List matches = getQuery().findAll(visibleText); -// Region visibleRegion = new Region(start, end-start); -// TextPresentation presentation = new TextPresentation(visibleRegion, 20); -// presentation.setDefaultStyleRange(new StyleRange(0, document.getLength(), null, null)); -// for (TextRange m : matches) { -// presentation.addStyleRange(new StyleRange(m.start+start, m.len, null, YELLOW)); -// } -// details.changeTextPresentation(presentation, true); -// -// return; -// } catch (BadLocationException e) { -// } -// } -// details.setDocument(null); -// } -// } - /** * Handle selection in the items list by updating labels of selected and * unselected items and refresh the details field using the selection. @@ -1471,4 +1552,94 @@ public QuickTextQuery getQuery() { return searcher.getQuery(); } + private class TextViewerWrapper { + + final IViewerDescriptor descriptor; + final ITextViewerHandle handle; + final Composite viewerParent; + final AtomicBoolean shouldIgnoreResize = new AtomicBoolean(); + + public TextViewerWrapper(IViewerDescriptor descriptor, ITextViewerHandle handle, Composite viewerParent) { + this.descriptor = descriptor; + this.handle = handle; + this.viewerParent = viewerParent; + } + + void setInput(IDocument input, StyleRange[] ranges, IFile file) { + // setting document triggers resize of the text widget that is OK to ignore + var wasInCall = shouldIgnoreResize.getAndSet(true); + try { + handle.setViewerInput(input, ranges, file); + } finally { + if (!wasInCall) { + shouldIgnoreResize.set(false); + } + } + } + + void showInViewersParent() { + // layout() of parent triggers resize of the text widget that is OK to ignore + var wasInCall = shouldIgnoreResize.getAndSet(true); + try { + viewersParentLayout.topControl = viewerParent; + viewersParent.layout(); + } finally { + if (!wasInCall) { + shouldIgnoreResize.set(false); + } + } + } + } + + private class ViewerSelectionAction extends Action { + + final IViewerDescriptor descriptor; + + public ViewerSelectionAction(String text, IViewerDescriptor descriptor, LineItem selectedItem) { + super(text, IAction.AS_RADIO_BUTTON); + this.descriptor = descriptor; + setChecked(createAsChecked(selectedItem)); + } + + boolean createAsChecked(LineItem selectedItem) { + return currentViewerWrapper.descriptor == descriptor + && selectedItem != null + && SELECTED_VIEWERS.get(selectedItem.getFile()) == descriptor; + } + + @Override + public void run() { + if (isChecked()) { + LineItem item = (LineItem) ((IStructuredSelection) list.getSelection()).getFirstElement(); + var file = item.getFile(); + applySelection(file); + + replaceViewer(descriptor, file); + + var document = lastDocument; + lastDocument = null; // to force setInput() + showInViewer(item, file, document); + } + } + + void applySelection(IFile file) { + SELECTED_VIEWERS.put(file, descriptor); + } + } + + private class DefaultViewerSelectionAction extends ViewerSelectionAction { + public DefaultViewerSelectionAction(IViewerDescriptor descriptor, LineItem selectedItem) { + super(QuickSearchMessages.QuickSearchDialog_defaultViewer, descriptor, selectedItem); + } + + @Override + boolean createAsChecked(LineItem selectedItem) { + return selectedItem == null || !SELECTED_VIEWERS.containsKey(selectedItem.getFile()); + } + + @Override + void applySelection(IFile file) { + SELECTED_VIEWERS.remove(file); + } + } } diff --git a/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/ui/QuickSearchMessages.java b/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/ui/QuickSearchMessages.java new file mode 100644 index 00000000000..2f790cf6c3f --- /dev/null +++ b/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/ui/QuickSearchMessages.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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 + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Jozef Tomek - initial API and implementation + *******************************************************************************/ +package org.eclipse.text.quicksearch.internal.ui; + +import org.eclipse.osgi.util.NLS; + +public final class QuickSearchMessages extends NLS { + + private static final String BUNDLE_NAME = "org.eclipse.text.quicksearch.internal.ui.QuickSearchMessages";//$NON-NLS-1$ + + private QuickSearchMessages() { + // Do not instantiate + } + + public static String QuickSearchDialog_switchButtonTooltip; + public static String QuickSearchDialog_defaultViewer; + + static { + NLS.initializeMessages(BUNDLE_NAME, QuickSearchMessages.class); + } +} diff --git a/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/ui/QuickSearchMessages.properties b/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/ui/QuickSearchMessages.properties new file mode 100644 index 00000000000..8a6c733f5b8 --- /dev/null +++ b/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/ui/QuickSearchMessages.properties @@ -0,0 +1,2 @@ +QuickSearchDialog_switchButtonTooltip=Switch Source Viewer +QuickSearchDialog_defaultViewer=Default Viewer \ No newline at end of file diff --git a/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/ui/ViewerDescriptor.java b/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/ui/ViewerDescriptor.java new file mode 100644 index 00000000000..4e956f8e523 --- /dev/null +++ b/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/ui/ViewerDescriptor.java @@ -0,0 +1,98 @@ +/******************************************************************************* + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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 + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Jozef Tomek - initial API and implementation + *******************************************************************************/ +package org.eclipse.text.quicksearch.internal.ui; + +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IConfigurationElement; +import org.eclipse.text.quicksearch.ITextViewerCreator; + +/** + * Creates ITextViewerCreators from an IConfigurationElement. + * + * @see ITextViewerCreator + */ +public class ViewerDescriptor implements IViewerDescriptor { + private final static String ID_ATTRIBUTE = "id"; //$NON-NLS-1$ + private final static String CLASS_ATTRIBUTE = "class"; //$NON-NLS-1$ + private final static String LINKED_EDITOR_ATTRIBUTE = "linkedEditor"; //$NON-NLS-1$ + private final static String LABEL_ATTRIBUTE = "label"; //$NON-NLS-1$ + + private final IConfigurationElement fConfiguration; + private final String fViewerId; + private final String fLabel; + private final String fLinkedEditorId; + + private ITextViewerCreator fViewerCreator; + + public ViewerDescriptor(IConfigurationElement config) { + fConfiguration = config; + fViewerId = config.getContributor().getName() + config.getAttribute(ID_ATTRIBUTE); + fLabel = fConfiguration.getAttribute(LABEL_ATTRIBUTE); + fLinkedEditorId = fConfiguration.getAttribute(LINKED_EDITOR_ATTRIBUTE); + } + + @Override + public ITextViewerCreator getViewerCreator() { + if (fViewerCreator == null) { + try { + fViewerCreator = (ITextViewerCreator) fConfiguration.createExecutableExtension(CLASS_ATTRIBUTE); + } catch (CoreException e) { + QuickSearchActivator.log(e); + } + } + return fViewerCreator; + } + + @Override + public String getLabel() { + return fLabel; + } + + String getLinkedEditorId() { + return fLinkedEditorId; + } + + String getViewerClass() { + return fConfiguration.getAttribute(CLASS_ATTRIBUTE); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("ViewerDescriptor ["); //$NON-NLS-1$ + sb.append("fViewerId="); //$NON-NLS-1$ + sb.append(fViewerId); + sb.append(", "); //$NON-NLS-1$ + if (fViewerCreator != null) { + sb.append("viewerCreator="); //$NON-NLS-1$ + sb.append(fViewerCreator); + sb.append(", "); //$NON-NLS-1$ + } + String viewerClass = getViewerClass(); + if (viewerClass != null) { + sb.append("viewerClass="); //$NON-NLS-1$ + sb.append(viewerClass); + sb.append(", "); //$NON-NLS-1$ + } + if (fConfiguration != null) { + sb.append("configuration="); //$NON-NLS-1$ + sb.append(fConfiguration); + } + sb.append("]"); //$NON-NLS-1$ + return sb.toString(); + } + +} diff --git a/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/util/DocumentFetcher.java b/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/util/DocumentFetcher.java index 10d9f649d53..b18c5736919 100644 --- a/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/util/DocumentFetcher.java +++ b/bundles/org.eclipse.text.quicksearch/src/org/eclipse/text/quicksearch/internal/util/DocumentFetcher.java @@ -50,14 +50,18 @@ */ public class DocumentFetcher { + private static final ITextFileBufferManager BUFFER_MANAGER = FileBuffers.getTextFileBufferManager(); + private Map dirtyEditors; //Simple cache remembers the last fetched file and document. private IFile lastFile = null; private IDocument lastDocument = null; + private boolean disconnectLastFile = false; IDocumentProvider provider = new TextFileDocumentProvider(); + public DocumentFetcher() { if (PlatformUI.isWorkbenchRunning()) { dirtyEditors = evalNonFileBufferDocuments(); @@ -82,9 +86,10 @@ public DocumentFetcher() { * buffer nor corresponds to an existing file in the workspace. */ public IDocument getDocument(IFile file) { - if (file==lastFile) { + if (lastFile != null && lastFile.equals(file)) { return lastDocument; } + disconnectLastFile(); lastFile = file; lastDocument = dirtyEditors.get(file); if (lastDocument==null) { @@ -97,8 +102,7 @@ public IDocument getDocument(IFile file) { } private IDocument getOpenDocument(IFile file) { - ITextFileBufferManager bufferManager= FileBuffers.getTextFileBufferManager(); - ITextFileBuffer textFileBuffer= bufferManager.getTextFileBuffer(file.getFullPath(), LocationKind.IFILE); + ITextFileBuffer textFileBuffer= BUFFER_MANAGER.getTextFileBuffer(file.getFullPath(), LocationKind.IFILE); if (textFileBuffer != null) { return textFileBuffer.getDocument(); } @@ -106,23 +110,19 @@ private IDocument getOpenDocument(IFile file) { } private IDocument getClosedDocument(IFile file) { - //No in the manager yet. Try to create a temporary buffer then remove it again. - ITextFileBufferManager bufferManager = FileBuffers.getTextFileBufferManager(); + //No in the manager yet. Try to create a temporary buffer - required for some extensions to work + // (will get removed once not used anymore) IPath location = file.getFullPath(); //Must use workspace location, not fs location for API below. ITextFileBuffer buffer = null; try { - bufferManager.connect(location, LocationKind.IFILE, new NullProgressMonitor()); - buffer = bufferManager.getTextFileBuffer(location, LocationKind.IFILE); + BUFFER_MANAGER.connect(location, LocationKind.IFILE, new NullProgressMonitor()); + disconnectLastFile = true; + buffer = BUFFER_MANAGER.getTextFileBuffer(location, LocationKind.IFILE); if (buffer!=null) { return buffer.getDocument(); } } catch (Throwable e) { QuickSearchActivator.log(e); - } finally { - try { - bufferManager.disconnect(location, LocationKind.IFILE, new NullProgressMonitor()); - } catch (CoreException e) { - } } return null; } @@ -151,8 +151,7 @@ private void evaluateTextEditor(Map result, IEditorPart ep) { if (input instanceof IFileEditorInput) { IFile file= ((IFileEditorInput) input).getFile(); if (!result.containsKey(file)) { // take the first editor found - ITextFileBufferManager bufferManager= FileBuffers.getTextFileBufferManager(); - ITextFileBuffer textFileBuffer= bufferManager.getTextFileBuffer(file.getFullPath(), LocationKind.IFILE); + ITextFileBuffer textFileBuffer= BUFFER_MANAGER.getTextFileBuffer(file.getFullPath(), LocationKind.IFILE); if (textFileBuffer != null) { // file buffer has precedence result.put(file, textFileBuffer.getDocument()); @@ -167,4 +166,19 @@ private void evaluateTextEditor(Map result, IEditorPart ep) { } } + private void disconnectLastFile() { + if (disconnectLastFile && lastFile != null) { + disconnectLastFile = false; + try { + BUFFER_MANAGER.disconnect(lastFile.getFullPath(), LocationKind.IFILE, new NullProgressMonitor()); + } catch (CoreException e) { + QuickSearchActivator.log(e); + } + } + } + + public void destroy() { + disconnectLastFile(); + } + } diff --git a/bundles/org.eclipse.ui.genericeditor/META-INF/MANIFEST.MF b/bundles/org.eclipse.ui.genericeditor/META-INF/MANIFEST.MF index 31a9a9c320a..e26154a1225 100644 --- a/bundles/org.eclipse.ui.genericeditor/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.ui.genericeditor/META-INF/MANIFEST.MF @@ -15,7 +15,8 @@ Require-Bundle: org.eclipse.ui.workbench.texteditor;bundle-version="3.10.0", org.eclipse.ui.ide;bundle-version="3.12.0", org.eclipse.core.resources;bundle-version="3.11.0", org.eclipse.core.expressions;bundle-version="3.6.0", - org.eclipse.compare;bundle-version="3.11.0";resolution:=optional + org.eclipse.compare;bundle-version="3.11.0";resolution:=optional, + org.eclipse.text.quicksearch;bundle-version="1.3.0" Export-Package: org.eclipse.ui.internal.genericeditor;x-internal:=true, org.eclipse.ui.internal.genericeditor.hover;x-internal:=true, org.eclipse.ui.internal.genericeditor.markers;x-internal:=true, diff --git a/bundles/org.eclipse.ui.genericeditor/plugin.properties b/bundles/org.eclipse.ui.genericeditor/plugin.properties index 9bbbe849ff2..b6b0a54917d 100644 --- a/bundles/org.eclipse.ui.genericeditor/plugin.properties +++ b/bundles/org.eclipse.ui.genericeditor/plugin.properties @@ -25,6 +25,7 @@ ExtPoint.foldingReconcilers=Folding Reconcilers ExtPoint.characterPairMatchers=Character Pair Matcher Providers ExtPoint.hyperlinkDetectorTarget=Generic Text Editor ExtPoint.iconProviders=Generic Editor Icon Providers +ExtPoint.quicksearchSourceViewer=Generic Code Viewer openDeclarationCommand_name=Open Declaration context_name=in Generic Code Editor context_description=When editing in the Generic Code Editor diff --git a/bundles/org.eclipse.ui.genericeditor/plugin.xml b/bundles/org.eclipse.ui.genericeditor/plugin.xml index 107f075c39e..e9557603bf0 100644 --- a/bundles/org.eclipse.ui.genericeditor/plugin.xml +++ b/bundles/org.eclipse.ui.genericeditor/plugin.xml @@ -14,6 +14,7 @@ Sopot Cela & Mickael Istria (Red Hat Inc). -initial implementation Lucas Bullen (Red Hat Inc.) - Bug 508829, 521382 Simon Scholz - Bug 527830 + Jozef Tomek - Quick Search text viewers extension --> @@ -285,4 +286,16 @@ name="%PreferencePages.GenericTextEditors"> + + + + + diff --git a/bundles/org.eclipse.ui.genericeditor/src/org/eclipse/ui/internal/genericeditor/quicksearch/GenericEditorViewerCreator.java b/bundles/org.eclipse.ui.genericeditor/src/org/eclipse/ui/internal/genericeditor/quicksearch/GenericEditorViewerCreator.java new file mode 100644 index 00000000000..447d2a1ca57 --- /dev/null +++ b/bundles/org.eclipse.ui.genericeditor/src/org/eclipse/ui/internal/genericeditor/quicksearch/GenericEditorViewerCreator.java @@ -0,0 +1,69 @@ +/******************************************************************************* + * Copyright (c) 2025 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * 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 + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Jozef Tomek - initial API and implementation + *******************************************************************************/ +package org.eclipse.ui.internal.genericeditor.quicksearch; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.source.CompositeRuler; +import org.eclipse.jface.text.source.SourceViewer; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.text.quicksearch.ITextViewerCreator; +import org.eclipse.text.quicksearch.ReconcilingAwareSourceViewerHandle; +import org.eclipse.text.quicksearch.SourceViewerConfigurer; +import org.eclipse.ui.editors.text.EditorsUI; +import org.eclipse.ui.internal.genericeditor.ExtensionBasedTextViewerConfiguration; +import org.eclipse.ui.internal.genericeditor.GenericEditorPlugin; +import org.eclipse.ui.texteditor.ChainedPreferenceStore; + +/** + * Creates quicksearch text viewer handles that use {@link GenericEditorViewer}. + */ +public class GenericEditorViewerCreator implements ITextViewerCreator { + + @Override + public ITextViewerHandle createTextViewer(Composite parent) { + return new GenericEditorSourceViewerHandle(parent); + } + + private class GenericEditorSourceViewerHandle extends ReconcilingAwareSourceViewerHandle { + + public GenericEditorSourceViewerHandle(Composite parent) { + super(new SourceViewerConfigurer<>(GenericEditorViewer::new), parent, false, false); + } + } + + static class GenericEditorViewer extends SourceViewer { + + public GenericEditorViewer(Composite parent, CompositeRuler verticalRuler, int styles) { + super(parent, verticalRuler, styles); + } + + @Override + public void setInput(Object input) { + unconfigure(); + + if (input instanceof IDocument doc) { + setDocument(doc); + var configuration = new ExtensionBasedTextViewerConfiguration(null, + new ChainedPreferenceStore(new IPreferenceStore[] { EditorsUI.getPreferenceStore(), + GenericEditorPlugin.getDefault().getPreferenceStore() })); + configure(configuration); + } + } + + } + +}