+ 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..5dd7a73cb0ce
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/api/LanguageFeatures.java
@@ -0,0 +1,184 @@
+/*
+ * 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.spi.friend.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 class 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..783344b89f3a
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/configuration/BracesMatcherFactoryImpl.java
@@ -0,0 +1,246 @@
+/*
+ * 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.spi.friend.LanguageConfiguration;
+import org.netbeans.modules.lsp.client.spi.friend.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..9f5fd4376899
--- /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.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/ToggleCommentActionImpl.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/configuration/ToggleCommentActionImpl.java
new file mode 100644
index 000000000000..bd54b279fc9e
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/configuration/ToggleCommentActionImpl.java
@@ -0,0 +1,220 @@
+/*
+ * 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.awt.event.ActionEvent;
+import java.util.Map;
+import static javax.swing.Action.SHORT_DESCRIPTION;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.Caret;
+import javax.swing.text.JTextComponent;
+import org.netbeans.api.editor.NavigationHistory;
+import org.netbeans.api.editor.caret.CaretInfo;
+import org.netbeans.api.editor.caret.EditorCaret;
+import org.netbeans.api.editor.document.LineDocumentUtils;
+import org.netbeans.api.editor.mimelookup.MimeLookup;
+import org.netbeans.editor.BaseAction;
+import org.netbeans.editor.BaseDocument;
+import org.netbeans.editor.Utilities;
+import org.netbeans.editor.ext.ExtKit.ToggleCommentAction;
+import static org.netbeans.editor.ext.ExtKit.toggleCommentAction;
+import org.netbeans.lib.editor.util.CharSequenceUtilities;
+import org.netbeans.lib.editor.util.swing.DocumentUtilities;
+import org.netbeans.modules.editor.NbEditorUtilities;
+import org.netbeans.modules.lsp.client.spi.friend.LanguageConfiguration;
+import org.openide.util.NbBundle;
+
+public class ToggleCommentActionImpl extends BaseAction {
+
+ //copied from the ToggleCommentAction:
+ static final long serialVersionUID = -1L;
+
+ public ToggleCommentActionImpl() {
+ super(toggleCommentAction);
+ putValue(SHORT_DESCRIPTION, NbBundle.getMessage(ToggleCommentAction.class, "ToggleCommentAction_shortDescription")); //NOI18N
+
+ putValue(BaseAction.ICON_RESOURCE_PROPERTY, "org/netbeans/modules/editor/resources/comment.png"); // NOI18N
+ }
+
+ public void actionPerformed(ActionEvent evt, JTextComponent target) {
+ Boolean forceComment = (Boolean) getValue("force-uncomment");
+ String mimeType = NbEditorUtilities.getMimeType(target);
+ LanguageConfiguration lc = MimeLookup.getLookup(mimeType).lookup(LanguageConfiguration.class);
+ String lineCommentString = lc != null && lc.comments != null && lc.comments.lineComment != null ? lc.comments.lineComment.comment : null;
+ if (lineCommentString != null) {
+ commentUncomment(evt, target, forceComment, lineCommentString, lineCommentString.length(), lc != null && lc.comments != null && lc.comments.lineComment != null ? lc.comments.lineComment.noIdent : false);
+ } else {
+ target.getToolkit().beep();
+ }
+ }
+
+ private void commentUncomment(ActionEvent evt, final JTextComponent target, final Boolean forceComment, String lineCommentString, int lineCommentStringLen, boolean noIndent) {
+ if (target != null) {
+ if (!target.isEditable() || !target.isEnabled()) {
+ target.getToolkit().beep();
+ return;
+ }
+ final Caret caret = target.getCaret();
+ final BaseDocument doc = (BaseDocument)target.getDocument();
+ doc.runAtomicAsUser (new Runnable () {
+ public void run () {
+ if(caret instanceof EditorCaret) {
+ EditorCaret editorCaret = (EditorCaret) caret;
+ boolean beeped = false;
+ for (CaretInfo caretInfo : editorCaret.getSortedCarets()) {
+ try {
+ int startPos;
+ int endPos;
+
+ if (caretInfo.isSelectionShowing()) {
+ int start = Math.min(caretInfo.getDot(), caretInfo.getMark());
+ int end = Math.max(caretInfo.getDot(), caretInfo.getMark());
+ startPos = LineDocumentUtils.getLineStartOffset(doc, start);
+ endPos = end;
+ if (endPos > 0 && LineDocumentUtils.getLineStartOffset(doc, endPos) == endPos) {
+ endPos--;
+ }
+ endPos = LineDocumentUtils.getLineEndOffset(doc, endPos);
+ } else { // selection not visible
+ startPos = LineDocumentUtils.getLineStartOffset(doc, caretInfo.getDot());
+ endPos = LineDocumentUtils.getLineEndOffset(doc, caretInfo.getDot());
+ }
+
+ int lineCount = LineDocumentUtils.getLineCount(doc, startPos, endPos);
+ boolean comment = forceComment != null ? forceComment : !allComments(doc, startPos, lineCount, lineCommentString, lineCommentStringLen);
+
+ if (comment) {
+ comment(doc, startPos, lineCount, lineCommentString, noIndent);
+ } else {
+ uncomment(doc, startPos, lineCount, lineCommentString, lineCommentStringLen);
+ }
+ // TODO:
+// NavigationHistory.getEdits().markWaypoint(target, startPos, false, true);
+ } catch (BadLocationException e) {
+ if(!beeped) {
+ target.getToolkit().beep();
+ beeped = true;
+ }
+ }
+ }
+ } else {
+ try {
+ int startPos;
+ int endPos;
+
+ if (Utilities.isSelectionShowing(caret)) {
+ startPos = LineDocumentUtils.getLineStartOffset(doc, target.getSelectionStart());
+ endPos = target.getSelectionEnd();
+ if (endPos > 0 && LineDocumentUtils.getLineStartOffset(doc, endPos) == endPos) {
+ endPos--;
+ }
+ endPos = LineDocumentUtils.getLineEndOffset(doc, endPos);
+ } else { // selection not visible
+ startPos = LineDocumentUtils.getLineStartOffset(doc, caret.getDot());
+ endPos = LineDocumentUtils.getLineEndOffset(doc, caret.getDot());
+ }
+
+ int lineCount = LineDocumentUtils.getLineCount(doc, startPos, endPos);
+ boolean comment = forceComment != null ? forceComment : !allComments(doc, startPos, lineCount, lineCommentString, lineCommentStringLen);
+
+ if (comment) {
+ comment(doc, startPos, lineCount, lineCommentString, noIndent);
+ } else {
+ uncomment(doc, startPos, lineCount, lineCommentString, lineCommentStringLen);
+ }
+ NavigationHistory.getEdits().markWaypoint(target, startPos, false, true);
+ } catch (BadLocationException e) {
+ target.getToolkit().beep();
+ }
+ }
+ }
+ });
+ }
+ }
+
+ private boolean allComments(BaseDocument doc, int startOffset, int lineCount, String lineCommentString, int lineCommentStringLen) throws BadLocationException {
+ for (int offset = startOffset; lineCount > 0; lineCount--) {
+ int firstNonWhitePos = LineDocumentUtils.getLineFirstNonWhitespace(doc, offset);
+ if (firstNonWhitePos == -1) {
+ return false;
+ }
+
+ if (LineDocumentUtils.getLineEndOffset(doc, firstNonWhitePos) - firstNonWhitePos < lineCommentStringLen) {
+ return false;
+ }
+
+ CharSequence maybeLineComment = DocumentUtilities.getText(doc, firstNonWhitePos, lineCommentStringLen);
+ if (!CharSequenceUtilities.textEquals(maybeLineComment, lineCommentString)) {
+ return false;
+ }
+
+ offset = Utilities.getRowStart(doc, offset, +1);
+ }
+ return true;
+ }
+
+ private int commonIndent(BaseDocument doc, int startOffset, int lineCount) throws BadLocationException {
+ int indent = Integer.MAX_VALUE;
+ for (int offset = startOffset; lineCount > 0 && indent > 0; lineCount--) {
+ int firstNonWhiteSpace = LineDocumentUtils.getLineFirstNonWhitespace(doc, offset);
+
+ if (firstNonWhiteSpace == (-1)) {
+ indent = 0;
+ break;
+ }
+
+ indent = Math.min(indent, firstNonWhiteSpace - offset);
+
+ offset = Utilities.getRowStart(doc, offset, +1);
+ }
+ return indent;
+ }
+
+ private void comment(BaseDocument doc, int startOffset, int lineCount, String lineCommentString, boolean noIndent) throws BadLocationException {
+ int indent = noIndent ? 0 : commonIndent(doc, startOffset, lineCount); //TODO: reflect noIndent
+ for (int offset = startOffset; lineCount > 0; lineCount--) {
+ doc.insertString(offset + indent, lineCommentString + " ", null); // NOI18N
+ offset = Utilities.getRowStart(doc, offset, +1);
+ }
+ }
+
+ private void uncomment(BaseDocument doc, int startOffset, int lineCount, String lineCommentString, int lineCommentStringLen) throws BadLocationException {
+ for (int offset = startOffset; lineCount > 0; lineCount--) {
+ // Get the first non-whitespace char on the current line
+ int firstNonWhitePos = LineDocumentUtils.getLineFirstNonWhitespace(doc, offset);
+
+ // If there is any, check wheter it's the line-comment-chars and remove them
+ if (firstNonWhitePos != -1) {
+ int lineContentLen = LineDocumentUtils.getLineEndOffset(doc, firstNonWhitePos) - firstNonWhitePos;
+ if (lineContentLen >= lineCommentStringLen) {
+ CharSequence maybeLineComment = DocumentUtilities.getText(doc, firstNonWhitePos, lineCommentStringLen + (lineContentLen > lineCommentStringLen ? 1 : 0));
+ if (CharSequenceUtilities.textEquals(maybeLineComment.subSequence(0, lineCommentStringLen), lineCommentString)) {
+ int toRemove = lineCommentStringLen;
+ if (maybeLineComment.length() > lineCommentStringLen && maybeLineComment.charAt(maybeLineComment.length() - 1) == ' ') {
+ toRemove++;
+ }
+ doc.remove(firstNonWhitePos, toRemove);
+ }
+ }
+ }
+
+ offset = Utilities.getRowStart(doc, offset, +1);
+ }
+ }
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/configuration/TypingCompletionImpl.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/configuration/TypingCompletionImpl.java
new file mode 100644
index 000000000000..9179193c0ebd
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/bindings/configuration/TypingCompletionImpl.java
@@ -0,0 +1,217 @@
+/*
+ * 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.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.stream.Collectors;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.Position;
+import org.netbeans.api.editor.mimelookup.MimeLookup;
+import org.netbeans.api.editor.mimelookup.MimePath;
+import org.netbeans.api.lexer.Token;
+import org.netbeans.api.lexer.TokenHierarchy;
+import org.netbeans.api.lexer.TokenSequence;
+import org.netbeans.lib.editor.util.swing.DocumentUtilities;
+import org.netbeans.modules.lsp.client.spi.friend.LanguageConfiguration;
+import org.netbeans.modules.lsp.client.spi.friend.LanguageConfiguration.AutoClosingPair;
+import org.netbeans.modules.lsp.client.spi.friend.LanguageConfiguration.CharacterPair;
+import org.netbeans.modules.lsp.client.spi.friend.LanguageConfiguration.SyntaxTokenType;
+import org.netbeans.spi.editor.typinghooks.TypedTextInterceptor;
+
+public class TypingCompletionImpl implements TypedTextInterceptor {
+
+ private static final Object KEY_SKIP_POSITIONS = new Object();
+ private static final int SIMPLE_CHAR_LIMIT = 128;
+ private static final String DEFAULT_AUTOCLOSE_BEFORE = " \n\t";
+ private final AutoClosingPair[][] simpleLastChar2ClosingPair;
+ private final Map lastChar2ClosingPair;
+ private final Map surroundingPairs;
+ private final String autoCloseBefore;
+ private int moveOffset;
+
+ public TypingCompletionImpl(LanguageConfiguration lc) {
+ Map> tempLastChar2ClosingPair = new HashMap<>();
+ if (lc.autoClosingPairs != null) {
+ Arrays.stream(lc.autoClosingPairs)
+ .forEach(p -> tempLastChar2ClosingPair.computeIfAbsent(p.open.charAt(p.open.length() - 1), x -> new ArrayList<>()).add(p));
+ }
+ simpleLastChar2ClosingPair = new AutoClosingPair[SIMPLE_CHAR_LIMIT][];
+ for (Iterator>> it = tempLastChar2ClosingPair.entrySet().iterator(); it.hasNext();) {
+ Entry> e = it.next();
+ char lastChar = e.getKey();
+ if (lastChar < SIMPLE_CHAR_LIMIT) {
+ simpleLastChar2ClosingPair[lastChar] = e.getValue().toArray(AutoClosingPair[]::new);
+ it.remove();
+ }
+ }
+ lastChar2ClosingPair = tempLastChar2ClosingPair.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue().toArray(AutoClosingPair[]::new)));
+ autoCloseBefore = lc.autoCloseBefore != null ? lc.autoCloseBefore : DEFAULT_AUTOCLOSE_BEFORE;
+ surroundingPairs = new HashMap<>();
+ for (CharacterPair characterPair : lc.surroundingPairs) {
+ surroundingPairs.put(characterPair.first, characterPair.second);
+ }
+ }
+
+ @Override
+ public boolean beforeInsert(Context context) throws BadLocationException {
+ return false;
+ }
+
+ @Override
+ public void insert(MutableContext context) throws BadLocationException {
+ List skipPositions = (List) context.getDocument().getProperty(KEY_SKIP_POSITIONS);
+ if (skipPositions != null) {
+ //TODO: there can be very many skip positions, evict at some point?
+ for (Iterator it = skipPositions.iterator(); it.hasNext();) {
+ Position skipPosition = it.next();
+ if (skipPosition.getOffset() == context.getOffset() && context.getDocument().getText(skipPosition.getOffset(), 1).equals(context.getText())) {
+ context.setText("", 0);
+ moveOffset = 1;
+ it.remove();
+ return ;
+ }
+ }
+ }
+
+ if (!context.getReplacedText().isEmpty()) {
+ String close = surroundingPairs.get(context.getText());
+
+ if (close != null) {
+ String newText = context.getText() + context.getReplacedText() + close;
+
+ context.setText(newText, newText.length());
+ return ;
+ }
+ }
+
+ char next = DocumentUtilities.getText(context.getDocument(), context.getOffset(), 1).charAt(0);
+ if (autoCloseBefore.indexOf(next) == (-1)) {
+ return ;
+ }
+ char lastChar = context.getText().charAt(0);
+ AutoClosingPair[] candidates = lastChar < SIMPLE_CHAR_LIMIT ? simpleLastChar2ClosingPair[lastChar]
+ : lastChar2ClosingPair.get(lastChar);
+
+ if (candidates != null) {
+ SyntaxTokenType type = null;
+
+ NEXT: for (AutoClosingPair candidate : candidates) {
+ if (candidate.open.length() > 1) {
+ int prefixLen = candidate.open.length() - 1;
+ if (context.getDocument().getLength() <= prefixLen) {
+ continue;
+ }
+ if (!candidate.open.substring(0, prefixLen).equals(context.getDocument().getText(context.getOffset() - prefixLen, prefixLen))) {
+ continue;
+ }
+ }
+ if (candidate.notIn != null && candidate.notIn.length > 0) {
+ if (type == null) {
+ type = inferSyntaxTokenType(context);
+ }
+ for (SyntaxTokenType notIn : candidate.notIn) {
+ if (type == notIn) {
+ continue NEXT;
+ }
+ }
+ }
+ context.setText(context.getText() + candidate.close, 1);
+ moveOffset = -1;
+ return ;
+ }
+ }
+ }
+
+ private SyntaxTokenType inferSyntaxTokenType(MutableContext context) {
+ TokenSequence> ts = TokenHierarchy.get(context.getDocument()).tokenSequence();
+
+ ts.move(context.getOffset());
+
+ if (ts.moveNext()) {
+ Token> token = ts.token();
+
+ if (token.getProperty("categories") instanceof List> categories) {
+ for (Object cat : categories) {
+ if (cat instanceof String category) {
+ if (detectTypeFromText(category) instanceof SyntaxTokenType detected) {
+ return detected;
+ }
+ }
+ }
+ } else {
+ if (detectTypeFromText(token.id().name().toLowerCase()) instanceof SyntaxTokenType detected) {
+ return detected;
+ }
+ if (detectTypeFromText(token.id().primaryCategory().toLowerCase()) instanceof SyntaxTokenType detected) {
+ return detected;
+ }
+ }
+
+ }
+
+ return SyntaxTokenType.Other;
+ }
+
+ private Object detectTypeFromText(String text) {
+ if (text.contains("string")) {
+ return SyntaxTokenType.String;
+ } else if (text.contains("comment")) {
+ return SyntaxTokenType.Comment;
+ } else if (text.contains("regex")) {
+ return SyntaxTokenType.RegEx;
+ }
+ return null;
+ }
+
+ @Override
+ public void afterInsert(Context context) throws BadLocationException {
+ if (moveOffset == (-1)) {
+ if (context.getText().length() == 2) {
+ Position skipPosition = context.getDocument().createPosition(context.getComponent().getCaretPosition());
+ List skipPositions = (List) context.getDocument().getProperty(KEY_SKIP_POSITIONS);
+ if (skipPositions == null) {
+ skipPositions = new ArrayList<>();
+ }
+ skipPositions.add(skipPosition);
+ context.getDocument().putProperty(KEY_SKIP_POSITIONS, skipPositions);
+ }
+ } else {
+ context.getComponent().setCaretPosition(context.getComponent().getCaretPosition() + moveOffset);
+ }
+ }
+
+ @Override
+ public void cancelled(Context context) {
+ }
+
+ public static final class FactoryImpl implements Factory {
+
+ @Override
+ public TypedTextInterceptor createTypedTextInterceptor(MimePath mimePath) {
+ LanguageConfiguration lc = MimeLookup.getLookup(mimePath).lookup(LanguageConfiguration.class);
+ return lc != null && lc.autoClosingPairs != null && lc.autoClosingPairs.length > 0 ? new TypingCompletionImpl(lc) : null;
+ }
+ }
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/Bundle.properties b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/Bundle.properties
index c5e9b4baa886..3471c4a77c1b 100644
--- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/Bundle.properties
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/Bundle.properties
@@ -43,3 +43,6 @@ text/x-generic-lsp=Language Server Protocol Client (generic)
LanguageDescriptionPanel.debugger.text=Enable &Breakpoints
LanguageDescriptionPanel.languageIdLabel.text=Language Identifier:
LanguageDescriptionPanel.languageId.text=
+LanguageDescriptionPanel.jLabel1.text=Language Configuration JSON:
+LanguageDescriptionPanel.languageConfiguration.text=
+LanguageDescriptionPanel.browseLanguageConfiguration.text=...
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/GenericEditorKit.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/GenericEditorKit.java
new file mode 100644
index 000000000000..c3944f0f60a4
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/GenericEditorKit.java
@@ -0,0 +1,35 @@
+/*
+ * 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.options;
+
+import java.util.Map;
+import org.netbeans.modules.editor.NbEditorKit;
+
+public class GenericEditorKit extends NbEditorKit {
+ private final String mimeType;
+
+ public GenericEditorKit(String mimeType) {
+ this.mimeType = mimeType;
+ }
+
+ @Override
+ public String getContentType() {
+ return mimeType;
+ }
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageDescriptionPanel.form b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageDescriptionPanel.form
index 76e4a5f9c182..183f2d2a515d 100644
--- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageDescriptionPanel.form
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageDescriptionPanel.form
@@ -52,7 +52,7 @@
-
+
@@ -122,7 +122,7 @@
-
+
@@ -226,7 +226,7 @@
-
+
@@ -245,7 +245,7 @@
-
+
@@ -258,7 +258,7 @@
-
+
@@ -277,7 +277,7 @@
-
+
@@ -289,7 +289,7 @@
-
+
@@ -301,7 +301,7 @@
-
+
@@ -314,7 +314,7 @@
-
+
@@ -333,5 +333,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageDescriptionPanel.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageDescriptionPanel.java
index 981b87e40e89..4eea0706e418 100644
--- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageDescriptionPanel.java
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageDescriptionPanel.java
@@ -22,6 +22,7 @@
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
+import java.awt.event.ActionEvent;
import java.io.File;
import java.util.Locale;
import java.util.Set;
@@ -62,6 +63,7 @@ public LanguageDescriptionPanel(LanguageDescription desc, Set usedIds) {
this.id = desc.id;
this.extensions.setText(desc.extensions);
this.syntax.setText(desc.syntaxGrammar);
+ this.languageConfiguration.setText(desc.languageConfiguration);
this.server.setText(desc.languageServer);
this.name.setText(desc.name);
this.icon.setText(desc.icon);
@@ -75,6 +77,7 @@ public LanguageDescription getDescription() {
id,
this.extensions.getText(),
this.syntax.getText(),
+ this.languageConfiguration.getText(),
this.server.getText(),
this.name.getText(),
this.icon.getText(),
@@ -112,6 +115,9 @@ private void initComponents() {
languageIdLabel = new JLabel();
languageId = new JTextField();
filler1 = new Box.Filler(new Dimension(0, 0), new Dimension(0, 0), new Dimension(0, 32767));
+ jLabel1 = new JLabel();
+ languageConfiguration = new JTextField();
+ browseLanguageConfiguration = new JButton();
setLayout(new GridBagLayout());
@@ -119,7 +125,7 @@ private void initComponents() {
Mnemonics.setLocalizedText(nameLabel, NbBundle.getMessage(LanguageDescriptionPanel.class, "LanguageDescriptionPanel.nameLabel.text")); // NOI18N
gridBagConstraints = new GridBagConstraints();
gridBagConstraints.gridx = 0;
- gridBagConstraints.gridy = 5;
+ gridBagConstraints.gridy = 6;
gridBagConstraints.anchor = GridBagConstraints.BASELINE_LEADING;
gridBagConstraints.insets = new Insets(5, 5, 5, 5);
add(nameLabel, gridBagConstraints);
@@ -156,7 +162,7 @@ private void initComponents() {
name.setText(NbBundle.getMessage(LanguageDescriptionPanel.class, "LanguageDescriptionPanel.name.text")); // NOI18N
gridBagConstraints = new GridBagConstraints();
gridBagConstraints.gridx = 1;
- gridBagConstraints.gridy = 5;
+ gridBagConstraints.gridy = 6;
gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;
gridBagConstraints.anchor = GridBagConstraints.BASELINE_LEADING;
gridBagConstraints.weightx = 1.0;
@@ -226,7 +232,7 @@ private void initComponents() {
Mnemonics.setLocalizedText(optionalParams, NbBundle.getMessage(LanguageDescriptionPanel.class, "LanguageDescriptionPanel.optionalParams.text")); // NOI18N
gridBagConstraints = new GridBagConstraints();
gridBagConstraints.gridx = 0;
- gridBagConstraints.gridy = 4;
+ gridBagConstraints.gridy = 5;
gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
gridBagConstraints.insets = new Insets(5, 5, 5, 5);
add(optionalParams, gridBagConstraints);
@@ -235,7 +241,7 @@ private void initComponents() {
Mnemonics.setLocalizedText(iconLabel, NbBundle.getMessage(LanguageDescriptionPanel.class, "LanguageDescriptionPanel.iconLabel.text")); // NOI18N
gridBagConstraints = new GridBagConstraints();
gridBagConstraints.gridx = 0;
- gridBagConstraints.gridy = 6;
+ gridBagConstraints.gridy = 7;
gridBagConstraints.anchor = GridBagConstraints.BASELINE_LEADING;
gridBagConstraints.insets = new Insets(5, 5, 5, 5);
add(iconLabel, gridBagConstraints);
@@ -244,7 +250,7 @@ private void initComponents() {
icon.setText(NbBundle.getMessage(LanguageDescriptionPanel.class, "LanguageDescriptionPanel.icon.text")); // NOI18N
gridBagConstraints = new GridBagConstraints();
gridBagConstraints.gridx = 1;
- gridBagConstraints.gridy = 6;
+ gridBagConstraints.gridy = 7;
gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;
gridBagConstraints.anchor = GridBagConstraints.BASELINE_LEADING;
gridBagConstraints.weightx = 1.0;
@@ -255,7 +261,7 @@ private void initComponents() {
browseIcon.addActionListener(this::browseIconActionPerformed);
gridBagConstraints = new GridBagConstraints();
gridBagConstraints.gridx = 2;
- gridBagConstraints.gridy = 7;
+ gridBagConstraints.gridy = 8;
gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
gridBagConstraints.insets = new Insets(5, 5, 5, 5);
add(browseIcon, gridBagConstraints);
@@ -263,7 +269,7 @@ private void initComponents() {
Mnemonics.setLocalizedText(debugger, NbBundle.getMessage(LanguageDescriptionPanel.class, "LanguageDescriptionPanel.debugger.text")); // NOI18N
gridBagConstraints = new GridBagConstraints();
gridBagConstraints.gridx = 0;
- gridBagConstraints.gridy = 3;
+ gridBagConstraints.gridy = 4;
gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
gridBagConstraints.insets = new Insets(5, 5, 5, 5);
add(debugger, gridBagConstraints);
@@ -271,7 +277,7 @@ private void initComponents() {
Mnemonics.setLocalizedText(languageIdLabel, NbBundle.getMessage(LanguageDescriptionPanel.class, "LanguageDescriptionPanel.languageIdLabel.text")); // NOI18N
gridBagConstraints = new GridBagConstraints();
gridBagConstraints.gridx = 0;
- gridBagConstraints.gridy = 7;
+ gridBagConstraints.gridy = 8;
gridBagConstraints.anchor = GridBagConstraints.BASELINE_LEADING;
gridBagConstraints.insets = new Insets(5, 5, 5, 5);
add(languageIdLabel, gridBagConstraints);
@@ -280,7 +286,7 @@ private void initComponents() {
languageId.setText(NbBundle.getMessage(LanguageDescriptionPanel.class, "LanguageDescriptionPanel.languageId.text")); // NOI18N
gridBagConstraints = new GridBagConstraints();
gridBagConstraints.gridx = 1;
- gridBagConstraints.gridy = 7;
+ gridBagConstraints.gridy = 8;
gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;
gridBagConstraints.anchor = GridBagConstraints.BASELINE_LEADING;
gridBagConstraints.weightx = 1.0;
@@ -291,6 +297,33 @@ private void initComponents() {
gridBagConstraints.gridy = 8;
gridBagConstraints.weighty = 1.0;
add(filler1, gridBagConstraints);
+
+ Mnemonics.setLocalizedText(jLabel1, NbBundle.getMessage(LanguageDescriptionPanel.class, "LanguageDescriptionPanel.jLabel1.text")); // NOI18N
+ gridBagConstraints = new GridBagConstraints();
+ gridBagConstraints.gridx = 0;
+ gridBagConstraints.gridy = 3;
+ gridBagConstraints.anchor = GridBagConstraints.BASELINE_LEADING;
+ gridBagConstraints.insets = new Insets(5, 5, 5, 5);
+ add(jLabel1, gridBagConstraints);
+
+ languageConfiguration.setText(NbBundle.getMessage(LanguageDescriptionPanel.class, "LanguageDescriptionPanel.languageConfiguration.text")); // NOI18N
+ gridBagConstraints = new GridBagConstraints();
+ gridBagConstraints.gridx = 1;
+ gridBagConstraints.gridy = 3;
+ gridBagConstraints.fill = GridBagConstraints.HORIZONTAL;
+ gridBagConstraints.anchor = GridBagConstraints.BASELINE_LEADING;
+ gridBagConstraints.weightx = 1.0;
+ gridBagConstraints.insets = new Insets(5, 5, 5, 5);
+ add(languageConfiguration, gridBagConstraints);
+
+ Mnemonics.setLocalizedText(browseLanguageConfiguration, NbBundle.getMessage(LanguageDescriptionPanel.class, "LanguageDescriptionPanel.browseLanguageConfiguration.text")); // NOI18N
+ browseLanguageConfiguration.addActionListener(this::browseLanguageConfigurationActionPerformed);
+ gridBagConstraints = new GridBagConstraints();
+ gridBagConstraints.gridx = 2;
+ gridBagConstraints.gridy = 3;
+ gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
+ gridBagConstraints.insets = new Insets(5, 5, 5, 5);
+ add(browseLanguageConfiguration, gridBagConstraints);
}// //GEN-END:initComponents
@Messages("DESC_JSONFilter=Grammars (.json, .xml, .tmLanguage)")
@@ -350,12 +383,24 @@ private void browseServerActionPerformed(java.awt.event.ActionEvent evt) {//GEN-
}
}//GEN-LAST:event_browseServerActionPerformed
+ private void browseLanguageConfigurationActionPerformed(ActionEvent evt) {//GEN-FIRST:event_browseLanguageConfigurationActionPerformed
+ JFileChooser chooser = new JFileChooser();
+ chooser.setAcceptAllFileFilterUsed(true);
+ chooser.setSelectedFile(new File(languageConfiguration.getText()));
+ if (chooser.showDialog(null, Bundle.BTN_Select()) == JFileChooser.APPROVE_OPTION) {
+ languageConfiguration.setText(chooser.getSelectedFile().getAbsolutePath());
+ }
+ }//GEN-LAST:event_browseLanguageConfigurationActionPerformed
+
// Variables declaration - do not modify//GEN-BEGIN:variables
+ private JButton browseLanguageConfiguration;
private JCheckBox debugger;
private JTextField extensions;
private Box.Filler filler1;
private JTextField icon;
+ private JLabel jLabel1;
+ private JTextField languageConfiguration;
private JTextField languageId;
private JLabel languageIdLabel;
private JTextField name;
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageServersPanel.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageServersPanel.java
index 983b59fabf50..2685f5b85117 100644
--- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageServersPanel.java
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageServersPanel.java
@@ -40,7 +40,7 @@
final class LanguageServersPanel extends javax.swing.JPanel {
- private static final LanguageDescription PROTOTYPE = new LanguageDescription(null, null, null, null, "MMMMMMMMMMMMMMMMM", null, false, null);
+ private static final LanguageDescription PROTOTYPE = new LanguageDescription(null, null, null, null, null, "MMMMMMMMMMMMMMMMM", null, false, null);
private final LanguageServersOptionsPanelController controller;
private final DefaultListModel languages;
private final Set usedIds;
@@ -244,7 +244,7 @@ public Component getListCellRendererComponent(JList> list, Object value, int i
if (value instanceof LanguageDescription) {
LanguageDescription ld = (LanguageDescription) value;
value = ld.name != null && !ld.name.isEmpty() ? ld.name : Bundle.TXT_Files(ld.extensions);
- icon = ImageUtilities.image2Icon(Utils.loadIcon(ld.icon != null ? FileUtil.toFileObject(new File(ld.icon)) : null, BeanInfo.ICON_COLOR_16x16));
+ icon = ImageUtilities.image2Icon(Utils.loadIcon(ld.icon != null && !ld.icon.isBlank() ? FileUtil.toFileObject(FileUtil.normalizeFile(new File(ld.icon))) : null, BeanInfo.ICON_COLOR_16x16));
}
super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
setIcon(icon);
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageStorage.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageStorage.java
index c48c1c0cbab9..777cf0f489a5 100644
--- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageStorage.java
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageStorage.java
@@ -26,6 +26,7 @@
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
+import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
@@ -33,11 +34,20 @@
import java.util.Set;
import java.util.stream.Collectors;
import javax.swing.event.ChangeEvent;
+import javax.swing.text.EditorKit;
+import javax.swing.text.TextAction;
import org.netbeans.modules.lsp.client.debugger.api.RegisterDAPBreakpoints;
import org.eclipse.tm4e.core.internal.grammar.raw.RawGrammarReader;
import org.eclipse.tm4e.core.registry.IGrammarSource;
import org.netbeans.core.spi.multiview.MultiViewFactory;
+import org.netbeans.modules.lsp.client.api.LanguageFeatures;
+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;
+import org.netbeans.modules.lsp.client.spi.friend.LanguageConfiguration;
import org.netbeans.modules.textmate.lexer.TextmateTokenId;
+import org.netbeans.spi.editor.bracesmatching.BracesMatcherFactory;
+import org.netbeans.spi.editor.typinghooks.TypedTextInterceptor;
import org.netbeans.spi.navigator.NavigatorPanel;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
@@ -165,6 +175,37 @@ static void store(List languages) {
multiViewRegistration.setAttribute("position", 100);
multiViewRegistration.setAttribute("method", "createEditor");
+ FileObject editorKit = FileUtil.createData(FileUtil.getConfigRoot(), "Editors/" + description.mimeType + "/EditorKit.instance");
+ Method createGenericEditorKit = LanguageFeatures.class.getDeclaredMethod("editorKit", FileObject.class);
+ editorKit.setAttribute("methodvalue:instanceCreate", createGenericEditorKit);
+ editorKit.setAttribute("instanceOf", EditorKit.class.getName());
+ editorKit.setAttribute("mimeType", description.mimeType);
+
+ FileObject bracesMatcher = FileUtil.createData(FileUtil.getConfigRoot(), "Editors/" + description.mimeType + "/BracesMatchers/languageConfigurationMatcher.instance");
+ Method createBracesMatcherImpl = LanguageFeatures.class.getDeclaredMethod("braceMatcher");
+ bracesMatcher.setAttribute("methodvalue:instanceCreate", createBracesMatcherImpl);
+ bracesMatcher.setAttribute("instanceOf", BracesMatcherFactory.class.getName());
+
+ FileObject typingCompletion = FileUtil.createData(FileUtil.getConfigRoot(), "Editors/" + description.mimeType + "/typingCompletionImpl.instance");
+ Method createTypingCompletionFactory = LanguageFeatures.class.getDeclaredMethod("typingCompletion");
+ typingCompletion.setAttribute("methodvalue:instanceCreate", createTypingCompletionFactory);
+ typingCompletion.setAttribute("instanceOf", TypedTextInterceptor.Factory.class.getName());
+
+ FileObject toggleCommentAction = FileUtil.createData(FileUtil.getConfigRoot(), "Editors/" + description.mimeType + "/Actions/toggleComment.instance");
+ Method createToggleCommentActionImpl = LanguageFeatures.class.getDeclaredMethod("toggleCommentAction");
+ toggleCommentAction.setAttribute("methodvalue:instanceCreate", createToggleCommentActionImpl);
+ toggleCommentAction.setAttribute("instanceOf", TextAction.class.getName());
+
+ FileObject languageConfiguration = FileUtil.createData(FileUtil.getConfigRoot(), "Editors/" + description.mimeType + "/languageConfiguration.instance");
+ Method createLanguageConfiguration = LanguageFeatures.class.getDeclaredMethod("languageConfiguration", FileObject.class);
+ languageConfiguration.setAttribute("methodvalue:instanceCreate", createLanguageConfiguration);
+ languageConfiguration.setAttribute("instanceOf", org.netbeans.modules.lsp.client.spi.friend.LanguageConfiguration.class.getName());
+ FileObject languageConfigurationSource = FileUtil.toFileObject(Path.of(description.languageConfiguration));
+ try (InputStream in = languageConfigurationSource.getInputStream();
+ OutputStream out = languageConfiguration.getOutputStream()) {
+ in.transferTo(out);
+ }
+
FileObject icon = FileUtil.getConfigFile("Loaders/" + description.mimeType + "/Factories/icon.png");
if (icon != null) {
icon.delete();
@@ -280,6 +321,7 @@ public static class LanguageDescription {
public String id;
public String extensions;
public String syntaxGrammar;
+ public String languageConfiguration;
public String languageServer;
public String name;
public String icon;
@@ -291,6 +333,7 @@ public LanguageDescription() {
this.id = null;
this.extensions = null;
this.syntaxGrammar = null;
+ this.languageConfiguration = null;
this.languageServer = null;
this.name = null;
this.icon = null;
@@ -299,10 +342,11 @@ public LanguageDescription() {
this.languageId = null;
}
- public LanguageDescription(String id, String extensions, String syntaxGrammar, String languageServer, String name, String icon, boolean debugger, String languageId) {
+ public LanguageDescription(String id, String extensions, String syntaxGrammar, String languageConfiguration, String languageServer, String name, String icon, boolean debugger, String languageId) {
this.id = id;
this.extensions = extensions;
this.syntaxGrammar = syntaxGrammar;
+ this.languageConfiguration = languageConfiguration;
this.languageServer = languageServer;
this.name = name;
this.icon = icon;
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/spi/friend/LanguageConfiguration.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/spi/friend/LanguageConfiguration.java
new file mode 100644
index 000000000000..10306b4c8d52
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/spi/friend/LanguageConfiguration.java
@@ -0,0 +1,280 @@
+/*
+ * 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.spi.friend;
+
+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.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.
+ */
+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)
+ .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