From afe66c741118fb4207e854e4e93649f43922a024 Mon Sep 17 00:00:00 2001 From: sebthom Date: Fri, 17 May 2024 10:28:37 +0200 Subject: [PATCH] feat: add option to display TextMate token info in hover --- .../org/eclipse/tm4e/core/model/TMModel.java | 3 +- .../org/eclipse/tm4e/core/model/TMToken.java | 6 +- .../core/model/TMTokenizationSupport.java | 6 +- org.eclipse.tm4e.ui/plugin.xml | 8 +- .../java/org/eclipse/tm4e/ui/TMUIPlugin.java | 4 + .../tm4e/ui/internal/TMUIMessages.java | 1 + .../tm4e/ui/internal/TMUIMessages.properties | 1 + .../ui/internal/hover/TMTokenTextHover.java | 116 ++++++++++++++++++ .../tm4e/ui/internal/hover/package-info.java | 4 + .../ui/internal/model/TMModelManager.java | 5 + .../preferences/AbstractPreferencePage.java | 2 +- .../preferences/PreferenceConstants.java | 2 + .../preferences/PreferenceHelper.java | 10 ++ .../preferences/TextMatePreferencePage.java | 79 ++++++++---- 14 files changed, 217 insertions(+), 30 deletions(-) create mode 100644 org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/hover/TMTokenTextHover.java create mode 100644 org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/hover/package-info.java diff --git a/org.eclipse.tm4e.core/src/main/java/org/eclipse/tm4e/core/model/TMModel.java b/org.eclipse.tm4e.core/src/main/java/org/eclipse/tm4e/core/model/TMModel.java index 880126220..d365f27fe 100644 --- a/org.eclipse.tm4e.core/src/main/java/org/eclipse/tm4e/core/model/TMModel.java +++ b/org.eclipse.tm4e.core/src/main/java/org/eclipse/tm4e/core/model/TMModel.java @@ -20,6 +20,7 @@ import java.lang.System.Logger; import java.time.Duration; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.concurrent.BlockingQueue; @@ -255,7 +256,7 @@ private void revalidateTokens() { // check if complete line was tokenized if (r.stoppedEarly) { // treat the rest of the line as one default token - r.tokens.add(new TMToken(r.actualStopOffset, "")); + r.tokens.add(new TMToken(r.actualStopOffset, "", Collections.emptyList())); // Use the line's starting state as end state in case of incomplete tokenization r.endState = currLineTokens.startState; } diff --git a/org.eclipse.tm4e.core/src/main/java/org/eclipse/tm4e/core/model/TMToken.java b/org.eclipse.tm4e.core/src/main/java/org/eclipse/tm4e/core/model/TMToken.java index 776ba8de6..fda5151fc 100644 --- a/org.eclipse.tm4e.core/src/main/java/org/eclipse/tm4e/core/model/TMToken.java +++ b/org.eclipse.tm4e.core/src/main/java/org/eclipse/tm4e/core/model/TMToken.java @@ -16,6 +16,8 @@ */ package org.eclipse.tm4e.core.model; +import java.util.List; + import org.eclipse.jdt.annotation.Nullable; /** @@ -28,10 +30,12 @@ public final class TMToken { public final int startIndex; public final String type; // public readonly language: string + public final List scopes; - public TMToken(final int startIndex, final String type) { + public TMToken(final int startIndex, final String type, final List scopes) { this.startIndex = startIndex; this.type = type; + this.scopes = scopes; } @Override diff --git a/org.eclipse.tm4e.core/src/main/java/org/eclipse/tm4e/core/model/TMTokenizationSupport.java b/org.eclipse.tm4e.core/src/main/java/org/eclipse/tm4e/core/model/TMTokenizationSupport.java index cad686117..bbea29503 100644 --- a/org.eclipse.tm4e.core/src/main/java/org/eclipse/tm4e/core/model/TMTokenizationSupport.java +++ b/org.eclipse.tm4e.core/src/main/java/org/eclipse/tm4e/core/model/TMTokenizationSupport.java @@ -29,6 +29,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.eclipse.tm4e.core.grammar.IGrammar; import org.eclipse.tm4e.core.grammar.IStateStack; +import org.eclipse.tm4e.core.grammar.IToken; import org.eclipse.tm4e.core.internal.grammar.StateStack; import org.eclipse.tm4e.core.internal.utils.MoreCollections; import org.eclipse.tm4e.core.internal.utils.StringUtils; @@ -83,13 +84,12 @@ public TokenizationResult tokenize(final String line, // Create the result early and fill in the tokens later final var tmTokens = new ArrayList(tokens.length < 10 ? tokens.length : 10); String lastTokenType = null; - for (final var token : tokens) { + for (final IToken token : tokens) { final String tokenType = decodeTextMateTokenCached.apply(decodeMap, token.getScopes()); // do not push a new token if the type is exactly the same (also helps with ligatures) if (!tokenType.equals(lastTokenType)) { - final int tokenStartIndex = token.getStartIndex(); - tmTokens.add(new TMToken(tokenStartIndex + offsetDelta, tokenType)); + tmTokens.add(new TMToken(token.getStartIndex() + offsetDelta, tokenType, token.getScopes())); lastTokenType = tokenType; } } diff --git a/org.eclipse.tm4e.ui/plugin.xml b/org.eclipse.tm4e.ui/plugin.xml index 899c41e53..0180377c3 100644 --- a/org.eclipse.tm4e.ui/plugin.xml +++ b/org.eclipse.tm4e.ui/plugin.xml @@ -37,7 +37,7 @@ path="./themes/Eclipse-light.css" /> + path="./themes/WTP-XML-Classic.css" /> + + + + diff --git a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/TMUIPlugin.java b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/TMUIPlugin.java index e01d12d4b..7cd457d84 100644 --- a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/TMUIPlugin.java +++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/TMUIPlugin.java @@ -43,6 +43,10 @@ public class TMUIPlugin extends AbstractUIPlugin { @Nullable private static volatile TMUIPlugin plugin; + public static boolean getPreference(final String key, final boolean defaultValue) { + return Platform.getPreferencesService().getBoolean(PLUGIN_ID, key, defaultValue, null /* = search in all available scopes */); + } + public static @Nullable String getPreference(final String key, final @Nullable String defaultValue) { return Platform.getPreferencesService().getString(PLUGIN_ID, key, defaultValue, null /* = search in all available scopes */); } diff --git a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/TMUIMessages.java b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/TMUIMessages.java index a2c59ba26..d03c9bac4 100644 --- a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/TMUIMessages.java +++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/TMUIMessages.java @@ -34,6 +34,7 @@ public final class TMUIMessages extends NLS { public static String TextMatePreferencePage_LanguageConfigurationRelatedLink; public static String TextMatePreferencePage_TaskTagsRelatedLink; public static String TextMatePreferencePage_ThemeRelatedLink; + public static String TextMatePreferencePage_ShowTextMateTokenInfoHover; // Grammar preferences page public static String GrammarPreferencePage_title; diff --git a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/TMUIMessages.properties b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/TMUIMessages.properties index 0ece72564..87d611d5f 100644 --- a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/TMUIMessages.properties +++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/TMUIMessages.properties @@ -21,6 +21,7 @@ TextMatePreferencePage_GrammarRelatedLink=See ''{0}'' for associating edi TextMatePreferencePage_LanguageConfigurationRelatedLink=See ''{0}'' for associating editors with language configurations. TextMatePreferencePage_TaskTagsRelatedLink=See ''{0}'' for task tags configuration. TextMatePreferencePage_ThemeRelatedLink=See ''{0}'' for associating editors with themes. +TextMatePreferencePage_ShowTextMateTokenInfoHover=Show TextMate token info in hovers. GrammarPreferencePage_title=TextMate grammars GrammarPreferencePage_description=Register, configure or remove TextMate grammars: diff --git a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/hover/TMTokenTextHover.java b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/hover/TMTokenTextHover.java new file mode 100644 index 000000000..bd96b5e46 --- /dev/null +++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/hover/TMTokenTextHover.java @@ -0,0 +1,116 @@ +/******************************************************************************* + * Copyright (c) 2024 Vegard IT GmbH and others. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Sebastian Thomschke (Vegard IT) - initial implementation + *******************************************************************************/ +package org.eclipse.tm4e.ui.internal.hover; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jface.text.AbstractReusableInformationControlCreator; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.DefaultInformationControl; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IInformationControl; +import org.eclipse.jface.text.IInformationControlCreator; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.ITextHover; +import org.eclipse.jface.text.ITextHoverExtension; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.Region; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.tm4e.core.model.TMToken; +import org.eclipse.tm4e.ui.internal.model.TMDocumentModel; +import org.eclipse.tm4e.ui.internal.model.TMModelManager; +import org.eclipse.tm4e.ui.internal.preferences.PreferenceHelper; +import org.eclipse.ui.editors.text.EditorsUI; + +public class TMTokenTextHover implements ITextHover, ITextHoverExtension { + + private static final class RegionWithTMToken extends Region { + final TMToken token; + final String tokenText; + + RegionWithTMToken(final int offset, final int length, final String tokenText, final TMToken token) { + super(offset, length); + this.tokenText = tokenText; + this.token = token; + } + } + + @Override + public IInformationControlCreator getHoverControlCreator() { + // setup a hover control that interprets basic HTML input + return new AbstractReusableInformationControlCreator() { + @Override + protected IInformationControl doCreateInformationControl(final @NonNullByDefault({}) Shell parent) { + return new DefaultInformationControl(parent, EditorsUI.getTooltipAffordanceString()); + } + }; + } + + @Override + public @Nullable String getHoverInfo(final @NonNullByDefault({}) ITextViewer textViewer, + final @NonNullByDefault({}) IRegion hoverRegion) { + if (hoverRegion instanceof final RegionWithTMToken regionWithToken) { + final var text = regionWithToken.tokenText.replace(' ', '·').replace('\t', '→'); + return "" + text + " (" + text.length() + + " chars)
" + + "
" + + "Token Type: " + regionWithToken.token.type + "
" + + "TextMate Scopes:
  • " + String.join("
  • ", regionWithToken.token.scopes); + } + return null; + } + + @Override + public @Nullable IRegion getHoverRegion(final @NonNullByDefault({}) ITextViewer textViewer, final int offset) { + if (!PreferenceHelper.isTMTokenHoverEnabled()) + return null; + + final @Nullable IDocument doc = textViewer.getDocument(); + if (doc == null) + return null; + + final TMDocumentModel model = TMModelManager.INSTANCE.getConnectedModel(doc); + if (model == null) + return null; + + try { + // retrieve parsed TM tokens of the hovered line + final int lineIndex = doc.getLineOfOffset(offset); + final var tokens = model.getLineTokens(lineIndex); + if (tokens == null) + return null; + + // find the TM token at the hover position + final int lineStartOffset = doc.getLineOffset(lineIndex); + TMToken hoveredToken = null; + TMToken nextToken = null; + for (final TMToken token : tokens) { + if (token.startIndex <= offset - lineStartOffset) { + hoveredToken = token; + } else { + nextToken = token; + break; + } + } + if (hoveredToken == null) + return null; + + final int regionOffset = lineStartOffset + hoveredToken.startIndex; + final int regionLength = nextToken == null + ? doc.getLineLength(lineIndex) - hoveredToken.startIndex + : nextToken.startIndex - hoveredToken.startIndex; + return new RegionWithTMToken(regionOffset, regionLength, doc.get(regionOffset, regionLength), hoveredToken); + } catch (final BadLocationException e) { + return null; + } + } +} diff --git a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/hover/package-info.java b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/hover/package-info.java new file mode 100644 index 000000000..4bf69a41e --- /dev/null +++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/hover/package-info.java @@ -0,0 +1,4 @@ +@NonNullByDefault +package org.eclipse.tm4e.ui.internal.hover; + +import org.eclipse.jdt.annotation.NonNullByDefault; diff --git a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/model/TMModelManager.java b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/model/TMModelManager.java index 3f3c72b0c..b2d29e722 100644 --- a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/model/TMModelManager.java +++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/model/TMModelManager.java @@ -16,6 +16,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jface.text.IDocument; import org.eclipse.tm4e.ui.model.ITMModelManager; @@ -44,6 +45,10 @@ public void disconnect(final IDocument document) { } } + public @Nullable TMDocumentModel getConnectedModel(final IDocument document) { + return models.get(document); + } + @Override public boolean isConnected(final IDocument document) { return models.containsKey(document); diff --git a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/preferences/AbstractPreferencePage.java b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/preferences/AbstractPreferencePage.java index 43423b190..09ff08da3 100644 --- a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/preferences/AbstractPreferencePage.java +++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/preferences/AbstractPreferencePage.java @@ -22,7 +22,7 @@ public abstract class AbstractPreferencePage extends PreferencePage implements I private final @Nullable String title; - protected AbstractPreferencePage(final @Nullable String title, final String description) { + protected AbstractPreferencePage(final @Nullable String title, final @Nullable String description) { this.title = title; setDescription(description); } diff --git a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/preferences/PreferenceConstants.java b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/preferences/PreferenceConstants.java index 0f8359a72..79164b536 100644 --- a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/preferences/PreferenceConstants.java +++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/preferences/PreferenceConstants.java @@ -25,6 +25,8 @@ public final class PreferenceConstants { public static final String DEFAULT_DARK_THEME = "org.eclipse.tm4e.ui.themes.defaultDarkTheme"; public static final String DEFAULT_LIGHT_THEME = "org.eclipse.tm4e.ui.themes.defaultLightTheme"; + public static final String TMTOKEN_HOVER_ENABLED = "org.eclipse.tm4e.ui.tmScopeHoverEnabled"; + private PreferenceConstants() { } } diff --git a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/preferences/PreferenceHelper.java b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/preferences/PreferenceHelper.java index c1a73a160..e05adccc5 100644 --- a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/preferences/PreferenceHelper.java +++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/preferences/PreferenceHelper.java @@ -110,6 +110,16 @@ public static void saveMarkerConfigs(final Set markerConfigs) thro prefs.flush(); } + public static boolean isTMTokenHoverEnabled() { + return TMUIPlugin.getPreference(PreferenceConstants.TMTOKEN_HOVER_ENABLED, false); + } + + public static void saveTMTokenHoverEnabled(boolean isEnabled) throws BackingStoreException { + final var prefs = InstanceScope.INSTANCE.getNode(TMUIPlugin.PLUGIN_ID); + prefs.putBoolean(PreferenceConstants.TMTOKEN_HOVER_ENABLED, isEnabled); + prefs.flush(); + } + private PreferenceHelper() { } } diff --git a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/preferences/TextMatePreferencePage.java b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/preferences/TextMatePreferencePage.java index bbe4f4f0a..04e6fa525 100644 --- a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/preferences/TextMatePreferencePage.java +++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/preferences/TextMatePreferencePage.java @@ -11,63 +11,96 @@ */ package org.eclipse.tm4e.ui.internal.preferences; +import static org.eclipse.tm4e.core.internal.utils.NullSafetyHelper.lazyNonNull; + import org.eclipse.core.runtime.Platform; -import org.eclipse.jdt.annotation.Nullable; -import org.eclipse.jface.preference.PreferencePage; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jface.layout.GridLayoutFactory; import org.eclipse.swt.SWT; import org.eclipse.swt.layout.GridData; -import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.tm4e.ui.TMUIPlugin; import org.eclipse.tm4e.ui.internal.TMUIMessages; import org.eclipse.ui.IWorkbench; -import org.eclipse.ui.IWorkbenchPreferencePage; import org.eclipse.ui.dialogs.PreferenceLinkArea; import org.eclipse.ui.preferences.IWorkbenchPreferenceContainer; +import org.osgi.service.prefs.BackingStoreException; /** * TextMate Global preferences page. - * */ -public final class TextMatePreferencePage extends PreferencePage implements IWorkbenchPreferencePage { +public final class TextMatePreferencePage extends AbstractPreferencePage { + + private Button tmTokenHoverCheckbox = lazyNonNull(); + + public TextMatePreferencePage() { + super(null, null); + } @Override - protected Control createContents(@Nullable final Composite parent) { + protected Control createContents(final @NonNullByDefault({}) Composite parent) { final var composite = new Composite(parent, SWT.NONE); - final var layout = new GridLayout(1, false); - layout.marginHeight = layout.marginWidth = 0; - composite.setLayout(layout); + composite.setLayout(GridLayoutFactory.fillDefaults().create()); + + addRelatedLinks(composite); + + new Label(composite, SWT.SEPARATOR | SWT.HORIZONTAL) + .setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + + tmTokenHoverCheckbox = new Button(composite, SWT.CHECK); + tmTokenHoverCheckbox.setText(TMUIMessages.TextMatePreferencePage_ShowTextMateTokenInfoHover); + tmTokenHoverCheckbox.setSelection(PreferenceHelper.isTMTokenHoverEnabled()); + + applyDialogFont(composite); + return composite; + } + private void addRelatedLinks(final Composite parent) { // Add link to grammar preference page - addRelatedLink(composite, GrammarPreferencePage.PAGE_ID, TMUIMessages.TextMatePreferencePage_GrammarRelatedLink); + addRelatedLink(parent, GrammarPreferencePage.PAGE_ID, TMUIMessages.TextMatePreferencePage_GrammarRelatedLink); // Add link to language configuration preference page - if (Platform.getBundle("org.eclipse.tm4e.languageconfiguration") != null) { //$NON-NLS-1$ - addRelatedLink(composite, - "org.eclipse.tm4e.languageconfiguration.preferences.LanguageConfigurationPreferencePage", //$NON-NLS-1$ + if (Platform.getBundle("org.eclipse.tm4e.languageconfiguration") != null) { + addRelatedLink(parent, + "org.eclipse.tm4e.languageconfiguration.preferences.LanguageConfigurationPreferencePage", TMUIMessages.TextMatePreferencePage_LanguageConfigurationRelatedLink); } // Add link to task tags preference page - addRelatedLink(composite, TaskTagsPreferencePage.PAGE_ID, TMUIMessages.TextMatePreferencePage_TaskTagsRelatedLink); + addRelatedLink(parent, TaskTagsPreferencePage.PAGE_ID, TMUIMessages.TextMatePreferencePage_TaskTagsRelatedLink); // Add link to theme preference page - addRelatedLink(composite, ThemePreferencePage.PAGE_ID, TMUIMessages.TextMatePreferencePage_ThemeRelatedLink); - - applyDialogFont(composite); - return composite; - + addRelatedLink(parent, ThemePreferencePage.PAGE_ID, TMUIMessages.TextMatePreferencePage_ThemeRelatedLink); } private void addRelatedLink(final Composite parent, final String pageId, final String message) { final var contentTypeArea = new PreferenceLinkArea(parent, SWT.NONE, pageId, message, (IWorkbenchPreferenceContainer) getContainer(), null); - final var data = new GridData(GridData.FILL_HORIZONTAL | GridData.GRAB_HORIZONTAL); - contentTypeArea.getControl().setLayoutData(data); + contentTypeArea.getControl() + .setLayoutData(new GridData(GridData.FILL_HORIZONTAL | GridData.GRAB_HORIZONTAL)); } @Override - public void init(@Nullable final IWorkbench workbench) { + public void init(final @NonNullByDefault({}) IWorkbench workbench) { + } + + @Override + protected void performDefaults() { + tmTokenHoverCheckbox.setSelection(PreferenceHelper.isTMTokenHoverEnabled()); + } + + @Override + public boolean performOk() { + try { + PreferenceHelper.saveTMTokenHoverEnabled(tmTokenHoverCheckbox.getSelection()); + } catch (final BackingStoreException ex) { + TMUIPlugin.logError(ex); + return false; + } + return super.performOk(); } }