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();