Skip to content

Commit 82a0bf6

Browse files
authored
feat(langcfg): respect embedded content types for commenting/auto edit (#960)
1 parent 9f0f69c commit 82a0bf6

5 files changed

Lines changed: 538 additions & 44 deletions

File tree

org.eclipse.tm4e.languageconfiguration.tests/META-INF/MANIFEST.MF

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Require-Bundle: org.eclipse.core.runtime,
1616
org.eclipse.ui.workbench,
1717
org.eclipse.ui.workbench.texteditor,
1818
org.eclipse.tm4e.languageconfiguration,
19+
org.eclipse.tm4e.language_pack,
1920
org.eclipse.tm4e.ui,
2021
org.eclipse.tm4e.ui.tests,
2122
assertj-core
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
/**
2+
* Copyright (c) 2025 Vegard IT GmbH and others.
3+
* This program and the accompanying materials are made
4+
* available under the terms of the Eclipse Public License 2.0
5+
* which is available at https://www.eclipse.org/legal/epl-2.0/
6+
*
7+
* SPDX-License-Identifier: EPL-2.0
8+
*/
9+
package org.eclipse.tm4e.languageconfiguration.tests;
10+
11+
import static org.assertj.core.api.Assertions.*;
12+
13+
import java.io.ByteArrayInputStream;
14+
15+
import org.eclipse.core.resources.IFile;
16+
import org.eclipse.core.resources.IProject;
17+
import org.eclipse.core.resources.ResourcesPlugin;
18+
import org.eclipse.core.runtime.content.IContentType;
19+
import org.eclipse.jface.text.IDocument;
20+
import org.eclipse.jface.text.TextSelection;
21+
import org.eclipse.swt.custom.StyledText;
22+
import org.eclipse.swt.widgets.Control;
23+
import org.eclipse.tm4e.languageconfiguration.internal.ToggleLineCommentHandler;
24+
import org.eclipse.tm4e.ui.internal.utils.UI;
25+
import org.eclipse.tm4e.ui.tests.support.TestUtils;
26+
import org.eclipse.tm4e.ui.text.TMPartitions;
27+
import org.eclipse.ui.PlatformUI;
28+
import org.eclipse.ui.handlers.IHandlerService;
29+
import org.eclipse.ui.ide.IDE;
30+
import org.eclipse.ui.texteditor.ITextEditor;
31+
import org.junit.jupiter.api.AfterEach;
32+
import org.junit.jupiter.api.Test;
33+
34+
/**
35+
* Verifies TM partition mapping to content types and that auto-edit strategies
36+
* use partition-aware content types (basic smoke).
37+
*/
38+
public class TestPartitionAware {
39+
40+
@AfterEach
41+
public void tearDown() throws Exception {
42+
UI.getActivePage().closeAllEditors(false);
43+
for (final IProject p : ResourcesPlugin.getWorkspace().getRoot().getProjects()) {
44+
p.delete(true, true, null);
45+
}
46+
}
47+
48+
@Test
49+
public void testPartitionMapsToContentTypes() throws Exception {
50+
final IProject p = ResourcesPlugin.getWorkspace().getRoot().getProject(getClass().getName() + System.currentTimeMillis());
51+
p.create(null);
52+
p.open(null);
53+
final IFile file = p.getFile("embedded.html");
54+
final String html = """
55+
<html>
56+
<head>
57+
<style>
58+
body { color: red; }
59+
</style>
60+
</head>
61+
<body>
62+
<script>
63+
function x() { return 1; }
64+
</script>
65+
</body>
66+
</html>""";
67+
file.create(new ByteArrayInputStream(html.getBytes()), true, null);
68+
69+
final var editor = (ITextEditor) IDE.openEditor(UI.getActivePage(), file);
70+
final IDocument doc = editor.getDocumentProvider().getDocument(editor.getEditorInput());
71+
72+
// ensure TM partitioner installed and model tokenized
73+
TestUtils.waitForModelReady(doc, 5_000);
74+
TestUtils.waitForAndAssertCondition(5_000, () -> TMPartitions.hasPartitioning(doc));
75+
76+
final int cssIdx = doc.get().indexOf("color");
77+
final IContentType[] cssTypes = TMPartitions.getContentTypesForOffset(doc, cssIdx);
78+
assertThat(cssTypes).isNotEmpty();
79+
assertThat(cssTypes).anySatisfy(ct -> assertThat(ct.getId()).isEqualTo("org.eclipse.tm4e.language_pack.css"));
80+
81+
final int jsIdx = doc.get().indexOf("function x");
82+
final IContentType[] jsTypes = TMPartitions.getContentTypesForOffset(doc, jsIdx);
83+
assertThat(jsTypes).isNotEmpty();
84+
assertThat(jsTypes).anySatisfy(ct -> assertThat(ct.getId()).isEqualTo("org.eclipse.tm4e.language_pack.javascript"));
85+
}
86+
87+
@Test
88+
public void testAutoClosingUsesPartitionAtOffset() throws Exception {
89+
final IProject p = ResourcesPlugin.getWorkspace().getRoot().getProject(getClass().getName() + System.currentTimeMillis());
90+
p.create(null);
91+
p.open(null);
92+
final IFile file = p.getFile("embedded.html");
93+
final String html = """
94+
<html>
95+
<head>
96+
<style>
97+
body { color: red; }
98+
</style>
99+
</head>
100+
<body>
101+
<script>
102+
let a = 1;
103+
</script>
104+
</body>
105+
</html>""";
106+
file.create(new ByteArrayInputStream(html.getBytes()), true, null);
107+
108+
final var editor = (ITextEditor) IDE.openEditor(UI.getActivePage(), file);
109+
final IDocument doc = editor.getDocumentProvider().getDocument(editor.getEditorInput());
110+
TestUtils.waitForModelReady(doc, 5_000);
111+
TestUtils.waitForAndAssertCondition(5_000, () -> TMPartitions.hasPartitioning(doc));
112+
final StyledText styled = (StyledText) editor.getAdapter(Control.class);
113+
114+
// Move caret to inside <script>, end of 'let a = 1;'
115+
final int caret = doc.get().indexOf("let a = 1;")
116+
+ "let a = 1;".length();
117+
styled.setCaretOffset(caret);
118+
119+
// Type '(' and expect auto-close ')'. JS language configuration must be active here.
120+
final String before = styled.getText();
121+
styled.replaceTextRange(styled.getCaretOffset(), 0, "(");
122+
final String after = styled.getText();
123+
assertThat(after.length()).isEqualTo(before.length() + 2);
124+
assertThat(after.substring(caret, caret + 2)).isEqualTo("()");
125+
assertThat(styled.getCaretOffset()).isEqualTo(caret + 1);
126+
}
127+
128+
@Test
129+
public void testToggleLineCommentPartitionAwareEmbeddedScript() throws Exception {
130+
final IProject p = ResourcesPlugin.getWorkspace().getRoot().getProject(getClass().getName() + System.currentTimeMillis());
131+
p.create(null);
132+
p.open(null);
133+
final IFile file = p.getFile("partition-comments.html");
134+
final String html = """
135+
<html>
136+
<head>
137+
<style>
138+
body { color: red; }
139+
</style>
140+
</head>
141+
<body>
142+
<script>
143+
const first = 1;
144+
const second = 2;
145+
</script>
146+
</body>
147+
</html>""";
148+
file.create(new ByteArrayInputStream(html.getBytes()), true, null);
149+
150+
final var editor = (ITextEditor) IDE.openEditor(UI.getActivePage(), file);
151+
final IDocument doc = editor.getDocumentProvider().getDocument(editor.getEditorInput());
152+
TestUtils.waitForModelReady(doc, 5_000);
153+
TestUtils.waitForAndAssertCondition(5_000, () -> TMPartitions.hasPartitioning(doc));
154+
155+
final var service = PlatformUI.getWorkbench().getService(IHandlerService.class);
156+
final String original = doc.get();
157+
final int start = original.indexOf("const first = 1;");
158+
final int end = original.indexOf("const second = 2;") + "const second = 2;".length();
159+
editor.getSelectionProvider().setSelection(new TextSelection(start, end - start));
160+
161+
service.executeCommand(ToggleLineCommentHandler.TOGGLE_LINE_COMMENT_COMMAND_ID, null);
162+
final String commented = doc.get();
163+
final String expected = original.replace(" const first = 1;", "// const first = 1;")
164+
.replace(" const second = 2;", "// const second = 2;");
165+
assertThat(commented).isEqualTo(expected);
166+
167+
// Wait until the JS partition is restored for the commented lines, so that
168+
// the second toggle can again resolve the JS line comment token from
169+
// TMPartitions instead of temporarily seeing base/HTML.
170+
TestUtils.waitForAndAssertCondition(5_000, () -> {
171+
final int jsIdx = doc.get().indexOf("const first = 1;");
172+
if (jsIdx < 0)
173+
return false;
174+
return TMPartitions.getContentTypesForOffset(doc, jsIdx).length > 0;
175+
});
176+
177+
service.executeCommand(ToggleLineCommentHandler.TOGGLE_LINE_COMMENT_COMMAND_ID, null);
178+
assertThat(doc.get()).isEqualTo(original);
179+
}
180+
181+
@Test
182+
public void testBlockCommentCommandsPartitionAwareEmbeddedScript() throws Exception {
183+
final IProject p = ResourcesPlugin.getWorkspace().getRoot().getProject(getClass().getName() + System.currentTimeMillis());
184+
p.create(null);
185+
p.open(null);
186+
final IFile file = p.getFile("partition-block-comments.html");
187+
final String html = """
188+
<html>
189+
<body>
190+
<script>
191+
const alpha = 1;
192+
const beta = 2;
193+
</script>
194+
</body>
195+
</html>""";
196+
file.create(new ByteArrayInputStream(html.getBytes()), true, null);
197+
198+
final var editor = (ITextEditor) IDE.openEditor(UI.getActivePage(), file);
199+
final IDocument doc = editor.getDocumentProvider().getDocument(editor.getEditorInput());
200+
TestUtils.waitForModelReady(doc, 5_000);
201+
TestUtils.waitForAndAssertCondition(5_000, () -> TMPartitions.hasPartitioning(doc));
202+
203+
final var service = PlatformUI.getWorkbench().getService(IHandlerService.class);
204+
final String original = doc.get();
205+
final int start = original.indexOf("const alpha = 1;");
206+
final int end = original.indexOf("const beta = 2;") + "const beta = 2;".length();
207+
editor.getSelectionProvider().setSelection(new TextSelection(start, end - start));
208+
209+
service.executeCommand(ToggleLineCommentHandler.ADD_BLOCK_COMMENT_COMMAND_ID, null);
210+
final String afterAdd = doc.get();
211+
final String expectedAdd = original.substring(0, start) + "/*"
212+
+ original.substring(start, end) + "*/" + original.substring(end);
213+
assertThat(afterAdd).isEqualTo(expectedAdd);
214+
215+
editor.getSelectionProvider().setSelection(new TextSelection(start, end - start + 4));
216+
service.executeCommand(ToggleLineCommentHandler.REMOVE_BLOCK_COMMENT_COMMAND_ID, null);
217+
assertThat(doc.get()).isEqualTo(original);
218+
}
219+
220+
@Test
221+
public void testBlockCommentCommandsHtmlPartition() throws Exception {
222+
final IProject p = ResourcesPlugin.getWorkspace().getRoot().getProject(getClass().getName() + System.currentTimeMillis());
223+
p.create(null);
224+
p.open(null);
225+
final IFile file = p.getFile("html-block-comments.html");
226+
final String html = """
227+
<html>
228+
<body>
229+
<p>Alpha</p>
230+
</body>
231+
</html>""";
232+
file.create(new ByteArrayInputStream(html.getBytes()), true, null);
233+
234+
final var editor = (ITextEditor) IDE.openEditor(UI.getActivePage(), file);
235+
final IDocument doc = editor.getDocumentProvider().getDocument(editor.getEditorInput());
236+
TestUtils.waitForModelReady(doc, 5_000);
237+
TestUtils.waitForAndAssertCondition(5_000, () -> TMPartitions.hasPartitioning(doc));
238+
239+
final var service = PlatformUI.getWorkbench().getService(IHandlerService.class);
240+
final String original = doc.get();
241+
final int start = original.indexOf("Alpha");
242+
final int length = "Alpha".length();
243+
editor.getSelectionProvider().setSelection(new TextSelection(start, length));
244+
245+
service.executeCommand(ToggleLineCommentHandler.ADD_BLOCK_COMMENT_COMMAND_ID, null);
246+
final String afterAdd = doc.get();
247+
final String expected = original.substring(0, start) + "<!--Alpha-->" + original.substring(start + length);
248+
assertThat(afterAdd).isEqualTo(expected);
249+
250+
final int commentedOffset = afterAdd.indexOf("<!--Alpha-->");
251+
editor.getSelectionProvider()
252+
.setSelection(new TextSelection(commentedOffset, "<!--Alpha-->".length()));
253+
service.executeCommand(ToggleLineCommentHandler.REMOVE_BLOCK_COMMENT_COMMAND_ID, null);
254+
assertThat(doc.get()).isEqualTo(original);
255+
}
256+
257+
@Test
258+
public void testGetPartitionAndScopesInEmbeddedHtml() throws Exception {
259+
final IProject p = ResourcesPlugin.getWorkspace().getRoot().getProject(getClass().getName() + System.currentTimeMillis());
260+
p.create(null);
261+
p.open(null);
262+
final IFile file = p.getFile("embedded.html");
263+
final String html = """
264+
<html>
265+
<head>
266+
<style>
267+
body { color: red; }
268+
</style>
269+
</head>
270+
<body>
271+
<script>
272+
function x() { return 1; }
273+
</script>
274+
</body>
275+
</html>""";
276+
file.create(new ByteArrayInputStream(html.getBytes()), true, null);
277+
278+
final var editor = (ITextEditor) IDE.openEditor(UI.getActivePage(), file);
279+
final IDocument doc = editor.getDocumentProvider().getDocument(editor.getEditorInput());
280+
281+
TestUtils.waitForModelReady(doc, 5_000);
282+
TestUtils.waitForAndAssertCondition(5_000, () -> TMPartitions.hasPartitioning(doc));
283+
284+
// Base HTML at the opening tag
285+
final int htmlIdx = doc.get().indexOf("<html>") + 1;
286+
final var htmlPart = TMPartitions.getPartition(doc, htmlIdx);
287+
assertThat(htmlPart.getType()).isEqualTo("tm4e:text.html");
288+
assertThat(htmlPart.getGrammarScope()).isEqualTo("text.html");
289+
290+
// CSS inside <style>
291+
final int cssIdx = doc.get().indexOf("color");
292+
final var cssPart = TMPartitions.getPartition(doc, cssIdx);
293+
assertThat(cssPart.getType()).isEqualTo("tm4e:source.css");
294+
assertThat(cssPart.getGrammarScope()).isEqualTo("source.css");
295+
296+
// JavaScript inside <script>
297+
final int jsIdx = doc.get().indexOf("function x");
298+
final var jsPart = TMPartitions.getPartition(doc, jsIdx);
299+
assertThat(jsPart.getType()).isEqualTo("tm4e:source.js");
300+
assertThat(jsPart.getGrammarScope()).isEqualTo("source.js");
301+
}
302+
}

org.eclipse.tm4e.languageconfiguration/src/main/java/org/eclipse/tm4e/languageconfiguration/internal/LanguageConfigurationAutoEditStrategy.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*
99
* Contributors:
1010
* Angelo Zerr <angelo.zerr@gmail.com> - initial API and implementation
11+
* Sebastian Thomschke (Vegard IT GmbH) - resolve auto-edit content types from TM partitions
1112
*/
1213
package org.eclipse.tm4e.languageconfiguration.internal;
1314

@@ -35,6 +36,7 @@
3536
import org.eclipse.tm4e.ui.internal.model.TMModelManager;
3637
import org.eclipse.tm4e.ui.internal.utils.ContentTypeHelper;
3738
import org.eclipse.tm4e.ui.internal.utils.UI;
39+
import org.eclipse.tm4e.ui.text.TMPartitions;
3840

3941
/**
4042
* {@link IAutoEditStrategy} which uses VSCode language-configuration.json.
@@ -65,10 +67,17 @@ public void customizeDocumentCommand(final IDocument doc, final DocumentCommand
6567
if (contentTypes.length == 0 || command.getCommandCount() > 1)
6668
return;
6769

70+
// Determine effective content types based on TM4E partition at the edit location
71+
IContentType[] effectiveContentTypes = contentTypes;
72+
final var tmPartitionContentTypes = TMPartitions.getContentTypesForOffset(doc, command.offset);
73+
if (tmPartitionContentTypes.length > 0) {
74+
effectiveContentTypes = tmPartitionContentTypes;
75+
}
76+
6877
if (isEnter(doc, command)) {
6978
// enter-key pressed
7079
final var cursorCfg = TextEditorPrefs.getCursorConfiguration(UI.getActiveTextEditor());
71-
onEnter(cursorCfg, doc, contentTypes, command);
80+
onEnter(cursorCfg, doc, effectiveContentTypes, command);
7281
return;
7382
}
7483

@@ -78,7 +87,7 @@ public void customizeDocumentCommand(final IDocument doc, final DocumentCommand
7887
// auto surround pair
7988
final var textSelection = UI.getActiveTextSelection();
8089
if (textSelection != null && textSelection.getLength() > 0) {
81-
for (final IContentType contentType : contentTypes) {
90+
for (final IContentType contentType : effectiveContentTypes) {
8291
if (!registry.shouldSurroundingPairs(contentType))
8392
continue;
8493

@@ -104,7 +113,7 @@ public void customizeDocumentCommand(final IDocument doc, final DocumentCommand
104113
}
105114

106115
// auto close pair
107-
for (final IContentType contentType : contentTypes) {
116+
for (final IContentType contentType : effectiveContentTypes) {
108117
final var autoClosingPair = registry.getAutoClosingPair(doc.get(), command.offset, command.text, contentType);
109118
if (autoClosingPair == null) {
110119
continue;

0 commit comments

Comments
 (0)