From 501ede88852ec05ac9f0414c75783effa33cb1e9 Mon Sep 17 00:00:00 2001 From: Sebastian Thomschke Date: Fri, 8 Mar 2024 19:43:57 +0100 Subject: [PATCH] feat: add support inserting surrounding pairs for text selections (#728) --- .../tests/TestSurroundingPairs.java | 82 +++++++++++++++++++ .../META-INF/MANIFEST.MF | 3 +- ...LanguageConfigurationAutoEditStrategy.java | 48 +++++++---- .../eclipse/tm4e/ui/internal/utils/UI.java | 10 +++ 4 files changed, 128 insertions(+), 15 deletions(-) create mode 100644 org.eclipse.tm4e.languageconfiguration.tests/src/main/java/org/eclipse/tm4e/languageconfiguration/tests/TestSurroundingPairs.java diff --git a/org.eclipse.tm4e.languageconfiguration.tests/src/main/java/org/eclipse/tm4e/languageconfiguration/tests/TestSurroundingPairs.java b/org.eclipse.tm4e.languageconfiguration.tests/src/main/java/org/eclipse/tm4e/languageconfiguration/tests/TestSurroundingPairs.java new file mode 100644 index 000000000..d495f5c6c --- /dev/null +++ b/org.eclipse.tm4e.languageconfiguration.tests/src/main/java/org/eclipse/tm4e/languageconfiguration/tests/TestSurroundingPairs.java @@ -0,0 +1,82 @@ +/** + * 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.languageconfiguration.tests; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.ByteArrayInputStream; +import java.util.stream.Stream; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.widgets.Control; +import org.eclipse.tm4e.languageconfiguration.internal.registry.LanguageConfigurationRegistryManager; +import org.eclipse.tm4e.ui.internal.utils.UI; +import org.eclipse.ui.ide.IDE; +import org.eclipse.ui.texteditor.ITextEditor; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +public class TestSurroundingPairs { + + @AfterEach + public void tearDown() throws Exception { + UI.getActivePage().closeAllEditors(false); + for (final IProject p : ResourcesPlugin.getWorkspace().getRoot().getProjects()) { + p.delete(true, null); + } + } + + @Test + public void testSurroundingPairs() throws Exception { + final IProject p = ResourcesPlugin.getWorkspace().getRoot().getProject(getClass().getName() + System.currentTimeMillis()); + p.create(null); + p.open(null); + final IFile file = p.getFile("test.lc-test"); + file.create(new ByteArrayInputStream(new byte[0]), true, null); + + final var contentType = file.getContentDescription().getContentType(); + final var langDef = Stream.of(LanguageConfigurationRegistryManager.getInstance().getDefinitions()) + .filter(e -> e.getContentType().equals(contentType)) + .findFirst() + .get(); + + final ITextEditor editor = (ITextEditor) IDE.openEditor(UI.getActivePage(), file); + final StyledText text = (StyledText) editor.getAdapter(Control.class); + + // test with enabled surrounding pairs + langDef.setMatchingPairsEnabled(true); + + text.setText("the mountain is high"); + text.setSelection(4, 12); + assertEquals(12, text.getCaretOffset()); + assertEquals("mountain", text.getSelectionText()); + text.insert("("); + assertEquals("the (mountain) is high", text.getText()); + assertEquals("mountain", text.getSelectionText()); + assertEquals(13, text.getCaretOffset()); + + // test with disabled surrounding pairs + langDef.setMatchingPairsEnabled(false); + + text.setText("the mountain is high"); + text.setSelection(4, 12); + assertEquals(12, text.getCaretOffset()); + assertEquals("mountain", text.getSelectionText()); + text.insert("("); + assertEquals("the ( is high", text.getText()); + + } +} diff --git a/org.eclipse.tm4e.languageconfiguration/META-INF/MANIFEST.MF b/org.eclipse.tm4e.languageconfiguration/META-INF/MANIFEST.MF index 3a19ddb3f..53911543c 100644 --- a/org.eclipse.tm4e.languageconfiguration/META-INF/MANIFEST.MF +++ b/org.eclipse.tm4e.languageconfiguration/META-INF/MANIFEST.MF @@ -23,6 +23,7 @@ Require-Bundle: org.eclipse.core.expressions, com.google.gson;bundle-version="[2.10.1,3.0.0)" Bundle-Activator: org.eclipse.tm4e.languageconfiguration.LanguageConfigurationPlugin Export-Package: org.eclipse.tm4e.languageconfiguration, - org.eclipse.tm4e.languageconfiguration.internal;x-friends:="org.eclipse.tm4e.languageconfiguration.tests" + org.eclipse.tm4e.languageconfiguration.internal;x-friends:="org.eclipse.tm4e.languageconfiguration.tests", + org.eclipse.tm4e.languageconfiguration.internal.registry;x-friends:="org.eclipse.tm4e.languageconfiguration.tests" Bundle-ActivationPolicy: lazy Automatic-Module-Name: org.eclipse.tm4e.languageconfiguration diff --git a/org.eclipse.tm4e.languageconfiguration/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/LanguageConfigurationAutoEditStrategy.java b/org.eclipse.tm4e.languageconfiguration/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/LanguageConfigurationAutoEditStrategy.java index c096bc990..3e92f8ad4 100644 --- a/org.eclipse.tm4e.languageconfiguration/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/LanguageConfigurationAutoEditStrategy.java +++ b/org.eclipse.tm4e.languageconfiguration/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/LanguageConfigurationAutoEditStrategy.java @@ -14,6 +14,7 @@ import static org.eclipse.tm4e.languageconfiguration.internal.utils.TextUtils.*; import java.util.Arrays; +import java.util.List; import org.eclipse.core.runtime.content.IContentType; import org.eclipse.jdt.annotation.Nullable; @@ -22,9 +23,9 @@ import org.eclipse.jface.text.DocumentCommand; import org.eclipse.jface.text.IAutoEditStrategy; import org.eclipse.jface.text.IDocument; -import org.eclipse.jface.text.ITextViewer; import org.eclipse.tm4e.core.model.TMToken; import org.eclipse.tm4e.languageconfiguration.LanguageConfigurationPlugin; +import org.eclipse.tm4e.languageconfiguration.internal.model.AutoClosingPair; import org.eclipse.tm4e.languageconfiguration.internal.model.AutoClosingPairConditional; import org.eclipse.tm4e.languageconfiguration.internal.model.CursorConfiguration; import org.eclipse.tm4e.languageconfiguration.internal.registry.LanguageConfigurationRegistryManager; @@ -44,12 +45,11 @@ public class LanguageConfigurationAutoEditStrategy implements IAutoEditStrategy private IContentType[] contentTypes = EMPTY_CONTENT_TYPES; private @Nullable IDocument document; - private @Nullable ITextViewer viewer; /** * @see - * github.com/microsoft/vscode/src/vs/editor/common/cursor/cursorTypeOperations.ts + * github.com/microsoft/vscode/src/vs/editor/common/cursor/cursorTypeOperations.ts#typeWithInterceptors */ @Override public void customizeDocumentCommand(@Nullable final IDocument doc, @Nullable final DocumentCommand command) { @@ -62,13 +62,11 @@ public void customizeDocumentCommand(@Nullable final IDocument doc, @Nullable fi this.document = doc; } - if (contentTypes.length == 0) + if (contentTypes.length == 0 || command.getCommandCount() > 1) return; - installViewer(); - if (isEnter(doc, command)) { - // key enter pressed + // enter-key pressed final var cursorCfg = TextEditorPrefs.getCursorConfiguration(UI.getActiveTextEditor()); onEnter(cursorCfg, doc, contentTypes, command); return; @@ -76,8 +74,36 @@ public void customizeDocumentCommand(@Nullable final IDocument doc, @Nullable fi final var registry = LanguageConfigurationRegistryManager.getInstance(); - // auto close pair if (command.text.length() == 1) { + // auto surround pair + final var textSelection = UI.getActiveTextSelection(); + if (textSelection != null && textSelection.getLength() > 0) { + for (final IContentType contentType : contentTypes) { + if (!registry.shouldSurroundingPairs(contentType)) + continue; + + final List surroundingPairs = registry.getSurroundingPairs(contentType); + if (surroundingPairs.isEmpty()) + continue; + + for (final AutoClosingPair pair : surroundingPairs) { + if (command.text.equals(pair.open)) { + // surround selection with pairs + try { + command.addCommand(command.offset + textSelection.getLength(), 0, pair.close, null); + command.length = 0; + command.caretOffset = command.offset + textSelection.getLength() + pair.open.length(); + command.shiftsCaret = false; + } catch (final BadLocationException ex) { + LanguageConfigurationPlugin.logError(ex); + } + return; + } + } + } + } + + // auto close pair for (final IContentType contentType : contentTypes) { final var autoClosingPair = registry.getAutoClosingPair(doc.get(), command.offset, command.text, contentType); if (autoClosingPair == null) { @@ -274,10 +300,4 @@ private static void onEnter(final CursorConfiguration cursorCfg, final IDocument // fail back to default for indentation new DefaultIndentLineAutoEditStrategy().customizeDocumentCommand(doc, command); } - - private void installViewer() { - if (viewer == null) { - viewer = UI.getActiveTextViewer(); - } - } } diff --git a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/utils/UI.java b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/utils/UI.java index f0bdfa69b..bdc825f8d 100644 --- a/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/utils/UI.java +++ b/org.eclipse.tm4e.ui/src/main/java/org/eclipse/tm4e/ui/internal/utils/UI.java @@ -17,6 +17,7 @@ import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jface.dialogs.Dialog; import org.eclipse.jface.resource.JFaceResources; +import org.eclipse.jface.text.ITextSelection; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.TableViewer; @@ -77,6 +78,15 @@ public static ITextEditor getActiveTextEditor() { return null; } + public static @Nullable ITextSelection getActiveTextSelection() { + final var editor = getActiveTextEditor(); + if (editor == null) + return null; + if (editor.getSelectionProvider().getSelection() instanceof final ITextSelection sel) + return sel; + return null; + } + @Nullable public static ITextViewer getActiveTextViewer() { final var editor = getActiveTextEditor();