From 35a3f6464985075b43ce9a58fb53121ca7c1968b Mon Sep 17 00:00:00 2001 From: Martin Lippert Date: Wed, 17 Jan 2024 14:13:39 +0100 Subject: [PATCH] GH-1163: added basic content-assist to easily add common spring namespaces --- .../languageserver/testharness/Editor.java | 40 +- .../boot/xml/SpringXMLCompletionEngine.java | 21 +- .../GenericXMLCompletionProposal.java | 28 +- .../NamespaceCompletionProvider.java | 218 ++++++++++ .../boot/xml/test/XMLContentAssistTest.java | 374 +++++++++++++++++- 5 files changed, 657 insertions(+), 24 deletions(-) create mode 100644 headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/completions/NamespaceCompletionProvider.java diff --git a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java index 816a5149a..cbcf3fd7e 100644 --- a/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java +++ b/headless-services/commons/language-server-test-harness/src/main/java/org/springframework/ide/vscode/languageserver/testharness/Editor.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2016, 2023 Pivotal, Inc. + * Copyright (c) 2016, 2024 Pivotal, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -57,6 +57,7 @@ import org.springframework.ide.vscode.commons.protocol.HighlightParams; import org.springframework.ide.vscode.commons.util.StringUtil; import org.springframework.ide.vscode.commons.util.Unicodes; import org.springframework.ide.vscode.commons.util.text.LanguageId; +import org.springframework.ide.vscode.commons.util.text.TextDocument; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -452,7 +453,13 @@ public class Editor { public void apply(CompletionItem completion) throws Exception { completion = harness.resolveCompletionItem(completion); Either edit = completion.getTextEdit(); - String docText = doc.getText(); + + TextDocument original = new TextDocument(doc.getUri(), doc.getLanguageId()); + original.setText(doc.getText()); + + int insertPosition = 0; + int insertLength = 0; + if (edit != null) { if (edit.isLeft()) { String replaceWith = edit.getLeft().getNewText(); @@ -480,16 +487,24 @@ public class Editor { } Range rng = edit.getLeft().getRange(); - int start = doc.toOffset(rng.getStart()); - int end = doc.toOffset(rng.getEnd()); + + int start = original.toOffset(rng.getStart()); + int end = original.toOffset(rng.getEnd()); replaceText(start, end, replaceWith); + + insertPosition = start; + insertLength = replaceWith.length() - (end - start); + selectionStart = selectionEnd = start+cursorReplaceOffset; } else { throw new UnsupportedOperationException("InsertReplaceEdit edits not supported"); } } else { String insertText = getInsertText(completion); - String newText = docText.substring(0, selectionStart) + insertText + docText.substring(selectionStart); + String newText = original.get().substring(0, selectionStart) + insertText + original.get().substring(selectionStart); + + insertPosition = selectionStart; + insertLength = insertText.length(); selectionStart+= insertText.length(); selectionEnd += insertText.length(); @@ -501,10 +516,19 @@ public class Editor { for (TextEdit te : completion.getAdditionalTextEdits()) { String replaceWith = te.getNewText(); Range rng = te.getRange(); - int start = doc.toOffset(rng.getStart()); - int end = doc.toOffset(rng.getEnd()); + int start = original.toOffset(rng.getStart()); + int end = original.toOffset(rng.getEnd()); + + // adjust start/end, if the completion insert text was positioned before the additional text edits + if (start > insertPosition) { + start += insertLength; + end += insertLength; + } + else { + selectionStart = selectionEnd = selectionStart - (end - start) + replaceWith.length(); + } + replaceText(start, end, replaceWith); - selectionStart = selectionEnd = selectionStart - (end - start) + replaceWith.length(); } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/SpringXMLCompletionEngine.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/SpringXMLCompletionEngine.java index 8f65e583d..461b1489d 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/SpringXMLCompletionEngine.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/SpringXMLCompletionEngine.java @@ -62,6 +62,7 @@ import org.springframework.ide.vscode.boot.app.SpringSymbolIndex; import org.springframework.ide.vscode.boot.xml.completions.BeanRefCompletionProposalProvider; import org.springframework.ide.vscode.boot.xml.completions.ConstructorArgNameCompletionProposalProvider; import org.springframework.ide.vscode.boot.xml.completions.GenericXMLCompletionProposal; +import org.springframework.ide.vscode.boot.xml.completions.NamespaceCompletionProvider; import org.springframework.ide.vscode.boot.xml.completions.PropertyNameCompletionProposalProvider; import org.springframework.ide.vscode.boot.xml.completions.TypeCompletionProposalProvider; import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits; @@ -85,7 +86,7 @@ public class SpringXMLCompletionEngine implements ICompletionEngine, LanguageSpe private final Map completionProviders; private final BootJavaConfig config; - + public SpringXMLCompletionEngine( SimpleLanguageServer server, JavaProjectFinder projectFinder, @@ -180,6 +181,22 @@ public class SpringXMLCompletionEngine implements ICompletionEngine, LanguageSpe } } break; + case AttributeName: + if (scanner.getTokenOffset() <= offset && offset < scanner.getTokenEnd()) { + if (node.getParentNode() != null && node.getParentNode() instanceof DOMDocument + && namespace.equals("http://www.springframework.org/schema/beans") && node.getNodeName().equals("beans")) { + return NamespaceCompletionProvider.createNamespaceCompletionProposals(doc, offset, token, node); + } + } + break; + case Whitespace: + if (scanner.getTokenOffset() <= offset && offset < scanner.getTokenEnd()) { + if (node.getParentNode() != null && node.getParentNode() instanceof DOMDocument + && namespace.equals("http://www.springframework.org/schema/beans") && node.getNodeName().equals("beans")) { + return NamespaceCompletionProvider.createNamespaceCompletionProposals(doc, offset, token, node); + } + } + break; default: break; } @@ -229,7 +246,7 @@ public class SpringXMLCompletionEngine implements ICompletionEngine, LanguageSpe return completions; } - + @Override public Collection supportedLanguages() { return ImmutableList.of(LanguageId.XML); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/completions/GenericXMLCompletionProposal.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/completions/GenericXMLCompletionProposal.java index 0870dff7f..b6dd750c0 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/completions/GenericXMLCompletionProposal.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/completions/GenericXMLCompletionProposal.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2019, 2020 Pivotal, Inc. + * Copyright (c) 2019, 2024 Pivotal, Inc. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -10,6 +10,9 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.xml.completions; +import java.util.Optional; +import java.util.function.Supplier; + import org.eclipse.lsp4j.CompletionItemKind; import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits; import org.springframework.ide.vscode.commons.languageserver.completion.ScoreableProposal; @@ -26,8 +29,14 @@ public class GenericXMLCompletionProposal extends ScoreableProposal { private final String detail; private final Renderable documentation; private final double score; + private final String filterText; + private final DocumentEdits additionalEdits; public GenericXMLCompletionProposal(String label, CompletionItemKind kind, DocumentEdits edits, String detail, Renderable documentation, double score) { + this(label, kind, edits, detail, documentation, score, label, null); + } + + public GenericXMLCompletionProposal(String label, CompletionItemKind kind, DocumentEdits edits, String detail, Renderable documentation, double score, String filterText, DocumentEdits additionalEdits) { super(); this.label = label; this.kind = kind; @@ -35,6 +44,8 @@ public class GenericXMLCompletionProposal extends ScoreableProposal { this.detail = detail; this.documentation = documentation; this.score = score; + this.filterText = filterText; + this.additionalEdits = additionalEdits; } @Override @@ -66,5 +77,20 @@ public class GenericXMLCompletionProposal extends ScoreableProposal { public double getBaseScore() { return this.score; } + + @Override + public String getFilterText() { + return this.filterText; + } + + @Override + public Optional> getAdditionalEdit() { + if (this.additionalEdits != null) { + return Optional.of(() -> additionalEdits); + } + else { + return Optional.empty(); + } + } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/completions/NamespaceCompletionProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/completions/NamespaceCompletionProvider.java new file mode 100644 index 000000000..862c0f9b6 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/xml/completions/NamespaceCompletionProvider.java @@ -0,0 +1,218 @@ +/******************************************************************************* + * Copyright (c) 2024 Broadcom + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Broadcom - initial API and implementation + *******************************************************************************/ +package org.springframework.ide.vscode.boot.xml.completions; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; + +import org.eclipse.lemminx.dom.DOMAttr; +import org.eclipse.lemminx.dom.DOMNode; +import org.eclipse.lemminx.dom.DOMRange; +import org.eclipse.lemminx.dom.parser.TokenType; +import org.eclipse.lsp4j.CompletionItemKind; +import org.springframework.ide.vscode.commons.languageserver.completion.DocumentEdits; +import org.springframework.ide.vscode.commons.languageserver.completion.ICompletionProposal; +import org.springframework.ide.vscode.commons.languageserver.completion.IndentUtil; +import org.springframework.ide.vscode.commons.languageserver.util.PrefixFinder; +import org.springframework.ide.vscode.commons.util.Renderable; +import org.springframework.ide.vscode.commons.util.Renderables; +import org.springframework.ide.vscode.commons.util.text.TextDocument; + +public class NamespaceCompletionProvider { + + private static PrefixFinder PREFIX_FINDER = new PrefixFinder() { + @Override + protected boolean isPrefixChar(char c) { + return Character.isAlphabetic(c) || c == ':'; + } + }; + + public static Collection createNamespaceCompletionProposals(TextDocument doc, int offset, TokenType token, DOMNode node) { + String prefix = PREFIX_FINDER.getPrefix(doc, offset); + + if (prefix != null) { + Collection result = new ArrayList<>(); + + NamespaceInformation[] namespaces = getNamespaces(); + for (NamespaceInformation namespace : namespaces) { + if (namespace.getNamespaceURI().startsWith(prefix)) { + result.add(createCompletionProposal(namespace, prefix, doc, offset, token, node)); + } + } + + return result; + } + else { + return Collections.emptyList(); + } + } + + private static ICompletionProposal createCompletionProposal(NamespaceInformation namespace, String prefix, TextDocument doc, int offset, TokenType token, DOMNode node) { + + IndentUtil indentUtil = new IndentUtil(doc); + + // main edit (inserts the namespace attribute itself) + DocumentEdits edits = new DocumentEdits(doc, false); + String startIndent = indentUtil.getReferenceIndent(offset, doc); + edits.replace(offset - prefix.length(), offset, namespace.getNamespaceURI() + "\n" + startIndent); + + // additional edit (inserts the namespace location to an existing xsi:schemaLocation attribute) + DocumentEdits additionalEdits = null; + DOMAttr attributeNode = node.getAttributeNode("xsi:schemaLocation"); + if (attributeNode != null) { + DOMRange range = attributeNode.getNodeAttrValue(); + + String locationIndent = indentUtil.getReferenceIndent(range.getEnd(), doc); + String locationIndentAtStart = indentUtil.getReferenceIndent(range.getStart(), doc); + + if (locationIndent.length() == locationIndentAtStart.length()) { + locationIndent = locationIndent + locationIndent; + } + + String additionalInsertText = "\n" + + locationIndent + + namespace.getNamespaceLocation(); + + additionalEdits = new DocumentEdits(doc, false); + additionalEdits.insert(range.getEnd() - 1, additionalInsertText); + } + + Renderable documentation = Renderables.text(namespace.getDetails()); + return new GenericXMLCompletionProposal(namespace.getLabel(), CompletionItemKind.Text, edits, namespace.getDetails(), documentation, 1.0d, namespace.getNamespaceURI(), additionalEdits); + } + + public static NamespaceInformation[] getNamespaces() { + return NAMESPACES; + } + + public static class NamespaceInformation { + + private final String label; + private final String details; + private final String namespaceURI; + private final String namespaceLocation; + + public NamespaceInformation(String label, String details, String namespaceURI, + String namespaceLocation) { + this.label = label; + this.details = details; + this.namespaceURI = namespaceURI; + this.namespaceLocation = namespaceLocation; + } + + public String getLabel() { + return label; + } + + public String getDetails() { + return details; + } + + public String getNamespaceURI() { + return namespaceURI; + } + + public String getNamespaceLocation() { + return namespaceLocation; + } + + } + + private static final NamespaceInformation[] NAMESPACES = { + + // core framework + new NamespaceInformation( + "http://www.springframework.org/schema/beans", + "http://www.springframework.org/schema/beans", + "xmlns:beans=\"http://www.springframework.org/schema/beans\"", + "http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"), + new NamespaceInformation( + "http://www.springframework.org/schema/context", + "http://www.springframework.org/schema/context", + "xmlns:context=\"http://www.springframework.org/schema/context\"", + "http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"), + new NamespaceInformation( + "http://www.springframework.org/schema/tx", + "http://www.springframework.org/schema/tx", + "xmlns:tx=\"http://www.springframework.org/schema/tx\"", + "http://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx.xsd"), + new NamespaceInformation( + "http://www.springframework.org/schema/aop", + "http://www.springframework.org/schema/aop", + "xmlns:aop=\"http://www.springframework.org/schema/aop\"", + "http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd"), + new NamespaceInformation( + "http://www.springframework.org/schema/oxm", + "http://www.springframework.org/schema/oxm", + "xmlns:oxm=\"http://www.springframework.org/schema/oxm\"", + "http://www.springframework.org/schema/oxm https://www.springframework.org/schema/oxm/spring-oxm.xsd"), + new NamespaceInformation( + "http://www.springframework.org/schema/jms", + "http://www.springframework.org/schema/jms", + "xmlns:jms=\"http://www.springframework.org/schema/jms\"", + "http://www.springframework.org/schema/jms https://www.springframework.org/schema/jms/spring-jms.xsd"), + new NamespaceInformation( + "http://www.springframework.org/schema/jdbc", + "http://www.springframework.org/schema/jdbc", + "xmlns:jdbc=\"http://www.springframework.org/schema/jdbc\"", + "http://www.springframework.org/schema/jdbc https://www.springframework.org/schema/jdbc/spring-jdbc.xsd"), + new NamespaceInformation( + "http://www.springframework.org/schema/tool", + "http://www.springframework.org/schema/tool", + "xmlns:tool=\"http://www.springframework.org/schema/tool\"", + "http://www.springframework.org/schema/tool https://www.springframework.org/schema/tool/spring-tool.xsd"), + new NamespaceInformation( + "http://www.springframework.org/schema/util", + "http://www.springframework.org/schema/util", + "xmlns:util=\"http://www.springframework.org/schema/util\"", + "http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd"), + new NamespaceInformation( + "http://www.springframework.org/schema/mvc", + "http://www.springframework.org/schema/mvc", + "xmlns:mvc=\"http://www.springframework.org/schema/mvc\"", + "http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd"), + new NamespaceInformation( + "http://www.springframework.org/schema/jee", + "http://www.springframework.org/schema/jee", + "xmlns:jee=\"http://www.springframework.org/schema/jee\"", + "http://www.springframework.org/schema/jee https://www.springframework.org/schema/jee/spring-jee.xsd"), + new NamespaceInformation( + "http://www.springframework.org/schema/lang", + "http://www.springframework.org/schema/lang", + "xmlns:lang=\"http://www.springframework.org/schema/lang\"", + "http://www.springframework.org/schema/lang https://www.springframework.org/schema/lang/spring-lang.xsd"), + new NamespaceInformation( + "http://www.springframework.org/schema/task", + "http://www.springframework.org/schema/task", + "xmlns:task=\"http://www.springframework.org/schema/task\"", + "http://www.springframework.org/schema/task https://www.springframework.org/schema/task/spring-task.xsd"), + new NamespaceInformation( + "http://www.springframework.org/schema/cache", + "http://www.springframework.org/schema/cache", + "xmlns:cache=\"http://www.springframework.org/schema/cache\"", + "http://www.springframework.org/schema/cache https://www.springframework.org/schema/cache/spring-cache.xsd"), + new NamespaceInformation( + "http://www.springframework.org/schema/websocket", + "http://www.springframework.org/schema/websocket", + "xmlns:websocket=\"http://www.springframework.org/schema/websocket\"", + "http://www.springframework.org/schema/websocket https://www.springframework.org/schema/websocket/spring-websocket.xsd"), + + // security + new NamespaceInformation( + "http://www.springframework.org/schema/security", + "http://www.springframework.org/schema/security", + "xmlns:security=\"http://www.springframework.org/schema/security\"", + "http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd"), + + }; + +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/xml/test/XMLContentAssistTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/xml/test/XMLContentAssistTest.java index ffa78e78d..9347031c1 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/xml/test/XMLContentAssistTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/xml/test/XMLContentAssistTest.java @@ -1,19 +1,18 @@ /******************************************************************************* - * Copyright (c) 2024 Broadcom, Inc. + * Copyright (c) 2024 Broadcom * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * https://www.eclipse.org/legal/epl-v10.html * * Contributors: - * Broadcom, Inc. - initial API and implementation + * Broadcom - initial API and implementation *******************************************************************************/ package org.springframework.ide.vscode.boot.xml.test; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -33,9 +32,9 @@ import org.springframework.ide.vscode.boot.app.BootLanguageServerBootApp; import org.springframework.ide.vscode.boot.app.SpringSymbolIndex; import org.springframework.ide.vscode.boot.bootiful.XmlBeansTestConf; import org.springframework.ide.vscode.boot.java.utils.test.MockProjectObserver; +import org.springframework.ide.vscode.boot.xml.completions.NamespaceCompletionProvider; import org.springframework.ide.vscode.commons.languageserver.util.Settings; import org.springframework.ide.vscode.commons.maven.java.MavenJavaProject; -import org.springframework.ide.vscode.commons.util.UriUtil; import org.springframework.ide.vscode.commons.util.text.LanguageId; import org.springframework.ide.vscode.languageserver.starter.LanguageServerAutoConf; import org.springframework.ide.vscode.languageserver.testharness.Editor; @@ -71,6 +70,7 @@ public class XMLContentAssistTest { private MavenJavaProject project; private Level originalLevel; + private int ALL_NAMESPACE_COMPLETIONS = NamespaceCompletionProvider.getNamespaces().length; @BeforeEach public void setup() throws Exception { @@ -112,10 +112,7 @@ public class XMLContentAssistTest { @Test void testEmptyXMLFileCompletions() throws Exception { - Path xmlFilePath = Paths.get(project.getLocationUri()).resolve("beans.xml"); - Editor editor = harness.newEditor(LanguageId.XML, "", - UriUtil.toUri(xmlFilePath.toFile()).toString() - ); + Editor editor = new Editor(harness, "<*>", LanguageId.XML); List completions = editor.getCompletions(); assertEquals(1, completions.size()); @@ -124,13 +121,364 @@ public class XMLContentAssistTest { @Test void testNoSkeletonSnippetForNonEmptyXMLFile() throws Exception { - Path xmlFilePath = Paths.get(project.getLocationUri()).resolve("beans.xml"); - Editor editor = harness.newEditor(LanguageId.XML, "\\n", - UriUtil.toUri(xmlFilePath.toFile()).toString() - ); + Editor editor = new Editor(harness, "<*>\\n", LanguageId.XML); + List completions = editor.getCompletions(); + assertEquals(0, completions.size()); + } + + @Test + void testAddNamespaceCompletion1() throws Exception { + Editor editor = new Editor(harness, """ + + xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> + + + + + """, + LanguageId.XML); + + List completions = editor.getCompletions(); + assertEquals(ALL_NAMESPACE_COMPLETIONS, completions.size()); + + CompletionItem contextItem = getContextCompletionItem(completions); + assertNotNull(contextItem); + + editor.apply(contextItem); + String editorContent = editor.getText(); + + assertEquals(""" + + xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> + + + + + """, + editorContent); + } + + @Test + void testAddNamespaceCompletion2() throws Exception { + Editor editor = new Editor(harness, """ + + + xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> + + + + + """, + LanguageId.XML); + + List completions = editor.getCompletions(); + assertEquals(ALL_NAMESPACE_COMPLETIONS, completions.size()); + + CompletionItem contextItem = getContextCompletionItem(completions); + assertNotNull(contextItem); + + editor.apply(contextItem); + String editorContent = editor.getText(); + + assertEquals(""" + + + xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> + + + + + """, + editorContent); + } + + @Test + void testAddNamespaceCompletion3() throws Exception { + Editor editor = new Editor(harness, """ + + + xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> + + + + + """, + LanguageId.XML); + + List completions = editor.getCompletions(); + assertEquals(ALL_NAMESPACE_COMPLETIONS, completions.size()); + + CompletionItem contextItem = getContextCompletionItem(completions); + assertNotNull(contextItem); + + editor.apply(contextItem); + String editorContent = editor.getText(); + + assertEquals(""" + + + xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> + + + + + """, + editorContent); + } + + @Test + void testAddNamespaceCompletion4() throws Exception { + Editor editor = new Editor(harness, """ + + + xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> + + + + + """, + LanguageId.XML); + + List completions = editor.getCompletions(); + assertEquals(ALL_NAMESPACE_COMPLETIONS, completions.size()); + + CompletionItem contextItem = getContextCompletionItem(completions); + assertNotNull(contextItem); + + editor.apply(contextItem); + String editorContent = editor.getText(); + + assertEquals(""" + + + xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> + + + + + """, + editorContent); + } + + @Test + void testAddNamespaceCompletion5() throws Exception { + Editor editor = new Editor(harness, """ + + + xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd"> + + + + + """, + LanguageId.XML); + + List completions = editor.getCompletions(); + assertEquals(ALL_NAMESPACE_COMPLETIONS, completions.size()); + + CompletionItem contextItem = getContextCompletionItem(completions); + assertNotNull(contextItem); + + editor.apply(contextItem); + String editorContent = editor.getText(); + + assertEquals(""" + + + xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd + http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> + + + + + """, + editorContent); + } + + + + @Test + void testNoNamespaceCompletionOutsideOfMainElement1() throws Exception { + Editor editor = new Editor(harness, """ + + + + + + <*> + + """, + LanguageId.XML); List completions = editor.getCompletions(); assertEquals(0, completions.size()); } + @Test + void testNoNamespaceCompletionOutsideOfMainElement2() throws Exception { + Editor editor = new Editor(harness, """ + + + + + <*> + + """, + LanguageId.XML); + + List completions = editor.getCompletions(); + assertEquals(0, completions.size()); + } + + @Test + void testNoNamespaceCompletionOutsideOfMainElement3() throws Exception { + Editor editor = new Editor(harness, """ + + + + + <<*>bean id="simpleObj" class="u.t.r.SimpleObj"> + + """, + LanguageId.XML); + + List completions = editor.getCompletions(); + assertEquals(0, completions.size()); + } + + @Test + void testNoNamespaceCompletionOutsideOfMainElement4() throws Exception { + Editor editor = new Editor(harness, """ + + + + + id="simpleObj" class="u.t.r.SimpleObj"> + + """, + LanguageId.XML); + + List completions = editor.getCompletions(); + assertEquals(0, completions.size()); + } + + @Test + void testNoNamespaceCompletionOutsideOfMainElement5() throws Exception { + Editor editor = new Editor(harness, """ + + + + + <<*>/bean> + + """, + LanguageId.XML); + + List completions = editor.getCompletions(); + assertEquals(0, completions.size()); + } + + @Test + void testNoNamespaceCompletionOutsideOfMainElement6() throws Exception { + Editor editor = new Editor(harness, """ + + + + + <<*>/bean> + + """, + LanguageId.XML); + + List completions = editor.getCompletions(); + assertEquals(0, completions.size()); + } + + @Test + void testNoNamespaceCompletionOutsideOfMainElement7() throws Exception { + Editor editor = new Editor(harness, """ + + + + + an> + + """, + LanguageId.XML); + + List completions = editor.getCompletions(); + assertEquals(0, completions.size()); + } + + @Test + void testNoNamespaceCompletionOutsideOfMainElement8() throws Exception { + Editor editor = new Editor(harness, """ + + + + + + > + """, + LanguageId.XML); + + List completions = editor.getCompletions(); + assertEquals(0, completions.size()); + } + + private CompletionItem getContextCompletionItem(List completions) { + for (CompletionItem completionItem : completions) { + if (completionItem.getLabel().contains("/schema/context")) { + return completionItem; + } + } + + return null; + } + }