Skip to content

Commit

Permalink
feat: add support inserting surrounding pairs for text selections (#728)
Browse files Browse the repository at this point in the history
  • Loading branch information
sebthom committed Mar 8, 2024
1 parent 737db92 commit 501ede8
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -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());

}
}
3 changes: 2 additions & 1 deletion org.eclipse.tm4e.languageconfiguration/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -44,12 +45,11 @@ public class LanguageConfigurationAutoEditStrategy implements IAutoEditStrategy

private IContentType[] contentTypes = EMPTY_CONTENT_TYPES;
private @Nullable IDocument document;
private @Nullable ITextViewer viewer;

/**
* @see <a href=
* "https://github.com/microsoft/vscode/blob/ba2cf46e20df3edf77bdd905acde3e175d985f70/src/vs/editor/common/cursor/cursorTypeOperations.ts#L934">
* github.com/microsoft/vscode/src/vs/editor/common/cursor/cursorTypeOperations.ts</a>
* github.com/microsoft/vscode/src/vs/editor/common/cursor/cursorTypeOperations.ts#typeWithInterceptors</a>
*/
@Override
public void customizeDocumentCommand(@Nullable final IDocument doc, @Nullable final DocumentCommand command) {
Expand All @@ -62,22 +62,48 @@ 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;
}

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<AutoClosingPair> 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) {
Expand Down Expand Up @@ -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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down

0 comments on commit 501ede8

Please sign in to comment.