+ Basic support for language-configuration.json added.
+
+
+
+
+ Adding LanguageIdProvider
diff --git a/ide/lsp.client/licenseinfo.xml b/ide/lsp.client/licenseinfo.xml
index 63d1496bfb22..8572fb97de65 100644
--- a/ide/lsp.client/licenseinfo.xml
+++ b/ide/lsp.client/licenseinfo.xml
@@ -53,4 +53,9 @@
+
+ test/unit/src/org/netbeans/modules/lsp/client/bindings/configuration/c.tmLanguage.json
+
+ Source: https://github.com/jeff-hykin/cpp-textmate-grammar
+
diff --git a/ide/lsp.client/nbproject/project.properties b/ide/lsp.client/nbproject/project.properties
index ab01bd46f054..d2d337cf901d 100644
--- a/ide/lsp.client/nbproject/project.properties
+++ b/ide/lsp.client/nbproject/project.properties
@@ -18,6 +18,7 @@
javac.release=17
javac.compilerargs=-Xlint -Xlint:-serial
javadoc.arch=${basedir}/arch.xml
+javadoc.apichanges=${basedir}/apichanges.xml
release.external/org.eclipse.lsp4j-0.24.0.jar=modules/ext/org.eclipse.lsp4j-0.24.0.jar
release.external/org.eclipse.lsp4j.generator-0.24.0.jar=modules/ext/org.eclipse.lsp4j.generator-0.24.0.jar
release.external/org.eclipse.lsp4j.jsonrpc-0.24.0.jar=modules/ext/org.eclipse.lsp4j.jsonrpc-0.24.0.jar
@@ -26,4 +27,4 @@ release.external/org.eclipse.lsp4j.jsonrpc.debug-0.24.0.jar=modules/ext/org.ecli
release.external/org.eclipse.xtend.lib-2.32.0.jar=modules/ext/org.eclipse.xtend.lib-2.32.0.jar
release.external/org.eclipse.xtend.lib.macro-2.32.0.jar=modules/ext/org.eclipse.xtend.lib.macro-2.32.0.jar
release.external/org.eclipse.xtext.xbase.lib-2.32.0.jar=modules/ext/org.eclipse.xtext.xbase.lib-2.32.0.jar
-spec.version.base=1.37.0
+spec.version.base=1.38.0
diff --git a/ide/lsp.client/nbproject/project.xml b/ide/lsp.client/nbproject/project.xml
index 49339b124086..e517c06cb839 100644
--- a/ide/lsp.client/nbproject/project.xml
+++ b/ide/lsp.client/nbproject/project.xml
@@ -102,6 +102,14 @@
1.0
+
+ org.netbeans.libs.jackson
+
+
+
+ 2
+
+ org.netbeans.modules.editor
@@ -111,6 +119,15 @@
1.90
+
+ org.netbeans.modules.editor.bracesmatching
+
+
+
+ 0
+ 1.72
+
+ org.netbeans.modules.editor.breadcrumbs
@@ -445,6 +462,10 @@
+
+ org.netbeans.modules.lexer.nbbridge
+
+ org.netbeans.modules.masterfs
@@ -474,6 +495,7 @@
+ org.netbeans.modules.lsp.client.apiorg.netbeans.modules.lsp.client.debugger.apiorg.netbeans.modules.lsp.client.debugger.spiorg.netbeans.modules.lsp.client.spi
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/api/LanguageConfigurationRegistration.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/api/LanguageConfigurationRegistration.java
new file mode 100644
index 000000000000..4f57de61c4f2
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/api/LanguageConfigurationRegistration.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.modules.lsp.client.api;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Target;
+
+/**Register a {@code language-configuration.json} for a given mime type.
+ *
+ * Will also register language features defined in {@link LanguageFeatures}
+ * as appropriate.
+ *
+ * @since 1.38
+ */
+@Repeatable(LanguageConfigurationRegistrations.class)
+@Target({ElementType.PACKAGE, ElementType.TYPE})
+public @interface LanguageConfigurationRegistration {
+ /** The mime-type under which the grammar should be registered.
+ *
+ * @return mime-type
+ */
+ public String mimeType();
+
+ /** The language configuration to register.
+ *
+ * @return language configuration
+ */
+ public String languageConfiguration();
+
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/api/LanguageConfigurationRegistrations.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/api/LanguageConfigurationRegistrations.java
new file mode 100644
index 000000000000..a595cea79399
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/api/LanguageConfigurationRegistrations.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.modules.lsp.client.api;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+/** Register multiple language configurations.
+ *
+ * @since 1.38
+ */
+@Target({ElementType.PACKAGE, ElementType.TYPE})
+public @interface LanguageConfigurationRegistrations {
+
+ /**The language configuration registration to delegate to.
+ *
+ * @return language configurations
+ */
+ public LanguageConfigurationRegistration[] value();
+
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/api/LanguageFeatures.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/api/LanguageFeatures.java
new file mode 100644
index 000000000000..6a0220a0f1e2
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/api/LanguageFeatures.java
@@ -0,0 +1,187 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.modules.lsp.client.api;
+
+import java.io.IOException;
+import org.netbeans.modules.lsp.client.bindings.configuration.BracesMatcherFactoryImpl;
+import org.netbeans.modules.lsp.client.bindings.configuration.ToggleCommentActionImpl;
+import org.netbeans.modules.lsp.client.bindings.configuration.TypingCompletionImpl.FactoryImpl;
+import org.netbeans.modules.lsp.client.options.GenericEditorKit;
+import org.netbeans.modules.lsp.client.bindings.configuration.LanguageConfiguration;
+import org.openide.filesystems.FileObject;
+
+/**
+ * Callbacks that can be put into the layer to enable various editor features
+ * driven by a {@code language-configuration.json} file.
+ *
+ *
Each method is a factory suitable for use as a layer
+ * {@code instanceCreate} target and should be registered under the
+ * {@code Editors/}/...} folder or an appropriate sub-folder.
+ *
+ * @since 1.38
+ */
+//javadoc partially by qwen3.8
+public final class LanguageFeatures {
+ private LanguageFeatures() {}
+
+ /**
+ * Creates a {@code GenericEditorKit} for the given MIME type.
+ *
+ *
Requires the {@code mimeType} attribute (String) on the registration
+ * file. If absent, {@code text/plain} is used as a fallback.
+ *
+ *
Sample registration:
+ * {@snippet language="xml":
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * }
+ *
+ * @param file the registration file, typically provided by the layer infrastructure.
+ * @return a {@link GenericEditorKit} configured for the specified MIME type.
+ */
+ public static Object editorKit(FileObject file) {
+ String mimeType = "text/plain";
+ if (file.getAttribute("mimeType") instanceof String mt) {
+ mimeType = mt;
+ }
+ return new GenericEditorKit(mimeType);
+ }
+
+ /**
+ * Loads and registers a {@code language-configuration.json} for the given
+ * MIME type.
+ *
+ *
The configuration content can be supplied either inline (as file text)
+ * or referenced via a {@code url} attribute (e.g. {@code nbresloc:}).
+ *
+ *
Sample registration:
+ * {@snippet language="xml":
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * }
+ *
+ * @param file the registration file, typically provided by the layer infrastructure.
+ * @return a parsed {@link LanguageConfiguration} instance.
+ * @throws IOException if the configuration content cannot be read or parsed.
+ */
+ public static Object languageConfiguration(FileObject file) throws IOException {
+ return LanguageConfiguration.from(file.asText());
+ }
+
+ /**
+ * Creates a {@code BracesMatcherFactory} that pairs braces according to the
+ * language configuration.
+ *
+ *
Sample registration:
+ * {@snippet language="xml":
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * }
+ *
+ * @return a {@link BracesMatcherFactoryImpl} instance.
+ */
+ public static Object braceMatcher() {
+ return new BracesMatcherFactoryImpl();
+ }
+
+ /**
+ * Creates a {@code TypedTextInterceptor.Factory} that drives typing
+ * completion (e.g. auto-close braces, auto-indent) based on the language
+ * configuration.
+ *
+ *
Sample registration:
+ * {@snippet language="xml":
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * }
+ *
+ * @return a {@link FactoryImpl} instance.
+ */
+ public static Object typingCompletion() {
+ return new FactoryImpl();
+ }
+
+ /**
+ * Creates a {@code TextAction} that toggles comments on the current
+ * selection or line, using the comment delimiters from the language
+ * configuration.
+ *
+ *
Requires that an appropriate {@code EditorKit} (as created by
+ * {@link #editorKit(FileObject)}) is registered for the same MIME type so
+ * that {@code EditorKit#getContentType()} resolves correctly.
+ *
+ *
Sample registration:
+ * {@snippet language="xml":
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * }
+ *
+ * @return a {@link ToggleCommentActionImpl} instance.
+ */
+ public static Object toggleCommentAction() {
+ return new ToggleCommentActionImpl();
+ }
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/configuration/BracesMatcherFactoryImpl.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/configuration/BracesMatcherFactoryImpl.java
new file mode 100644
index 000000000000..e7b7d9365a3e
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/configuration/BracesMatcherFactoryImpl.java
@@ -0,0 +1,245 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.modules.lsp.client.bindings.configuration;
+
+import java.util.Map;
+import java.util.logging.Logger;
+import javax.swing.text.AbstractDocument;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.Document;
+import javax.swing.text.Segment;
+import org.netbeans.api.editor.mimelookup.MimeLookup;
+import org.netbeans.modules.editor.NbEditorUtilities;
+import org.netbeans.modules.lsp.client.bindings.configuration.LanguageConfiguration.CharacterPair;
+import org.netbeans.spi.editor.bracesmatching.BracesMatcher;
+import org.netbeans.spi.editor.bracesmatching.BracesMatcherFactory;
+import org.netbeans.spi.editor.bracesmatching.MatcherContext;
+
+public class BracesMatcherFactoryImpl implements BracesMatcherFactory {
+
+ @Override
+ public BracesMatcher createMatcher(MatcherContext context) {
+ String mimeType = NbEditorUtilities.getMimeType(context.getDocument());
+ LanguageConfiguration lc = MimeLookup.getLookup(mimeType).lookup(LanguageConfiguration.class);
+ if (lc != null && lc.brackets != null) {
+ String[] bracketPairs = new String[2 * lc.brackets.length];
+ int i = 0;
+ for (CharacterPair p : lc.brackets) {
+ bracketPairs[i++] = p.first;
+ bracketPairs[i++] = p.second;
+ }
+ return new StringMatcher(context, -1, -1, bracketPairs);
+ }
+ return null;
+ }
+
+ //an adjusted copy of: ide/editor.bracesmatching/src/org/netbeans/spi/editor/bracesmatching/support/CharacterMatcher.java
+ private static final class StringMatcher implements BracesMatcher {
+
+ private static final Logger LOG = Logger.getLogger(StringMatcher.class.getName());
+
+ private final MatcherContext context;
+ private final String[] matchingPairs;
+ private final int lowerBound;
+ private final int upperBound;
+
+ private int originOffset;
+ private String originalBrace;
+ private String matchingBrace;
+ private boolean backward;
+
+ public StringMatcher(MatcherContext context, int lowerBound, int upperBound, String... matchingPairs) {
+ this.context = context;
+ this.lowerBound = lowerBound == -1 ? Integer.MIN_VALUE : lowerBound;
+ this.upperBound = upperBound == -1 ? Integer.MAX_VALUE : upperBound;
+
+ assert matchingPairs.length % 2 == 0 : "The matchingPairs parameter must contain even number of Strings."; //NOI18N
+ this.matchingPairs = matchingPairs;
+ }
+
+ // -----------------------------------------------------
+ // BracesMatcher implementation
+ // -----------------------------------------------------
+
+ public int [] findOrigin() throws BadLocationException {
+ ((AbstractDocument) context.getDocument()).readLock();
+ try {
+ int result [] = findChar(
+ context.getDocument(),
+ context.getSearchOffset(),
+ context.isSearchingBackward() ?
+ Math.max(context.getLimitOffset(), lowerBound) :
+ Math.min(context.getLimitOffset(), upperBound),
+ matchingPairs
+ );
+
+ if (result != null) {
+ originOffset = result[0];
+ originalBrace = matchingPairs[result[1]];
+ matchingBrace = matchingPairs[result[1] + result[2]];
+ backward = result[2] < 0;
+ return new int [] { originOffset, originOffset + originalBrace.length() };
+ } else {
+ return null;
+ }
+ } finally {
+ ((AbstractDocument) context.getDocument()).readUnlock();
+ }
+ }
+
+ public int [] findMatches() throws BadLocationException {
+ ((AbstractDocument) context.getDocument()).readLock();
+ try {
+ int offset = matchChar(
+ context.getDocument(),
+ backward ? originOffset : originOffset + 1,
+ backward ?
+ Math.max(0, lowerBound) :
+ Math.min(context.getDocument().getLength(), upperBound),
+ originalBrace,
+ matchingBrace
+ );
+
+ return offset != -1 ? new int [] { offset, offset + matchingBrace.length() } : null;
+ } finally {
+ ((AbstractDocument) context.getDocument()).readUnlock();
+ }
+ }
+ }
+
+ //copied and adjusted from: ide/editor.bracesmatching/src/org/netbeans/spi/editor/bracesmatching/support/BracesMatcherSupport.java
+ public static int [] findChar(Document document, int offset, int limit, String... pairs) throws BadLocationException {
+ assert pairs.length % 2 == 0 : "The pairs parameter must contain even number of String."; //NOI18N
+
+ boolean backward = limit < offset;
+ int lookahead = backward ? offset - limit : limit - offset;
+ int [] result = new int [3];
+
+ if (backward) {
+ // check the character at the left from the caret
+ Segment text = new Segment();
+ document.getText(offset - lookahead, lookahead, text);
+
+ for(int i = lookahead - 1; i >= 0; i--) {
+ if (MatcherContext.isTaskCanceled()) {
+ return null;
+ }
+ if (detectOrigin(result, text.array, text.offset + i, pairs)) {
+ result[0] = offset - (lookahead - i);
+ return result;
+ }
+ }
+ } else {
+ // check the character at the right from the caret
+ Segment text = new Segment();
+ document.getText(offset, lookahead, text);
+
+ for(int i = 0 ; i < lookahead; i++) {
+ if (MatcherContext.isTaskCanceled()) {
+ return null;
+ }
+ if (detectOrigin(result, text.array, text.offset + i, pairs)) {
+ result[0] = offset + i;
+ return result;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ public static int matchChar(Document document, int offset, int limit, String origin, String matching) throws BadLocationException {
+ boolean backward = limit < offset;
+ int lookahead = backward ? offset - limit : limit - offset;
+
+ if (backward) {
+ // check the character at the left from the caret
+ Segment text = new Segment();
+ document.getText(offset - lookahead, lookahead, text);
+
+ int count = 0;
+ for(int i = lookahead - 1; i >= 0; i--) {
+ if (MatcherContext.isTaskCanceled()) {
+ return -1;
+ }
+ if (matches(text.array, text.offset + i, origin)) {
+ count++;
+ } else if (matches(text.array, text.offset + i, matching)) {
+ if (count == 0) {
+ return offset - (lookahead - i);
+ } else {
+ count--;
+ }
+ }
+ }
+ } else {
+ // check the character at the right from the caret
+ Segment text = new Segment();
+ document.getText(offset, lookahead, text);
+
+ int count = 0;
+ for(int i = 0 ; i < lookahead; i++) {
+ if (MatcherContext.isTaskCanceled()) {
+ return -1;
+ }
+ if (matches(text.array, text.offset + i, origin)) {
+ count++;
+ } else if (matches(text.array, text.offset + i, matching)) {
+ if (count == 0) {
+ return offset + i;
+ } else {
+ count--;
+ }
+ }
+ }
+ }
+
+ return -1;
+ }
+
+ private static boolean detectOrigin(int [] results, char[] text, int startOffset, String... pairs) {
+ int cnt = pairs.length / 2;
+
+ for(int idx = 0; idx < 2; idx++) {
+ NEXT_CANDIDATE: for(int i = 0; i < cnt; i++) {
+ int i2 = 2 * i + idx;
+
+ if (matches(text, startOffset, pairs[i2])) {
+ results[1] = i2;
+ results[2] = idx == 0 ? 1 : -1;
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ private static boolean matches(char[] text, int startOffset, String bracket) {
+ //TODO: don't run out of text!
+ //TODO: cancellation here as well?
+ for (int bracketOffset = 0; bracketOffset < bracket.length(); bracketOffset++) {
+ if (text[startOffset + bracketOffset] != bracket.charAt(bracketOffset)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/configuration/CreateRegistrationProcessor.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/configuration/CreateRegistrationProcessor.java
new file mode 100644
index 000000000000..b88a20a386b3
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/configuration/CreateRegistrationProcessor.java
@@ -0,0 +1,237 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.netbeans.modules.lsp.client.bindings.configuration;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map.Entry;
+import java.util.Set;
+import javax.annotation.processing.Completion;
+import javax.annotation.processing.Processor;
+import javax.annotation.processing.RoundEnvironment;
+import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.element.AnnotationValue;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.element.Name;
+import javax.lang.model.element.TypeElement;
+import javax.swing.text.EditorKit;
+import javax.swing.text.TextAction;
+import org.netbeans.spi.editor.bracesmatching.BracesMatcherFactory;
+import org.netbeans.spi.editor.typinghooks.TypedTextInterceptor;
+import org.openide.filesystems.annotations.LayerBuilder;
+import org.openide.filesystems.annotations.LayerGeneratingProcessor;
+import org.openide.filesystems.annotations.LayerGenerationException;
+import org.openide.util.Exceptions;
+import org.openide.util.Lookup;
+import org.openide.util.NbCollections;
+import org.openide.util.lookup.ServiceProvider;
+
+/**Based on CreateRegistrationProcessor from ide/textmate.lexer.
+ */
+@SupportedAnnotationTypes({"org.netbeans.modules.lsp.client.api.LanguageConfigurationRegistration",
+ "org.netbeans.modules.lsp.client.api.LanguageConfigurationRegistrations"})
+@ServiceProvider(service = Processor.class)
+public class CreateRegistrationProcessor extends LayerGeneratingProcessor {
+
+ @Override
+ protected boolean handleProcess(Set extends TypeElement> annotations, RoundEnvironment roundEnv) throws LayerGenerationException {
+ TypeElement languageConfigurationRegistration = processingEnv.getElementUtils().getTypeElement("org.netbeans.modules.lsp.client.api.LanguageConfigurationRegistration");
+
+ for (Element el : roundEnv.getElementsAnnotatedWith(languageConfigurationRegistration)) {
+ for (AnnotationMirror am : el.getAnnotationMirrors()) {
+ if (!languageConfigurationRegistration.equals(am.getAnnotationType().asElement())) {
+ continue;
+ }
+
+ process(el, am);
+ }
+ }
+
+ TypeElement languageConfigurationRegistrations = processingEnv.getElementUtils().getTypeElement("org.netbeans.modules.lsp.client.api.LanguageConfigurationRegistrations");
+
+ for (Element el : roundEnv.getElementsAnnotatedWith(languageConfigurationRegistrations)) {
+ for (AnnotationMirror am : el.getAnnotationMirrors()) {
+ if (!languageConfigurationRegistrations.equals(am.getAnnotationType().asElement())) {
+ continue;
+ }
+
+ for (Entry extends ExecutableElement, ? extends AnnotationValue> e : am.getElementValues().entrySet()) {
+ if (!e.getKey().getSimpleName().contentEquals("value")) continue;
+
+ for (AnnotationMirror r : NbCollections.iterable(NbCollections.checkedIteratorByFilter(((Iterable) e.getValue().getValue()).iterator(), AnnotationMirror.class, true))) {
+ process(el, r);
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
+ private void process(Element toRegister, AnnotationMirror mimeRegistration) throws LayerGenerationException {
+ String languageConfiguration = null;
+ String mimeType = null;
+ for (Entry extends ExecutableElement, ? extends AnnotationValue> e : mimeRegistration.getElementValues().entrySet()) {
+ Name simpleName = e.getKey().getSimpleName();
+ if (simpleName.contentEquals("languageConfiguration")) {
+ languageConfiguration = LayerBuilder.absolutizeResource(toRegister, (String) e.getValue().getValue());
+ continue;
+ }
+ if (simpleName.contentEquals("mimeType")) {
+ mimeType = (String) e.getValue().getValue();
+ continue;
+ }
+ }
+
+ if (mimeType != null && languageConfiguration != null) {
+ LayerBuilder layer = layer(toRegister);
+ layer.file("Editors/" + mimeType + "/EditorKit.instance")
+ .methodvalue("instanceCreate", "org.netbeans.modules.lsp.client.api.LanguageFeatures", "editorKit")
+ .stringvalue("instanceOf", EditorKit.class.getName())
+ .stringvalue("mimeType", mimeType)
+ .write();
+ layer.file("Editors/" + mimeType + "/LanguageConfiguration/languageConfiguration.instance")
+ .url("nbresloc:/" + languageConfiguration)
+ .methodvalue("instanceCreate", "org.netbeans.modules.lsp.client.api.LanguageFeatures", "languageConfiguration")
+ .write();
+ layer.file("Editors/" + mimeType + "/BracesMatchers/languageConfigurationMatcher.instance")
+ .methodvalue("instanceCreate", "org.netbeans.modules.lsp.client.api.LanguageFeatures", "braceMatcher")
+ .stringvalue("instanceOf", BracesMatcherFactory.class.getName())
+ .write();
+ layer.file("Editors/" + mimeType + "/typingCompletion.instance")
+ .methodvalue("instanceCreate", "org.netbeans.modules.lsp.client.api.LanguageFeatures", "typingCompletion")
+ .stringvalue("instanceOf", TypedTextInterceptor.Factory.class.getName())
+ .write();
+ layer.file("Editors/" + mimeType + "/Actions/toggleComment.instance")
+ .methodvalue("instanceCreate", "org.netbeans.modules.lsp.client.api.LanguageFeatures", "toggleCommentAction")
+ .stringvalue("instanceOf", TextAction.class.getName())
+ .write();
+ }
+ }
+
+ @Override
+ public Iterable extends Completion> getCompletions(Element annotated, AnnotationMirror annotation, ExecutableElement attr, String userText) {
+ if (processingEnv == null || annotated == null) {
+ return Collections.emptyList();
+ }
+
+ if ( annotation == null
+ || !"org.netbeans.modules.lsp.client.api.LanguageConfigurationRegistration".contentEquals(((TypeElement) annotation.getAnnotationType().asElement()).getQualifiedName())) {
+ return Collections.emptyList();
+ }
+
+ if ("mimeType".contentEquals(attr.getSimpleName())) { // NOI18N
+ return completeMimePath(annotated, annotation, attr, userText);
+ }
+
+ return Collections.emptyList();
+ }
+
+ private static final String[] DEFAULT_COMPLETIONS = {"text/plain", "text/xml", "text/x-java"}; // NOI18N
+ private Processor COMPLETIONS;
+ private Iterable extends Completion> completeMimePath(
+ Element element, AnnotationMirror annotation,
+ ExecutableElement attr, String userText
+ ) {
+ if (userText == null) {
+ userText = "";
+ }
+ if (userText.startsWith("\"")) {
+ userText = userText.substring(1);
+ }
+
+ Set res = new HashSet();
+ if (COMPLETIONS == null) {
+ String pathCompletions = System.getProperty("org.openide.awt.ActionReference.completion");
+ if (pathCompletions != null) {
+ ClassLoader l = Lookup.getDefault().lookup(ClassLoader.class);
+ if (l == null) {
+ l = Thread.currentThread().getContextClassLoader();
+ }
+ if (l == null) {
+ l = CreateRegistrationProcessor.class.getClassLoader();
+ }
+ try {
+ COMPLETIONS = (Processor)Class.forName(pathCompletions, true, l).getDeclaredConstructor().newInstance();
+ } catch (Exception ex) {
+ Exceptions.printStackTrace(ex);
+ // no completions, OK
+ COMPLETIONS = this;
+ }
+ } else {
+ return res;
+ }
+ }
+ if (COMPLETIONS != null && COMPLETIONS != this) {
+ COMPLETIONS.init(processingEnv);
+ for (Completion completion : COMPLETIONS.getCompletions(element, annotation, attr, "Editors/" + userText)) {
+ String v = completion.getValue();
+ if (v == null) {
+ continue;
+ }
+ String[] arr = v.split("/");
+ if (arr.length > 3 || arr.length < 2) {
+ continue;
+ }
+ if (!arr[0].equals("\"Editors")) {
+ continue;
+ }
+ if (arr[1].length() == 0 || Character.isUpperCase(arr[1].charAt(0))) {
+ // upper case means folders created by @MimeLocation very likelly
+ continue;
+ }
+ if (arr.length > 2) {
+ res.add(new TypeCompletion('"' + arr[1] + '/' + arr[2]));
+ } else {
+ res.add(new TypeCompletion('"' + arr[1] + '/'));
+ }
+ }
+ }
+ if (res.isEmpty()) {
+ for (String c : DEFAULT_COMPLETIONS) {
+ if (c.startsWith(userText)) {
+ res.add(new TypeCompletion("\"" + c));
+ }
+ }
+ }
+
+ return res;
+ }
+
+ private static final class TypeCompletion implements Completion {
+
+ private final String type;
+
+ public TypeCompletion(String type) {
+ this.type = type;
+ }
+
+ public String getValue() {
+ return type;
+ }
+
+ public String getMessage() {
+ return null;
+ }
+
+ }
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/configuration/LanguageConfiguration.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/configuration/LanguageConfiguration.java
new file mode 100644
index 000000000000..91dd5c18728e
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/configuration/LanguageConfiguration.java
@@ -0,0 +1,283 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.modules.lsp.client.bindings.configuration;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.json.JsonReadFeature;
+import com.fasterxml.jackson.databind.json.JsonMapper;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.eclipse.tm4e.core.internal.parser.PropertySettable.ArrayList;
+import org.netbeans.api.annotations.common.NonNull;
+import org.netbeans.api.annotations.common.NullAllowed;
+import org.netbeans.spi.editor.mimelookup.MimeLocation;
+import org.openide.util.Exceptions;
+
+//based on the VS Code API's "LanguageConfiguration" (MIT)
+//TODO: the language-configuration.json is more broad, should be enhanced to support language-configuration.json:
+/**Represents language configuration, potentially coming from language-configuration.json.
+ */
+@MimeLocation(subfolderName="LanguageConfiguration")
+public final class LanguageConfiguration {
+ public final @NullAllowed CommentRule comments;
+ public final @NullAllowed CharacterPair[] brackets;
+ public final @NullAllowed RegExp wordPattern;
+ public final @NullAllowed IndentationRule indentationRules;
+ public final @NullAllowed OnEnterRule[] onEnterRules;
+ public final @NullAllowed AutoClosingPair[] autoClosingPairs;
+ public final @NullAllowed String autoCloseBefore;
+ public final @NullAllowed CharacterPair[] surroundingPairs;
+
+ private LanguageConfiguration(CommentRule comments, CharacterPair[] brackets, RegExp wordPattern, IndentationRule indentationRules, OnEnterRule[] onEnterRules, AutoClosingPair[] autoClosingPairs, String autoCloseBefore, CharacterPair[] surroundingPairs) {
+ this.comments = comments;
+ this.brackets = brackets;
+ this.wordPattern = wordPattern;
+ this.indentationRules = indentationRules;
+ this.onEnterRules = onEnterRules;
+ this.autoClosingPairs = autoClosingPairs;
+ this.autoCloseBefore = autoCloseBefore;
+ this.surroundingPairs = surroundingPairs;
+ }
+
+ public static LanguageConfiguration from(CommentRule comments, CharacterPair[] brackets, RegExp wordPattern, IndentationRule indentationRules, OnEnterRule[] onEnterRules, AutoClosingPair[] autoClosingPairs, String autoCloseBefore, CharacterPair[] surroundingPairs) {
+ return new LanguageConfiguration(comments, brackets, wordPattern, indentationRules, onEnterRules, autoClosingPairs, autoCloseBefore, surroundingPairs);
+ }
+
+ public static final class RegExp {
+ public final @NonNull String regExp;
+
+ public RegExp(String regExp) {
+ this.regExp = regExp;
+ }
+
+ public static RegExp from(String regExp) {
+ return new RegExp(regExp);
+ }
+
+ }
+ public static final class CharacterPair {
+ public final String first;
+ public final String second;
+
+ private CharacterPair(String first, String second) {
+ this.first = first;
+ this.second = second;
+ }
+
+ public static CharacterPair from(String first, String second) {
+ return new CharacterPair(first, second);
+ }
+ }
+
+ public static final class LineComment {
+ public final @NonNull String comment;
+ public final boolean noIdent;
+
+ private LineComment(String comment, boolean noIdent) {
+ this.comment = comment;
+ this.noIdent = noIdent;
+ }
+
+ public static LineComment from(String comment) {
+ return from(comment, false);
+ }
+
+ public static LineComment from(String comment, boolean noIdent) {
+ return new LineComment(comment, noIdent);
+ }
+ }
+
+ public static final class CommentRule {
+ public final @NullAllowed LineComment lineComment;
+ public final @NullAllowed CharacterPair blockComment;
+
+ private CommentRule(LineComment lineComment, CharacterPair blockComment) {
+ this.lineComment = lineComment;
+ this.blockComment = blockComment;
+ }
+
+ public static CommentRule from(LineComment lineComment, CharacterPair blockComment) {
+ return new CommentRule(lineComment, blockComment);
+ }
+ }
+
+ public static final class IndentationRule {
+ public @NullAllowed RegExp decreaseIndentPattern;
+ public @NullAllowed RegExp increaseIndentPattern;
+ public @NullAllowed RegExp indentNextLinePattern;
+ public @NullAllowed RegExp unIndentedLinePattern;
+
+ private IndentationRule(RegExp decreaseIndentPattern, RegExp increaseIndentPattern, RegExp indentNextLinePattern, RegExp unIndentedLinePattern) {
+ this.decreaseIndentPattern = decreaseIndentPattern;
+ this.increaseIndentPattern = increaseIndentPattern;
+ this.indentNextLinePattern = indentNextLinePattern;
+ this.unIndentedLinePattern = unIndentedLinePattern;
+ }
+
+ public static IndentationRule from(RegExp decreaseIndentPattern, RegExp increaseIndentPattern, RegExp indentNextLinePattern, RegExp unIndentedLinePattern) {
+ return new IndentationRule(decreaseIndentPattern, increaseIndentPattern, indentNextLinePattern, unIndentedLinePattern);
+ }
+ }
+
+ public static enum IndentAction {
+ None,
+ Indent,
+ IndentOutdent,
+ Outdent
+ }
+
+ public static final class EnterAction {
+ public final @NonNull IndentAction indentAction;
+ public final @NullAllowed String appendText;
+ public final @NullAllowed Integer removeText;
+
+ private EnterAction(IndentAction indentAction, String appendText, Integer removeText) {
+ this.indentAction = indentAction;
+ this.appendText = appendText;
+ this.removeText = removeText;
+ }
+
+ public static EnterAction from(IndentAction indentAction, String appendText, Integer removeText) {
+ return new EnterAction(indentAction, appendText, removeText);
+ }
+ }
+
+ public static final class OnEnterRule {
+ public final @NonNull RegExp beforeText;
+ public final @NullAllowed RegExp afterText;
+ public final @NullAllowed RegExp previousLineText;
+ public final @NonNull EnterAction action;
+
+ private OnEnterRule(RegExp beforeText, RegExp afterText, RegExp previousLineText, EnterAction action) {
+ this.beforeText = beforeText;
+ this.afterText = afterText;
+ this.previousLineText = previousLineText;
+ this.action = action;
+ }
+
+ public static OnEnterRule from(RegExp beforeText, RegExp afterText, RegExp previousLineText, EnterAction action) {
+ return new OnEnterRule(beforeText, afterText, previousLineText, action);
+ }
+ }
+
+ public static enum SyntaxTokenType {
+ Other,
+ Comment,
+ String,
+ RegEx
+ }
+
+ public static final class AutoClosingPair {
+ public @NonNull String open;
+ public @NonNull String close;
+ public @NullAllowed SyntaxTokenType[] notIn;
+
+ private AutoClosingPair(String open, String close, SyntaxTokenType[] notIn) {
+ this.open = open;
+ this.close = close;
+ this.notIn = notIn;
+ }
+
+ public static AutoClosingPair from(String open, String close, SyntaxTokenType[] notIn) {
+ return new AutoClosingPair(open, close, notIn);
+ }
+ }
+
+ //the language-configuration.json often does not fully follow the JSON spec,
+ //be lenient in parsing:
+ private static final JsonMapper MAPPER = JsonMapper.builder()
+ .enable(JsonReadFeature.ALLOW_JAVA_COMMENTS)
+ .enable(JsonReadFeature.ALLOW_TRAILING_COMMA)
+ .enable(JsonReadFeature.ALLOW_SINGLE_QUOTES)
+ .build();
+ public static LanguageConfiguration from(String source) {
+ //TODO resilience against "incorrect" config:
+ Map config;
+ try {
+ config = MAPPER.readValue(source, HashMap.class);
+ } catch (JsonProcessingException ex) {
+ Exceptions.printStackTrace(ex);
+ config = Map.of();
+ }
+ CommentRule comments = null;
+ Map commentsConfig = (Map) config.get("comments");
+ if (commentsConfig != null) {
+ LineComment lineComment = null;
+ Object lineCommentConfig = commentsConfig.get("lineComment");
+ if (lineCommentConfig instanceof String comment) {
+ lineComment = LineComment.from(comment);
+ } else if (lineCommentConfig instanceof Map vals) {
+ lineComment = LineComment.from((String) vals.get("comment"), (boolean) vals.getOrDefault("noIndent", false));
+ }
+ List commentsBlockConfig = (List) commentsConfig.get("blockComment");
+ CharacterPair blockComment = null;
+
+ if (commentsBlockConfig != null) {
+ blockComment = CharacterPair.from(commentsBlockConfig.get(0), commentsBlockConfig.get(1));
+ }
+ comments = CommentRule.from(lineComment, blockComment);
+ }
+ CharacterPair[] braces = gatherPairs(config.get("brackets"));
+ AutoClosingPair[] autoClosingPairs = null;
+ List