diff --git a/cpplite/cpplite.editor/licenseinfo.xml b/cpplite/cpplite.editor/licenseinfo.xml index bcf2b8a9cd58..c26273537f69 100644 --- a/cpplite/cpplite.editor/licenseinfo.xml +++ b/cpplite/cpplite.editor/licenseinfo.xml @@ -28,6 +28,11 @@ Source: https://github.com/jeff-hykin/cpp-textmate-grammar + + src/org/netbeans/modules/cpplite/editor/file/resources/language-configuration.json + + Source: https://github.com/microsoft/vscode/blob/main/extensions/cpp/language-configuration.json + src/org/netbeans/modules/cpplite/editor/file/resources/CCSrcIcon.gif src/org/netbeans/modules/cpplite/editor/file/resources/HDataIcon.gif diff --git a/cpplite/cpplite.editor/src/org/netbeans/modules/cpplite/editor/file/CDataObject.java b/cpplite/cpplite.editor/src/org/netbeans/modules/cpplite/editor/file/CDataObject.java index 3f142f3b4d37..f4ba710c2ea7 100644 --- a/cpplite/cpplite.editor/src/org/netbeans/modules/cpplite/editor/file/CDataObject.java +++ b/cpplite/cpplite.editor/src/org/netbeans/modules/cpplite/editor/file/CDataObject.java @@ -22,6 +22,7 @@ import org.netbeans.api.annotations.common.StaticResource; import org.netbeans.core.spi.multiview.MultiViewElement; import org.netbeans.core.spi.multiview.text.MultiViewEditorElement; +import org.netbeans.modules.lsp.client.api.LanguageConfigurationRegistration; import org.netbeans.modules.textmate.lexer.api.GrammarRegistration; import org.openide.awt.ActionID; import org.openide.awt.ActionReference; @@ -115,6 +116,7 @@ ) }) @GrammarRegistration(grammar="resources/c.tmLanguage.json", mimeType=MIMETypes.C) +@LanguageConfigurationRegistration(languageConfiguration="resources/language-configuration.json", mimeType=MIMETypes.C) public class CDataObject extends MultiDataObject { @StaticResource diff --git a/cpplite/cpplite.editor/src/org/netbeans/modules/cpplite/editor/file/CPPDataObject.java b/cpplite/cpplite.editor/src/org/netbeans/modules/cpplite/editor/file/CPPDataObject.java index e8421bfafe8a..66370b1f33ab 100644 --- a/cpplite/cpplite.editor/src/org/netbeans/modules/cpplite/editor/file/CPPDataObject.java +++ b/cpplite/cpplite.editor/src/org/netbeans/modules/cpplite/editor/file/CPPDataObject.java @@ -22,6 +22,7 @@ import org.netbeans.api.annotations.common.StaticResource; import org.netbeans.core.spi.multiview.MultiViewElement; import org.netbeans.core.spi.multiview.text.MultiViewEditorElement; +import org.netbeans.modules.lsp.client.api.LanguageConfigurationRegistration; import org.netbeans.modules.textmate.lexer.api.GrammarRegistration; import org.openide.awt.ActionID; import org.openide.awt.ActionReference; @@ -115,6 +116,7 @@ ) }) @GrammarRegistration(grammar="resources/cpp.tmLanguage.json", mimeType=MIMETypes.CPP) +@LanguageConfigurationRegistration(languageConfiguration="resources/language-configuration.json", mimeType=MIMETypes.CPP) public class CPPDataObject extends MultiDataObject { @StaticResource diff --git a/cpplite/cpplite.editor/src/org/netbeans/modules/cpplite/editor/file/HDataObject.java b/cpplite/cpplite.editor/src/org/netbeans/modules/cpplite/editor/file/HDataObject.java index 92c78a2c1c2b..4dc58cf62dd3 100644 --- a/cpplite/cpplite.editor/src/org/netbeans/modules/cpplite/editor/file/HDataObject.java +++ b/cpplite/cpplite.editor/src/org/netbeans/modules/cpplite/editor/file/HDataObject.java @@ -22,6 +22,7 @@ import org.netbeans.api.annotations.common.StaticResource; import org.netbeans.core.spi.multiview.MultiViewElement; import org.netbeans.core.spi.multiview.text.MultiViewEditorElement; +import org.netbeans.modules.lsp.client.api.LanguageConfigurationRegistration; import org.netbeans.modules.textmate.lexer.api.GrammarRegistration; import org.openide.awt.ActionID; import org.openide.awt.ActionReference; @@ -115,6 +116,7 @@ ) }) @GrammarRegistration(grammar="resources/c.tmLanguage.json", mimeType=MIMETypes.H) +@LanguageConfigurationRegistration(languageConfiguration="resources/language-configuration.json", mimeType=MIMETypes.H) public class HDataObject extends MultiDataObject { @StaticResource diff --git a/cpplite/cpplite.editor/src/org/netbeans/modules/cpplite/editor/file/HPPDataObject.java b/cpplite/cpplite.editor/src/org/netbeans/modules/cpplite/editor/file/HPPDataObject.java index eaa069555227..13d61eabc39d 100644 --- a/cpplite/cpplite.editor/src/org/netbeans/modules/cpplite/editor/file/HPPDataObject.java +++ b/cpplite/cpplite.editor/src/org/netbeans/modules/cpplite/editor/file/HPPDataObject.java @@ -22,6 +22,7 @@ import org.netbeans.api.annotations.common.StaticResource; import org.netbeans.core.spi.multiview.MultiViewElement; import org.netbeans.core.spi.multiview.text.MultiViewEditorElement; +import org.netbeans.modules.lsp.client.api.LanguageConfigurationRegistration; import org.netbeans.modules.textmate.lexer.api.GrammarRegistration; import org.openide.awt.ActionID; import org.openide.awt.ActionReference; @@ -115,6 +116,7 @@ ) }) @GrammarRegistration(grammar="resources/cpp.tmLanguage.json", mimeType=MIMETypes.HPP) +@LanguageConfigurationRegistration(languageConfiguration="resources/language-configuration.json", mimeType=MIMETypes.HPP) public class HPPDataObject extends MultiDataObject { @StaticResource diff --git a/cpplite/cpplite.editor/src/org/netbeans/modules/cpplite/editor/file/resources/language-configuration.json b/cpplite/cpplite.editor/src/org/netbeans/modules/cpplite/editor/file/resources/language-configuration.json new file mode 100644 index 000000000000..aa2817220b7f --- /dev/null +++ b/cpplite/cpplite.editor/src/org/netbeans/modules/cpplite/editor/file/resources/language-configuration.json @@ -0,0 +1,128 @@ +{ + "comments": { + "lineComment": "//", + "blockComment": [ + "/*", + "*/" + ] + }, + "brackets": [ + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ] + ], + "autoClosingPairs": [ + { + "open": "[", + "close": "]" + }, + { + "open": "{", + "close": "}" + }, + { + "open": "(", + "close": ")" + }, + { + "open": "'", + "close": "'", + "notIn": [ + "string", + "comment" + ] + }, + { + "open": "\"", + "close": "\"", + "notIn": [ + "string" + ] + }, + { + "open": "/*", + "close": "*/", + "notIn": [ + "string", + "comment" + ] + }, + { + "open": "/**", + "close": " */", + "notIn": [ + "string" + ] + } + ], + "surroundingPairs": [ + [ + "{", + "}" + ], + [ + "[", + "]" + ], + [ + "(", + ")" + ], + [ + "\"", + "\"" + ], + [ + "'", + "'" + ], + [ + "<", + ">" + ] + ], + "wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\#\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\,\\.\\<\\>\\/\\?\\s]+)", + "folding": { + "markers": { + "start": "^\\s*#\\s*pragma\\s+region\\b", + "end": "^\\s*#\\s*pragma\\s+endregion\\b" + } + }, + "indentationRules": { + "decreaseIndentPattern": { + "pattern": "^\\s*[\\}\\]\\)].*$" + }, + "increaseIndentPattern": { + "pattern": "^.*(\\{[^}]*|\\([^)]*|\\[[^\\]]*)$" + }, + }, + "onEnterRules": [ + { + // Decrease indentation after single line if/else if/else, for, or while + "previousLineText": "^\\s*(((else ?)?if|for|while)\\s*\\(.*\\)\\s*|else\\s*)$", + // But make sure line doesn't have braces or is not another if statement + "beforeText": "^\\s+([^{i\\s]|i(?!f\\b))", + "action": { + "indent": "outdent" + } + }, + // Add // when pressing enter from inside line comment + { + "beforeText": "^\\s*//\\s*\\S|\\s//\\s+\\S", + "afterText": "^(?!\\s*$)", + "action": { + "indent": "none", + "appendText": "// " + } + }, + ] +} diff --git a/enterprise/libs.jackson/build.xml b/ide/libs.jackson/build.xml similarity index 93% rename from enterprise/libs.jackson/build.xml rename to ide/libs.jackson/build.xml index 7a058d87d5a8..cdc9f2052326 100644 --- a/enterprise/libs.jackson/build.xml +++ b/ide/libs.jackson/build.xml @@ -19,6 +19,6 @@ under the License. --> - + diff --git a/enterprise/libs.jackson/external/binaries-list b/ide/libs.jackson/external/binaries-list similarity index 100% rename from enterprise/libs.jackson/external/binaries-list rename to ide/libs.jackson/external/binaries-list diff --git a/enterprise/libs.jackson/external/jackson-2.17.2-license.txt b/ide/libs.jackson/external/jackson-2.17.2-license.txt similarity index 100% rename from enterprise/libs.jackson/external/jackson-2.17.2-license.txt rename to ide/libs.jackson/external/jackson-2.17.2-license.txt diff --git a/enterprise/libs.jackson/manifest.mf b/ide/libs.jackson/manifest.mf similarity index 100% rename from enterprise/libs.jackson/manifest.mf rename to ide/libs.jackson/manifest.mf diff --git a/enterprise/libs.jackson/nbproject/org-netbeans-libs-jackson.sig b/ide/libs.jackson/nbproject/org-netbeans-libs-jackson.sig similarity index 100% rename from enterprise/libs.jackson/nbproject/org-netbeans-libs-jackson.sig rename to ide/libs.jackson/nbproject/org-netbeans-libs-jackson.sig diff --git a/enterprise/libs.jackson/nbproject/project.properties b/ide/libs.jackson/nbproject/project.properties similarity index 100% rename from enterprise/libs.jackson/nbproject/project.properties rename to ide/libs.jackson/nbproject/project.properties diff --git a/enterprise/libs.jackson/nbproject/project.xml b/ide/libs.jackson/nbproject/project.xml similarity index 100% rename from enterprise/libs.jackson/nbproject/project.xml rename to ide/libs.jackson/nbproject/project.xml diff --git a/enterprise/libs.jackson/src/org/netbeans/libs/jackson/Bundle.properties b/ide/libs.jackson/src/org/netbeans/libs/jackson/Bundle.properties similarity index 100% rename from enterprise/libs.jackson/src/org/netbeans/libs/jackson/Bundle.properties rename to ide/libs.jackson/src/org/netbeans/libs/jackson/Bundle.properties diff --git a/ide/lsp.client/apichanges.xml b/ide/lsp.client/apichanges.xml index b92e84280a35..f3359fd6b208 100644 --- a/ide/lsp.client/apichanges.xml +++ b/ide/lsp.client/apichanges.xml @@ -19,12 +19,27 @@ under the License. --> - + LSP Client API + + + Adding LanguageIdProvider + + + + + +

+ 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.api org.netbeans.modules.lsp.client.debugger.api org.netbeans.modules.lsp.client.debugger.spi org.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 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 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 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 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 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 autoClosingPairsConfig = (List) config.get("autoClosingPairs"); + if (autoClosingPairsConfig != null) { + List autoClosingPairsList = new ArrayList<>(); + for (Object conf : autoClosingPairsConfig) { + if (conf instanceof Map map) { + SyntaxTokenType[] notIn = null; + if (map.get("notIn") instanceof List notInList) { + notIn = notInList.stream().map(key -> switch ((String) key) { + case "string" -> SyntaxTokenType.String; + case "comment" -> SyntaxTokenType.Comment; + case "regex" -> SyntaxTokenType.RegEx; + default -> SyntaxTokenType.Other; + }).toArray(SyntaxTokenType[]::new); + } + autoClosingPairsList.add(new AutoClosingPair((String) map.get("open"), (String) map.get("close"), notIn)); + } else if (conf instanceof List pair && pair.size() == 2) { + autoClosingPairsList.add(new AutoClosingPair((String) pair.get(0), (String) pair.get(1), null)); + } + } + autoClosingPairs = autoClosingPairsList.toArray(AutoClosingPair[]::new); + } + String autoCloseBefore = null; + if (config.get("autoCloseBefore") instanceof String autoCloseBeforeValue) { + autoCloseBefore = autoCloseBeforeValue; + } + CharacterPair[] surroundingPairs = gatherPairs(config.get("surroundingPairs")); + return LanguageConfiguration.from(comments, braces, null, null, null, autoClosingPairs, autoCloseBefore, surroundingPairs); + } + + private static CharacterPair[] gatherPairs(Object pairsConfigObj) { + CharacterPair[] result = null; + + if (pairsConfigObj instanceof List pairsConfig) { + List bracesList = new ArrayList<>(); + for (Object conf : pairsConfig) { + if (conf instanceof List pair) { + bracesList.add(new CharacterPair((String) pair.get(0), (String) pair.get(1))); + } + } + result = bracesList.toArray(CharacterPair[]::new); + } + + return result; + } +} diff --git a/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/bindings/configuration/BracesMatcherFactoryImplTest.java b/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/bindings/configuration/BracesMatcherFactoryImplTest.java new file mode 100644 index 000000000000..c4de179b3b1a --- /dev/null +++ b/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/bindings/configuration/BracesMatcherFactoryImplTest.java @@ -0,0 +1,182 @@ +/* + * 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.regex.Pattern; +import javax.swing.JEditorPane; +import javax.swing.text.Caret; +import javax.swing.text.JTextComponent; +import org.junit.Test; +import org.netbeans.api.editor.mimelookup.MimePath; +import org.netbeans.modules.editor.NbEditorDocument; +import org.netbeans.modules.lsp.client.spi.friend.LanguageConfiguration; +import org.netbeans.spi.editor.mimelookup.MimeDataProvider; +import org.openide.util.Lookup; +import org.openide.util.lookup.Lookups; +import org.openide.util.lookup.ProxyLookup; +import org.openide.util.lookup.ServiceProvider; +import static org.junit.Assert.*; +import org.netbeans.modules.editor.bracesmatching.MasterMatcher; +import org.netbeans.modules.editor.bracesmatching.SpiAccessor; +import org.netbeans.spi.editor.bracesmatching.BracesMatcher; + +public class BracesMatcherFactoryImplTest { + + @Test + public void testBraceMatcher() throws Exception { + doTest(""" + foo1 %{% + ^}^| + foo3 + """, + LanguageConfiguration.from("{ 'brackets': [ [ '{', '}' ] ] }"), + true); + doTest(""" + foo1 %{% + ^|}^ + foo3 + """, + LanguageConfiguration.from("{ 'brackets': [ [ '{', '}' ] ] }"), + false); + doTest(""" + foo1 ^{|^ + %}% + foo3 + """, + LanguageConfiguration.from("{ 'brackets': [ [ '{', '}' ] ] }"), + true); + doTest(""" + foo1 ^|{^ + %}% + foo3 + """, + LanguageConfiguration.from("{ 'brackets': [ [ '{', '}' ] ] }"), + false); + doTest(""" + foo1 %{{% + ^}}^| + foo3 + """, + LanguageConfiguration.from("{ 'brackets': [ [ '{{', '}}' ] ] }"), + true); + doTest(""" + foo1 %{{% + ^|}}^ + foo3 + """, + LanguageConfiguration.from("{ 'brackets': [ [ '{{', '}}' ] ] }"), + false); + doTest(""" + foo1 ^{{|^ + %}}% + foo3 + """, + LanguageConfiguration.from("{ 'brackets': [ [ '{{', '}}' ] ] }"), + true); + doTest(""" + foo1 ^|{{^ + %}}% + foo3 + """, + LanguageConfiguration.from("{ 'brackets': [ [ '{{', '}}' ] ] }"), + false); + doTest(""" + foo1 %{{% + ^}|}^ + foo3 + """, + LanguageConfiguration.from("{ 'brackets': [ [ '{{', '}}' ] ] }"), + true); //we'll find the bracket only backwards - is that OK? + doTest(""" + foo1 %begin% + ^end^| + foo3 + """, + LanguageConfiguration.from("{ 'brackets': [ [ 'begin', 'end' ] ] }"), + true); + doTest(""" + foo1 %begin% + |^end^ + foo3 + """, + LanguageConfiguration.from("{ 'brackets': [ [ 'begin', 'end' ] ] }"), + false); + doTest(""" + foo1 ^begin^| + %end% + foo3 + """, + LanguageConfiguration.from("{ 'brackets': [ [ 'begin', 'end' ] ] }"), + true); + doTest(""" + foo1 |^begin^ + %end% + foo3 + """, + LanguageConfiguration.from("{ 'brackets': [ [ 'begin', 'end' ] ] }"), + false); + } + + private void doTest(String code, LanguageConfiguration config, boolean backwards) throws Exception { + NbEditorDocument doc = new NbEditorDocument("text/x-test"); + JTextComponent c = new JEditorPane(); + c.setDocument(doc); + int caret = code.replace("%", "").replace("^", "").indexOf('|'); + assertTrue(caret != (-1)); + int[] origin = detectSpan(code.replace("|", "").replace("%", ""), '^'); + int[] matching = detectSpan(code.replace("|", "").replace("^", ""), '%'); + doc.insertString(0, code.replace("|", "").replace("^", "").replace("%", ""), null); + LanguageConfigurationDataProvider.setLanguageConfiguration(config); + MasterMatcher.markTestThread(); //TODO: could be done only once + BracesMatcher matcher = new BracesMatcherFactoryImpl().createMatcher(SpiAccessor.get().createCaretContext(doc, caret, backwards, backwards ? caret : doc.getLength() - caret)); + assertArrayEquals(origin, matcher.findOrigin()); + assertArrayEquals(matching, matcher.findMatches()); + } + + private int[] detectSpan(String input, char mark) { + String[] parts = input.split(Pattern.quote("" + mark)); + assertEquals(3, parts.length); + return new int[] {parts[0].length(), parts[0].length() + parts[1].length()}; + } + + @ServiceProvider(service=MimeDataProvider.class) + public static final class LanguageConfigurationDataProvider implements MimeDataProvider { + + private static final LiveLookup LOOKUP = new LiveLookup(); + + @Override + public Lookup getLookup(MimePath mimePath) { + if ("text/x-test".equals(mimePath.getPath())) { + return LOOKUP; + } + + return null; + } + + public static void setLanguageConfiguration(LanguageConfiguration config) { + LOOKUP.setLookupsInt(Lookups.fixed(config)); + } + + private static final class LiveLookup extends ProxyLookup { + public void setLookupsInt(Lookup... lookups) { + super.setLookups(lookups); + } + } + } +} diff --git a/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/bindings/configuration/ToggleCommentActionImplTest.java b/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/bindings/configuration/ToggleCommentActionImplTest.java new file mode 100644 index 000000000000..b318751eaa1b --- /dev/null +++ b/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/bindings/configuration/ToggleCommentActionImplTest.java @@ -0,0 +1,222 @@ +/* + * 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 javax.swing.JEditorPane; +import javax.swing.text.Caret; +import javax.swing.text.JTextComponent; +import org.junit.Test; +import org.netbeans.api.editor.mimelookup.MimePath; +import org.netbeans.modules.editor.NbEditorDocument; +import org.netbeans.modules.lsp.client.spi.friend.LanguageConfiguration; +import org.netbeans.spi.editor.mimelookup.MimeDataProvider; +import org.openide.util.Lookup; +import org.openide.util.lookup.Lookups; +import org.openide.util.lookup.ProxyLookup; +import org.openide.util.lookup.ServiceProvider; +import static org.junit.Assert.*; + +public class ToggleCommentActionImplTest { + + @Test + public void testLineComment() throws Exception { + doTest(""" + foo1 + foo|2 + foo3 + """, + LanguageConfiguration.from("{ 'comments': { 'lineComment': '//' } }"), + """ + foo1 + // foo2 + foo3 + """); + doTest(""" + foo1 + | + foo3 + """, + LanguageConfiguration.from("{ 'comments': { 'lineComment': '//' } }"), + """ + foo1 + //\s + foo3 + """); + doTest(""" + foo1 + // foo|2 + foo3 + """, + LanguageConfiguration.from("{ 'comments': { 'lineComment': '//' } }"), + """ + foo1 + foo2 + foo3 + """); + doTest(""" + foo1 + //foo|2 + foo3 + """, + LanguageConfiguration.from("{ 'comments': { 'lineComment': '//' } }"), + """ + foo1 + foo2 + foo3 + """); + doTest(""" + foo1 + //| + foo3 + """, + LanguageConfiguration.from("{ 'comments': { 'lineComment': '//' } }"), + """ + foo1 + + foo3 + """); + doTest(""" + foo1 + foo|2 + foo|3 + """, + LanguageConfiguration.from("{ 'comments': { 'lineComment': '//' } }"), + """ + foo1 + // foo2 + // foo3 + """); + doTest(""" + foo1 + // f|oo2 + // f|oo3 + """, + LanguageConfiguration.from("{ 'comments': { 'lineComment': '//' } }"), + """ + foo1 + foo2 + foo3 + """); + doTest(""" + foo1 + foo|2 + foo|3 + """, + LanguageConfiguration.from("{ 'comments': { 'lineComment': { 'comment': '//', 'noIndent': false } } }"), + """ + foo1 + // foo2 + // foo3 + """); + doTest(""" + foo1 + foo|2 + foo|3 + """, + LanguageConfiguration.from("{ 'comments': { 'lineComment': { 'comment': '//', 'noIndent': true } } }"), + """ + foo1 + // foo2 + // foo3 + """); + doTest(""" + foo1 + // fo|o2 + // foo|3 + """, + LanguageConfiguration.from("{ 'comments': { 'lineComment': { 'comment': '//', 'noIndent': true } } }"), + """ + foo1 + foo2 + foo3 + """); + doTest(""" + foo1 + // fo|o2 + // foo|3 + """, + LanguageConfiguration.from("{ 'comments': { 'lineComment': { 'comment': '//', 'noIndent': false } } }"), + """ + foo1 + foo2 + foo3 + """); + } + + //TODO: block comment + + private void doTest(String code, LanguageConfiguration config, String expected) throws Exception { + //TODO: could be nice to be also test mark > caret. + NbEditorDocument doc = new NbEditorDocument("text/x-test"); + JTextComponent c = new JEditorPane(); + c.setDocument(doc); + String[] parts = code.split("\\|"); + int caret; + int mark; + if (parts.length == 1) { + fail("No caret position!"); + throw new AssertionError(); + } else if (parts.length == 2) { + caret = mark = parts[0].length(); + code = parts[0] + parts[1]; + } else if (parts.length == 3) { + mark = parts[0].length(); + caret = parts[0].length() + parts[1].length(); + code = parts[0] + parts[1] + parts[2]; + } else { + fail("Unknow caret pattern!"); + throw new AssertionError(); + } + assertTrue(caret != (-1)); + doc.insertString(0, code, null); + Caret caretObj = c.getCaret(); + caretObj.setDot(mark); + caretObj.moveDot(caret); + caretObj.setSelectionVisible(true); //TODO: should be able to control true/false in the test? + LanguageConfigurationDataProvider.setLanguageConfiguration(config); + new ToggleCommentActionImpl().actionPerformed(null, c); + String actual = doc.getText(0, doc.getLength()); + assertEquals(expected, actual); + } + + @ServiceProvider(service=MimeDataProvider.class) + public static final class LanguageConfigurationDataProvider implements MimeDataProvider { + + private static final LiveLookup LOOKUP = new LiveLookup(); + + @Override + public Lookup getLookup(MimePath mimePath) { + if ("text/x-test".equals(mimePath.getPath())) { + return LOOKUP; + } + + return null; + } + + public static void setLanguageConfiguration(LanguageConfiguration config) { + LOOKUP.setLookupsInt(Lookups.fixed(config)); + } + + private static final class LiveLookup extends ProxyLookup { + public void setLookupsInt(Lookup... lookups) { + super.setLookups(lookups); + } + } + } +} diff --git a/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/bindings/configuration/TypingCompletionImplTest.java b/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/bindings/configuration/TypingCompletionImplTest.java new file mode 100644 index 000000000000..a0cb88ef2bd4 --- /dev/null +++ b/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/bindings/configuration/TypingCompletionImplTest.java @@ -0,0 +1,214 @@ +/* + * 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.io.InputStream; +import java.io.OutputStream; +import javax.swing.JEditorPane; +import javax.swing.text.Caret; +import javax.swing.text.JTextComponent; +import org.junit.Test; +import org.netbeans.modules.editor.NbEditorDocument; +import org.netbeans.modules.lsp.client.spi.friend.LanguageConfiguration; +import org.netbeans.modules.editor.lib2.typinghooks.TypingHooksSpiAccessor; +import org.netbeans.modules.textmate.lexer.TextmateTokenId; +import org.netbeans.spi.editor.typinghooks.TypedTextInterceptor.MutableContext; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; + +import static org.junit.Assert.*; + +public class TypingCompletionImplTest { + +// @Test + public void testBracketCompletion() throws Exception { + doTest(""" + | + """, + LanguageConfiguration.from("{ 'autoClosingPairs': [ { 'open': '(', 'close': ')' } ] }"), + new Input('(', + """ + (|) + """), + new Input(')', + """ + ()| + """)); + doTest(""" + a| + """, + LanguageConfiguration.from("{ 'autoClosingPairs': [ { 'open': '(', 'close': ')' } ] }"), + new Input('(', + """ + a(|) + """), + new Input(')', + """ + a()| + """)); + doTest(""" + ' } ] }"), + new Input('-', + """ + + """)); + doTest(""" + !-| + """, + LanguageConfiguration.from("{ 'autoClosingPairs': [ { 'open': '' } ] }"), + new Input('-', + """ + !--| + """)); + doTest(""" + | + """, + LanguageConfiguration.from("{ 'autoClosingPairs': [ { 'open': 'č', 'close': 'ž' } ] }"), + new Input('č', + """ + č|ž + """), + new Input('ž', + """ + čž| + """)); + doTest(""" + | + """, + LanguageConfiguration.from("{ 'autoClosingPairs': [ { 'open': '(', 'close': ')' } ] }"), + new Input('č', + """ + č| + """)); + FileObject systemConfigRoot = FileUtil.getSystemConfigRoot(); + FileObject grammar = FileUtil.createData(systemConfigRoot, "Editors/text/x-test/grammar.json"); + grammar.setAttribute("textmate-grammar", "source.c"); + try (InputStream is = TypingCompletionImplTest.class.getResourceAsStream("c.tmLanguage.json"); + OutputStream os = grammar.getOutputStream()) { + FileUtil.copy(is, os); + } + TextmateTokenId.LanguageHierarchyImpl.refreshGrammars(); + doTest(""" + "|" + """, + LanguageConfiguration.from("{ 'autoClosingPairs': [ { 'open': '(', 'close': ')', 'notIn': [ 'string' ] } ] }"), + new Input('(', + """ + "(|" + """)); + doTest(""" + | + """, + LanguageConfiguration.from("{ 'autoClosingPairs': [ [ '(', ')' ] ] }"), + new Input('(', + """ + (|) + """), + new Input(')', + """ + ()| + """)); + doTest(""" + |> + """, + LanguageConfiguration.from("{ 'autoClosingPairs': [ { 'open': '(', 'close': ')' } ] }"), + new Input('(', + """ + (|> + """)); + doTest(""" + |> + """, + LanguageConfiguration.from("{ 'autoClosingPairs': [ { 'open': '(', 'close': ')' } ], 'autoCloseBefore': '>' }"), + new Input('(', + """ + (|)> + """)); + } + + @Test + public void testAutoSurrounding() throws Exception { + doTest(""" + ^hello| + """, + LanguageConfiguration.from("{ 'surroundingPairs': [ [ '(', ')' ] ] }"), + new Input('(', + """ + (hello)| + """)); + } + + private void doTest(String code, LanguageConfiguration config, Input... inputs) throws Exception { + NbEditorDocument doc = new NbEditorDocument("text/x-test"); + JTextComponent c = new JEditorPane(); + c.setDocument(doc); + ParsedCode parsedCode = detectSpan(code); + doc.insertString(0, parsedCode.code(), null); + Caret caretObj = c.getCaret(); + caretObj.setDot(parsedCode.mark()); + caretObj.moveDot(parsedCode.caret()); + TypingCompletionImpl typing = new TypingCompletionImpl(config); + for (Input input : inputs) { + int first = Math.min(caretObj.getMark(), caretObj.getDot()); + int second = Math.max(caretObj.getMark(), caretObj.getDot()); + String typed = String.valueOf(input.c()); + MutableContext context = TypingHooksSpiAccessor.get().createTtiContext(c, doc.createPosition(first), typed, doc.getText(first, second - first)); + boolean callAfterInsert = false; + if (!typing.beforeInsert(context)) { + doc.remove(first, second - first); + caretObj.setDot(first); + typing.insert(context); + callAfterInsert = true; + } + Object[] data = TypingHooksSpiAccessor.get().getTtiContextData(context); + if (data == null) { + data = new Object[] {typed, 1, false}; + } + doc.insertString(first, (String) data[0], null); + caretObj.setDot(caretObj.getDot() + (int) data[1]); + assertFalse((Boolean) data[2]); + if (callAfterInsert) { + typing.afterInsert(context); + } + String expected = input.expectedOutput(); + String[] expectedParts = expected.split("\\|"); + int expectedCaret = expectedParts[0].length(); + expected = expectedParts[0] + expectedParts[1]; + assertEquals(expectedCaret, caretObj.getDot()); + assertEquals(expected, doc.getText(0, doc.getLength())); + } + } + + private ParsedCode detectSpan(String code) { + int caret = code.replace("^", "").indexOf('|'); + assertTrue("Caret marked incorrectly", caret != (-1)); + int mark = code.replace("|", "").indexOf('^'); + if (mark == (-1)) { + mark = caret; + } + code = code.replace("|", "").replace("^", ""); + return new ParsedCode(code, mark, caret); + } + + private record ParsedCode(String code, int mark, int caret) {} + + private record Input(char c, String expectedOutput) {} +} diff --git a/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/bindings/configuration/c.tmLanguage.json b/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/bindings/configuration/c.tmLanguage.json new file mode 100644 index 000000000000..c753475408c7 --- /dev/null +++ b/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/bindings/configuration/c.tmLanguage.json @@ -0,0 +1,3348 @@ +{ + "scopeName": "source.c", + "fileTypes": [ + "c" + ], + "version": "", + "information_for_contributors": [ + "This code was auto generated by a much-more-readble ruby file", + "This file essentially an updated/improved fork of the atom syntax", + "see https://github.com/jeff-hykin/cpp-textmate-grammar/blob/master" + ], + "patterns": [ + { + "include": "#preprocessor-rule-enabled" + }, + { + "include": "#preprocessor-rule-disabled" + }, + { + "include": "#preprocessor-rule-conditional" + }, + { + "include": "#predefined_macros" + }, + { + "include": "#comments" + }, + { + "include": "#switch_statement" + }, + { + "match": "\\b(break|continue|do|else|for|goto|if|_Pragma|return|while)\\b", + "name": "keyword.control.c" + }, + { + "include": "#storage_types" + }, + { + "match": "typedef", + "name": "keyword.other.typedef.c" + }, + { + "match": "\\b(const|extern|register|restrict|static|volatile|inline)\\b", + "name": "storage.modifier.c" + }, + { + "match": "\\bk[A-Z]\\w*\\b", + "name": "constant.other.variable.mac-classic.c" + }, + { + "match": "\\bg[A-Z]\\w*\\b", + "name": "variable.other.readwrite.global.mac-classic.c" + }, + { + "match": "\\bs[A-Z]\\w*\\b", + "name": "variable.other.readwrite.static.mac-classic.c" + }, + { + "match": "\\b(NULL|true|false|TRUE|FALSE)\\b", + "name": "constant.language.c" + }, + { + "include": "#operators" + }, + { + "include": "#numbers" + }, + { + "include": "#strings" + }, + { + "name": "meta.preprocessor.macro.c", + "begin": "((?:(?:(?>\\s+)|(\\/\\*)((?>(?:[^\\*]|(?>\\*+)[^\\/])*)((?>\\*+)\\/)))+?|(?:(?:(?:(?:\\b|(?<=\\W))|(?=\\W))|\\A)|\\Z)))((#)\\s*define\\b)\\s+((?", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.c" + } + }, + "name": "string.quoted.other.lt-gt.include.c" + } + ] + }, + { + "include": "#pragma-mark" + }, + { + "begin": "^\\s*((#)\\s*line)\\b", + "beginCaptures": { + "1": { + "name": "keyword.control.directive.line.c" + }, + "2": { + "name": "punctuation.definition.directive.c" + } + }, + "end": "(?=(?://|/\\*))|(?))((?:(?:[a-zA-Z_][a-zA-Z_0-9]*)\\s*(?:(?:\\.)|(?:->)))*)\\s*([a-zA-Z_][a-zA-Z_0-9]*)(\\()", + "beginCaptures": { + "1": { + "name": "variable.object.c" + }, + "2": { + "name": "punctuation.separator.dot-access.c" + }, + "3": { + "name": "punctuation.separator.pointer-access.c" + }, + "4": { + "patterns": [ + { + "match": "\\.", + "name": "punctuation.separator.dot-access.c" + }, + { + "match": "->", + "name": "punctuation.separator.pointer-access.c" + }, + { + "match": "[a-zA-Z_][a-zA-Z_0-9]*", + "name": "variable.object.c" + }, + { + "name": "everything.else.c", + "match": ".+" + } + ] + }, + "5": { + "name": "entity.name.function.member.c" + }, + "6": { + "name": "punctuation.section.arguments.begin.bracket.round.function.member.c" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.section.arguments.end.bracket.round.function.member.c" + } + }, + "patterns": [ + { + "include": "#function-call-innards" + } + ] + }, + "backslash_escapes": { + "match": "(?x)\\\\ (\n\\\\\t\t\t |\n[abefnprtv'\"?] |\n[0-3][0-7]{,2}\t |\n[4-7]\\d?\t\t|\nx[a-fA-F0-9]{,2} |\nu[a-fA-F0-9]{,4} |\nU[a-fA-F0-9]{,8} )", + "name": "constant.character.escape.c" + }, + "block": { + "patterns": [ + { + "begin": "{", + "beginCaptures": { + "0": { + "name": "punctuation.section.block.begin.bracket.curly.c" + } + }, + "end": "}|(?=\\s*#\\s*(?:elif|else|endif)\\b)", + "endCaptures": { + "0": { + "name": "punctuation.section.block.end.bracket.curly.c" + } + }, + "name": "meta.block.c", + "patterns": [ + { + "include": "#block_innards" + } + ] + } + ] + }, + "block_innards": { + "patterns": [ + { + "include": "#preprocessor-rule-enabled-block" + }, + { + "include": "#preprocessor-rule-disabled-block" + }, + { + "include": "#preprocessor-rule-conditional-block" + }, + { + "include": "#method_access" + }, + { + "include": "#member_access" + }, + { + "include": "#c_function_call" + }, + { + "name": "meta.initialization.c", + "begin": "(?x)\n(?:\n (?:\n\t(?=\\s)(?=+!]+ | \\(\\) | \\[\\]))\n)\n\\s*(\\() # opening bracket", + "beginCaptures": { + "1": { + "name": "variable.other.c" + }, + "2": { + "name": "punctuation.section.parens.begin.bracket.round.initialization.c" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.section.parens.end.bracket.round.initialization.c" + } + }, + "patterns": [ + { + "include": "#function-call-innards" + } + ] + }, + { + "begin": "{", + "beginCaptures": { + "0": { + "name": "punctuation.section.block.begin.bracket.curly.c" + } + }, + "end": "}|(?=\\s*#\\s*(?:elif|else|endif)\\b)", + "endCaptures": { + "0": { + "name": "punctuation.section.block.end.bracket.curly.c" + } + }, + "patterns": [ + { + "include": "#block_innards" + } + ] + }, + { + "include": "#parens-block" + }, + { + "include": "$base" + } + ] + }, + "c_conditional_context": { + "patterns": [ + { + "include": "$self" + }, + { + "include": "#block_innards" + } + ] + }, + "c_function_call": { + "begin": "(?x)\n(?!(?:while|for|do|if|else|switch|catch|enumerate|return|typeid|alignof|alignas|sizeof|[cr]?iterate|and|and_eq|bitand|bitor|compl|not|not_eq|or|or_eq|typeid|xor|xor_eq|alignof|alignas)\\s*\\()\n(?=\n(?:[A-Za-z_][A-Za-z0-9_]*+|::)++\\s*\\( # actual name\n|\n(?:(?<=operator)(?:[-*&<>=+!]+|\\(\\)|\\[\\]))\\s*\\(\n)", + "end": "(?<=\\))(?!\\w)", + "name": "meta.function-call.c", + "patterns": [ + { + "include": "#function-call-innards" + } + ] + }, + "case_statement": { + "name": "meta.conditional.case.c", + "begin": "((?>(?:(?:(?>(?(?:[^\\*]|(?>\\*+)[^\\/])*)((?>\\*+)\\/)))+|(?:(?:(?:(?:\\b|(?<=\\W))|(?=\\W))|\\A)|\\Z))))((?\\s*)(\\/\\/[!\\/]+)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.comment.documentation.c" + } + }, + "end": "(?<=\\n)(?|%|\"|\\.|=|::|\\||\\-\\-|\\-\\-\\-)\\b(?:\\{[^}]*\\})?", + "name": "storage.type.class.doxygen.c" + }, + { + "match": "((?<=[\\s*!\\/])[\\\\@](?:a|em|e))\\s+(\\S+)", + "captures": { + "1": { + "name": "storage.type.class.doxygen.c" + }, + "2": { + "name": "markup.italic.doxygen.c" + } + } + }, + { + "match": "((?<=[\\s*!\\/])[\\\\@]b)\\s+(\\S+)", + "captures": { + "1": { + "name": "storage.type.class.doxygen.c" + }, + "2": { + "name": "markup.bold.doxygen.c" + } + } + }, + { + "match": "((?<=[\\s*!\\/])[\\\\@](?:c|p))\\s+(\\S+)", + "captures": { + "1": { + "name": "storage.type.class.doxygen.c" + }, + "2": { + "name": "markup.inline.raw.string.c" + } + } + }, + { + "match": "(?<=[\\s*!\\/])[\\\\@](?:a|anchor|b|c|cite|copybrief|copydetail|copydoc|def|dir|dontinclude|e|em|emoji|enum|example|extends|file|idlexcept|implements|include|includedoc|includelineno|latexinclude|link|memberof|namespace|p|package|ref|refitem|related|relates|relatedalso|relatesalso|verbinclude)\\b(?:\\{[^}]*\\})?", + "name": "storage.type.class.doxygen.c" + }, + { + "match": "(?<=[\\s*!\\/])[\\\\@](?:addindex|addtogroup|category|class|defgroup|diafile|dotfile|elseif|fn|headerfile|if|ifnot|image|ingroup|interface|line|mainpage|mscfile|name|overload|page|property|protocol|section|skip|skipline|snippet|snippetdoc|snippetlineno|struct|subpage|subsection|subsubsection|typedef|union|until|vhdlflow|weakgroup)\\b(?:\\{[^}]*\\})?", + "name": "storage.type.class.doxygen.c" + }, + { + "match": "((?<=[\\s*!\\/])[\\\\@]param)(?:\\s*\\[((?:,?\\s*(?:in|out)\\s*)+)\\])?\\s+(\\b\\w+\\b)", + "captures": { + "1": { + "name": "storage.type.class.doxygen.c" + }, + "2": { + "patterns": [ + { + "match": "in|out", + "name": "keyword.other.parameter.direction.$0.c" + } + ] + }, + "3": { + "name": "variable.parameter.c" + } + } + }, + { + "match": "(?<=[\\s*!\\/])[\\\\@](?:arg|attention|author|authors|brief|bug|copyright|date|deprecated|details|exception|invariant|li|note|par|paragraph|param|post|pre|remark|remarks|result|return|returns|retval|sa|see|short|since|test|throw|todo|tparam|version|warning|xrefitem)\\b(?:\\{[^}]*\\})?", + "name": "storage.type.class.doxygen.c" + }, + { + "match": "(?<=[\\s*!\\/])[\\\\@](?:code|cond|docbookonly|dot|htmlonly|internal|latexonly|link|manonly|msc|parblock|rtfonly|secreflist|uml|verbatim|xmlonly|endcode|endcond|enddocbookonly|enddot|endhtmlonly|endinternal|endlatexonly|endlink|endmanonly|endmsc|endparblock|endrtfonly|endsecreflist|enduml|endverbatim|endxmlonly)\\b(?:\\{[^}]*\\})?", + "name": "storage.type.class.doxygen.c" + }, + { + "match": "(?:\\b[A-Z]+:|@[a-z_]+:)", + "name": "storage.type.class.gtkdoc" + } + ] + }, + { + "match": "(\\/\\*[!*]+(?=\\s))(.+)([!*]*\\*\\/)", + "captures": { + "1": { + "name": "punctuation.definition.comment.begin.documentation.c" + }, + "2": { + "patterns": [ + { + "match": "(?<=[\\s*!\\/])[\\\\@](?:callergraph|callgraph|else|endif|f\\$|f\\[|f\\]|hidecallergraph|hidecallgraph|hiderefby|hiderefs|hideinitializer|htmlinclude|n|nosubgrouping|private|privatesection|protected|protectedsection|public|publicsection|pure|showinitializer|showrefby|showrefs|tableofcontents|\\$|\\#|<|>|%|\"|\\.|=|::|\\||\\-\\-|\\-\\-\\-)\\b(?:\\{[^}]*\\})?", + "name": "storage.type.class.doxygen.c" + }, + { + "match": "((?<=[\\s*!\\/])[\\\\@](?:a|em|e))\\s+(\\S+)", + "captures": { + "1": { + "name": "storage.type.class.doxygen.c" + }, + "2": { + "name": "markup.italic.doxygen.c" + } + } + }, + { + "match": "((?<=[\\s*!\\/])[\\\\@]b)\\s+(\\S+)", + "captures": { + "1": { + "name": "storage.type.class.doxygen.c" + }, + "2": { + "name": "markup.bold.doxygen.c" + } + } + }, + { + "match": "((?<=[\\s*!\\/])[\\\\@](?:c|p))\\s+(\\S+)", + "captures": { + "1": { + "name": "storage.type.class.doxygen.c" + }, + "2": { + "name": "markup.inline.raw.string.c" + } + } + }, + { + "match": "(?<=[\\s*!\\/])[\\\\@](?:a|anchor|b|c|cite|copybrief|copydetail|copydoc|def|dir|dontinclude|e|em|emoji|enum|example|extends|file|idlexcept|implements|include|includedoc|includelineno|latexinclude|link|memberof|namespace|p|package|ref|refitem|related|relates|relatedalso|relatesalso|verbinclude)\\b(?:\\{[^}]*\\})?", + "name": "storage.type.class.doxygen.c" + }, + { + "match": "(?<=[\\s*!\\/])[\\\\@](?:addindex|addtogroup|category|class|defgroup|diafile|dotfile|elseif|fn|headerfile|if|ifnot|image|ingroup|interface|line|mainpage|mscfile|name|overload|page|property|protocol|section|skip|skipline|snippet|snippetdoc|snippetlineno|struct|subpage|subsection|subsubsection|typedef|union|until|vhdlflow|weakgroup)\\b(?:\\{[^}]*\\})?", + "name": "storage.type.class.doxygen.c" + }, + { + "match": "((?<=[\\s*!\\/])[\\\\@]param)(?:\\s*\\[((?:,?\\s*(?:in|out)\\s*)+)\\])?\\s+(\\b\\w+\\b)", + "captures": { + "1": { + "name": "storage.type.class.doxygen.c" + }, + "2": { + "patterns": [ + { + "match": "in|out", + "name": "keyword.other.parameter.direction.$0.c" + } + ] + }, + "3": { + "name": "variable.parameter.c" + } + } + }, + { + "match": "(?<=[\\s*!\\/])[\\\\@](?:arg|attention|author|authors|brief|bug|copyright|date|deprecated|details|exception|invariant|li|note|par|paragraph|param|post|pre|remark|remarks|result|return|returns|retval|sa|see|short|since|test|throw|todo|tparam|version|warning|xrefitem)\\b(?:\\{[^}]*\\})?", + "name": "storage.type.class.doxygen.c" + }, + { + "match": "(?<=[\\s*!\\/])[\\\\@](?:code|cond|docbookonly|dot|htmlonly|internal|latexonly|link|manonly|msc|parblock|rtfonly|secreflist|uml|verbatim|xmlonly|endcode|endcond|enddocbookonly|enddot|endhtmlonly|endinternal|endlatexonly|endlink|endmanonly|endmsc|endparblock|endrtfonly|endsecreflist|enduml|endverbatim|endxmlonly)\\b(?:\\{[^}]*\\})?", + "name": "storage.type.class.doxygen.c" + }, + { + "match": "(?:\\b[A-Z]+:|@[a-z_]+:)", + "name": "storage.type.class.gtkdoc" + } + ] + }, + "3": { + "name": "punctuation.definition.comment.end.documentation.c" + } + }, + "name": "comment.block.documentation.c" + }, + { + "name": "comment.block.documentation.c", + "begin": "((?>\\s*)\\/\\*[!*]+(?:(?:\\n|$)|(?=\\s)))", + "beginCaptures": { + "1": { + "name": "punctuation.definition.comment.begin.documentation.c" + } + }, + "end": "([!*]*\\*\\/)", + "endCaptures": { + "1": { + "name": "punctuation.definition.comment.end.documentation.c" + } + }, + "patterns": [ + { + "match": "(?<=[\\s*!\\/])[\\\\@](?:callergraph|callgraph|else|endif|f\\$|f\\[|f\\]|hidecallergraph|hidecallgraph|hiderefby|hiderefs|hideinitializer|htmlinclude|n|nosubgrouping|private|privatesection|protected|protectedsection|public|publicsection|pure|showinitializer|showrefby|showrefs|tableofcontents|\\$|\\#|<|>|%|\"|\\.|=|::|\\||\\-\\-|\\-\\-\\-)\\b(?:\\{[^}]*\\})?", + "name": "storage.type.class.doxygen.c" + }, + { + "match": "((?<=[\\s*!\\/])[\\\\@](?:a|em|e))\\s+(\\S+)", + "captures": { + "1": { + "name": "storage.type.class.doxygen.c" + }, + "2": { + "name": "markup.italic.doxygen.c" + } + } + }, + { + "match": "((?<=[\\s*!\\/])[\\\\@]b)\\s+(\\S+)", + "captures": { + "1": { + "name": "storage.type.class.doxygen.c" + }, + "2": { + "name": "markup.bold.doxygen.c" + } + } + }, + { + "match": "((?<=[\\s*!\\/])[\\\\@](?:c|p))\\s+(\\S+)", + "captures": { + "1": { + "name": "storage.type.class.doxygen.c" + }, + "2": { + "name": "markup.inline.raw.string.c" + } + } + }, + { + "match": "(?<=[\\s*!\\/])[\\\\@](?:a|anchor|b|c|cite|copybrief|copydetail|copydoc|def|dir|dontinclude|e|em|emoji|enum|example|extends|file|idlexcept|implements|include|includedoc|includelineno|latexinclude|link|memberof|namespace|p|package|ref|refitem|related|relates|relatedalso|relatesalso|verbinclude)\\b(?:\\{[^}]*\\})?", + "name": "storage.type.class.doxygen.c" + }, + { + "match": "(?<=[\\s*!\\/])[\\\\@](?:addindex|addtogroup|category|class|defgroup|diafile|dotfile|elseif|fn|headerfile|if|ifnot|image|ingroup|interface|line|mainpage|mscfile|name|overload|page|property|protocol|section|skip|skipline|snippet|snippetdoc|snippetlineno|struct|subpage|subsection|subsubsection|typedef|union|until|vhdlflow|weakgroup)\\b(?:\\{[^}]*\\})?", + "name": "storage.type.class.doxygen.c" + }, + { + "match": "((?<=[\\s*!\\/])[\\\\@]param)(?:\\s*\\[((?:,?\\s*(?:in|out)\\s*)+)\\])?\\s+(\\b\\w+\\b)", + "captures": { + "1": { + "name": "storage.type.class.doxygen.c" + }, + "2": { + "patterns": [ + { + "match": "in|out", + "name": "keyword.other.parameter.direction.$0.c" + } + ] + }, + "3": { + "name": "variable.parameter.c" + } + } + }, + { + "match": "(?<=[\\s*!\\/])[\\\\@](?:arg|attention|author|authors|brief|bug|copyright|date|deprecated|details|exception|invariant|li|note|par|paragraph|param|post|pre|remark|remarks|result|return|returns|retval|sa|see|short|since|test|throw|todo|tparam|version|warning|xrefitem)\\b(?:\\{[^}]*\\})?", + "name": "storage.type.class.doxygen.c" + }, + { + "match": "(?<=[\\s*!\\/])[\\\\@](?:code|cond|docbookonly|dot|htmlonly|internal|latexonly|link|manonly|msc|parblock|rtfonly|secreflist|uml|verbatim|xmlonly|endcode|endcond|enddocbookonly|enddot|endhtmlonly|endinternal|endlatexonly|endlink|endmanonly|endmsc|endparblock|endrtfonly|endsecreflist|enduml|endverbatim|endxmlonly)\\b(?:\\{[^}]*\\})?", + "name": "storage.type.class.doxygen.c" + }, + { + "match": "(?:\\b[A-Z]+:|@[a-z_]+:)", + "name": "storage.type.class.gtkdoc" + } + ] + }, + { + "match": "^\\/\\* =(\\s*.*?)\\s*= \\*\\/$\\n?", + "captures": { + "1": { + "name": "meta.toc-list.banner.block.c" + } + }, + "name": "comment.block.banner.c" + }, + { + "name": "comment.block.c", + "begin": "(\\/\\*)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.comment.begin.c" + } + }, + "end": "(\\*\\/)", + "endCaptures": { + "1": { + "name": "punctuation.definition.comment.end.c" + } + } + }, + { + "match": "^\\/\\/ =(\\s*.*?)\\s*=$\\n?", + "captures": { + "1": { + "name": "meta.toc-list.banner.line.c" + } + }, + "name": "comment.line.banner.c" + }, + { + "begin": "((?:^[ \\t]+)?)(?=\\/\\/)", + "beginCaptures": { + "1": { + "name": "punctuation.whitespace.comment.leading.c" + } + }, + "end": "(?!\\G)", + "patterns": [ + { + "name": "comment.line.double-slash.c", + "begin": "(\\/\\/)", + "beginCaptures": { + "1": { + "name": "punctuation.definition.comment.c" + } + }, + "end": "(?=\\n)", + "patterns": [ + { + "include": "#line_continuation_character" + } + ] + } + ] + } + ] + }, + "default_statement": { + "name": "meta.conditional.case.c", + "begin": "((?>(?:(?:(?>(?(?:[^\\*]|(?>\\*+)[^\\/])*)((?>\\*+)\\/)))+|(?:(?:(?:(?:\\b|(?<=\\W))|(?=\\W))|\\A)|\\Z))))((?=+!]+|\\(\\)|\\[\\]))\n)\n\\s*(\\()", + "beginCaptures": { + "1": { + "name": "entity.name.function.c" + }, + "2": { + "name": "punctuation.section.arguments.begin.bracket.round.c" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.section.arguments.end.bracket.round.c" + } + }, + "patterns": [ + { + "include": "#function-call-innards" + } + ] + }, + { + "begin": "\\(", + "beginCaptures": { + "0": { + "name": "punctuation.section.parens.begin.bracket.round.c" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.section.parens.end.bracket.round.c" + } + }, + "patterns": [ + { + "include": "#function-call-innards" + } + ] + }, + { + "include": "#block_innards" + } + ] + }, + "function-innards": { + "patterns": [ + { + "include": "#comments" + }, + { + "include": "#storage_types" + }, + { + "include": "#operators" + }, + { + "include": "#vararg_ellipses" + }, + { + "name": "meta.function.definition.parameters.c", + "begin": "(?x)\n(?!(?:while|for|do|if|else|switch|catch|enumerate|return|typeid|alignof|alignas|sizeof|[cr]?iterate|and|and_eq|bitand|bitor|compl|not|not_eq|or|or_eq|typeid|xor|xor_eq|alignof|alignas)\\s*\\()\n(\n(?:[A-Za-z_][A-Za-z0-9_]*+|::)++ # actual name\n|\n(?:(?<=operator)(?:[-*&<>=+!]+|\\(\\)|\\[\\]))\n)\n\\s*(\\()", + "beginCaptures": { + "1": { + "name": "entity.name.function.c" + }, + "2": { + "name": "punctuation.section.parameters.begin.bracket.round.c" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.section.parameters.end.bracket.round.c" + } + }, + "patterns": [ + { + "include": "#probably_a_parameter" + }, + { + "include": "#function-innards" + } + ] + }, + { + "begin": "\\(", + "beginCaptures": { + "0": { + "name": "punctuation.section.parens.begin.bracket.round.c" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.section.parens.end.bracket.round.c" + } + }, + "patterns": [ + { + "include": "#function-innards" + } + ] + }, + { + "include": "$base" + } + ] + }, + "inline_comment": { + "match": "(\\/\\*)((?>(?:[^\\*]|(?>\\*+)[^\\/])*)((?>\\*+)\\/))", + "captures": { + "1": { + "name": "comment.block.c punctuation.definition.comment.begin.c" + }, + "2": { + "name": "comment.block.c" + }, + "3": { + "patterns": [ + { + "match": "\\*\\/", + "name": "comment.block.c punctuation.definition.comment.end.c" + }, + { + "match": "\\*", + "name": "comment.block.c" + } + ] + } + } + }, + "line_continuation_character": { + "patterns": [ + { + "match": "(\\\\)\\n", + "captures": { + "1": { + "name": "constant.character.escape.line-continuation.c" + } + } + } + ] + }, + "member_access": { + "match": "((?:[a-zA-Z_]\\w*|(?<=\\]|\\)))\\s*)(?:((?:\\.\\*|\\.))|((?:->\\*|->)))((?:[a-zA-Z_]\\w*\\s*(?:(?:(?:\\.\\*|\\.))|(?:(?:->\\*|->)))\\s*)*)\\s*(\\b(?!(?:atomic_uint_least64_t|atomic_uint_least16_t|atomic_uint_least32_t|atomic_uint_least8_t|atomic_int_least16_t|atomic_uint_fast64_t|atomic_uint_fast32_t|atomic_int_least64_t|atomic_int_least32_t|pthread_rwlockattr_t|atomic_uint_fast16_t|pthread_mutexattr_t|atomic_int_fast16_t|atomic_uint_fast8_t|atomic_int_fast64_t|atomic_int_least8_t|atomic_int_fast32_t|atomic_int_fast8_t|pthread_condattr_t|atomic_uintptr_t|atomic_ptrdiff_t|pthread_rwlock_t|atomic_uintmax_t|pthread_mutex_t|atomic_intmax_t|atomic_intptr_t|atomic_char32_t|atomic_char16_t|pthread_attr_t|atomic_wchar_t|uint_least64_t|uint_least32_t|uint_least16_t|pthread_cond_t|pthread_once_t|uint_fast64_t|uint_fast16_t|atomic_size_t|uint_least8_t|int_least64_t|int_least32_t|int_least16_t|pthread_key_t|atomic_ullong|atomic_ushort|uint_fast32_t|atomic_schar|atomic_short|uint_fast8_t|int_fast64_t|int_fast32_t|int_fast16_t|atomic_ulong|atomic_llong|int_least8_t|atomic_uchar|memory_order|suseconds_t|int_fast8_t|atomic_bool|atomic_char|atomic_uint|atomic_long|atomic_int|useconds_t|_Imaginary|blksize_t|pthread_t|in_addr_t|uintptr_t|in_port_t|uintmax_t|uintmax_t|blkcnt_t|uint16_t|unsigned|_Complex|uint32_t|intptr_t|intmax_t|intmax_t|uint64_t|u_quad_t|int64_t|int32_t|ssize_t|caddr_t|clock_t|uint8_t|u_short|swblk_t|segsz_t|int16_t|fixpt_t|daddr_t|nlink_t|qaddr_t|size_t|time_t|mode_t|signed|quad_t|ushort|u_long|u_char|double|int8_t|ino_t|uid_t|pid_t|_Bool|float|dev_t|div_t|short|gid_t|off_t|u_int|key_t|id_t|uint|long|void|char|bool|id_t|int)\\b)[a-zA-Z_]\\w*\\b(?!\\())", + "captures": { + "1": { + "name": "variable.other.object.access.c" + }, + "2": { + "name": "punctuation.separator.dot-access.c" + }, + "3": { + "name": "punctuation.separator.pointer-access.c" + }, + "4": { + "patterns": [ + { + "include": "#member_access" + }, + { + "include": "#method_access" + }, + { + "match": "((?:[a-zA-Z_]\\w*|(?<=\\]|\\)))\\s*)(?:((?:\\.\\*|\\.))|((?:->\\*|->)))", + "captures": { + "1": { + "name": "variable.other.object.access.c" + }, + "2": { + "name": "punctuation.separator.dot-access.c" + }, + "3": { + "name": "punctuation.separator.pointer-access.c" + } + } + } + ] + }, + "5": { + "name": "variable.other.member.c" + } + } + }, + "method_access": { + "contentName": "meta.function-call.member.c", + "begin": "((?:[a-zA-Z_]\\w*|(?<=\\]|\\)))\\s*)(?:((?:\\.\\*|\\.))|((?:->\\*|->)))((?:[a-zA-Z_]\\w*\\s*(?:(?:(?:\\.\\*|\\.))|(?:(?:->\\*|->)))\\s*)*)\\s*([a-zA-Z_]\\w*)(\\()", + "beginCaptures": { + "1": { + "name": "variable.other.object.access.c" + }, + "2": { + "name": "punctuation.separator.dot-access.c" + }, + "3": { + "name": "punctuation.separator.pointer-access.c" + }, + "4": { + "patterns": [ + { + "include": "#member_access" + }, + { + "include": "#method_access" + }, + { + "match": "((?:[a-zA-Z_]\\w*|(?<=\\]|\\)))\\s*)(?:((?:\\.\\*|\\.))|((?:->\\*|->)))", + "captures": { + "1": { + "name": "variable.other.object.access.c" + }, + "2": { + "name": "punctuation.separator.dot-access.c" + }, + "3": { + "name": "punctuation.separator.pointer-access.c" + } + } + } + ] + }, + "5": { + "name": "entity.name.function.member.c" + }, + "6": { + "name": "punctuation.section.arguments.begin.bracket.round.function.member.c" + } + }, + "end": "(\\))", + "endCaptures": { + "1": { + "name": "punctuation.section.arguments.end.bracket.round.function.member.c" + } + }, + "patterns": [ + { + "include": "#function-call-innards" + } + ] + }, + "numbers": { + "match": "(?>=|\\|=", + "name": "keyword.operator.assignment.compound.bitwise.c" + }, + { + "match": "<<|>>", + "name": "keyword.operator.bitwise.shift.c" + }, + { + "match": "!=|<=|>=|==|<|>", + "name": "keyword.operator.comparison.c" + }, + { + "match": "&&|!|\\|\\|", + "name": "keyword.operator.logical.c" + }, + { + "match": "&|\\||\\^|~", + "name": "keyword.operator.c" + }, + { + "match": "=", + "name": "keyword.operator.assignment.c" + }, + { + "match": "%|\\*|/|-|\\+", + "name": "keyword.operator.c" + }, + { + "begin": "(\\?)", + "beginCaptures": { + "1": { + "name": "keyword.operator.ternary.c" + } + }, + "end": "(:)", + "endCaptures": { + "1": { + "name": "keyword.operator.ternary.c" + } + }, + "patterns": [ + { + "include": "#function-call-innards" + }, + { + "include": "$base" + } + ] + } + ] + }, + "parens": { + "name": "meta.parens.c", + "begin": "\\(", + "beginCaptures": { + "0": { + "name": "punctuation.section.parens.begin.bracket.round.c" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.section.parens.end.bracket.round.c" + } + }, + "patterns": [ + { + "include": "$base" + } + ] + }, + "parens-block": { + "name": "meta.parens.block.c", + "begin": "\\(", + "beginCaptures": { + "0": { + "name": "punctuation.section.parens.begin.bracket.round.c" + } + }, + "end": "\\)", + "endCaptures": { + "0": { + "name": "punctuation.section.parens.end.bracket.round.c" + } + }, + "patterns": [ + { + "include": "#block_innards" + }, + { + "match": "(?-mix:(?=+!]+|\\(\\)|\\[\\]))\\s*\\(\n)", + "end": "(?<=\\))(?!\\w)|(?=+!]+|\\(\\)|\\[\\]))\n)\n\\s*(\\()", + "beginCaptures": { + "1": { + "name": "entity.name.function.c" + }, + "2": { + "name": "punctuation.section.arguments.begin.bracket.round.c" + } + }, + "end": "(\\))|(?\\]\\)]))\\s*([a-zA-Z_]\\w*)\\s*(?=(?:\\[\\]\\s*)?(?:,|\\)))", + "captures": { + "1": { + "name": "variable.parameter.probably.c" + } + } + }, + "static_assert": { + "begin": "((?>(?:(?:(?>(?(?:[^\\*]|(?>\\*+)[^\\/])*)((?>\\*+)\\/)))+|(?:(?:(?:(?:\\b|(?<=\\W))|(?=\\W))|\\A)|\\Z))))((?(?:(?:(?>(?(?:[^\\*]|(?>\\*+)[^\\/])*)((?>\\*+)\\/)))+|(?:(?:(?:(?:\\b|(?<=\\W))|(?=\\W))|\\A)|\\Z))))(\\()", + "beginCaptures": { + "1": { + "patterns": [ + { + "include": "#inline_comment" + } + ] + }, + "2": { + "name": "comment.block.c punctuation.definition.comment.begin.c" + }, + "3": { + "name": "comment.block.c" + }, + "4": { + "patterns": [ + { + "match": "\\*\\/", + "name": "comment.block.c punctuation.definition.comment.end.c" + }, + { + "match": "\\*", + "name": "comment.block.c" + } + ] + }, + "5": { + "name": "keyword.other.static_assert.c" + }, + "6": { + "patterns": [ + { + "include": "#inline_comment" + } + ] + }, + "7": { + "name": "comment.block.c punctuation.definition.comment.begin.c" + }, + "8": { + "name": "comment.block.c" + }, + "9": { + "patterns": [ + { + "match": "\\*\\/", + "name": "comment.block.c punctuation.definition.comment.end.c" + }, + { + "match": "\\*", + "name": "comment.block.c" + } + ] + }, + "10": { + "name": "punctuation.section.arguments.begin.bracket.round.static_assert.c" + } + }, + "end": "(\\))", + "endCaptures": { + "1": { + "name": "punctuation.section.arguments.end.bracket.round.static_assert.c" + } + }, + "patterns": [ + { + "name": "meta.static_assert.message.c", + "begin": "(,)\\s*(?=(?:L|u8|u|U\\s*\\\")?)", + "beginCaptures": { + "1": { + "name": "punctuation.separator.delimiter.comma.c" + } + }, + "end": "(?=\\))", + "patterns": [ + { + "include": "#string_context" + } + ] + }, + { + "include": "#evaluation_context" + } + ] + }, + "storage_types": { + "patterns": [ + { + "match": "(?-mix:(?\\s+)|(\\/\\*)((?>(?:[^\\*]|(?>\\*+)[^\\/])*)((?>\\*+)\\/)))+?|(?:(?:(?:(?:\\b|(?<=\\W))|(?=\\W))|\\A)|\\Z)))(?:\\n|$)", + "captures": { + "1": { + "patterns": [ + { + "include": "#inline_comment" + } + ] + }, + "2": { + "name": "comment.block.c punctuation.definition.comment.begin.c" + }, + "3": { + "name": "comment.block.c" + }, + "4": { + "patterns": [ + { + "match": "\\*\\/", + "name": "comment.block.c punctuation.definition.comment.end.c" + }, + { + "match": "\\*", + "name": "comment.block.c" + } + ] + } + } + }, + { + "include": "#comments_context" + }, + { + "include": "#comments" + }, + { + "begin": "(((?:(?:(?>\\s+)|(\\/\\*)((?>(?:[^\\*]|(?>\\*+)[^\\/])*)((?>\\*+)\\/)))+?|(?:(?:(?:(?:\\b|(?<=\\W))|(?=\\W))|\\A)|\\Z)))\\()", + "beginCaptures": { + "1": { + "name": "punctuation.section.parens.begin.bracket.round.assembly.c" + }, + "2": { + "patterns": [ + { + "include": "#inline_comment" + } + ] + }, + "3": { + "name": "comment.block.c punctuation.definition.comment.begin.c" + }, + "4": { + "name": "comment.block.c" + }, + "5": { + "patterns": [ + { + "match": "\\*\\/", + "name": "comment.block.c punctuation.definition.comment.end.c" + }, + { + "match": "\\*", + "name": "comment.block.c" + } + ] + } + }, + "end": "(\\))", + "endCaptures": { + "1": { + "name": "punctuation.section.parens.end.bracket.round.assembly.c" + } + }, + "patterns": [ + { + "name": "string.quoted.double.c", + "contentName": "meta.embedded.assembly.c", + "begin": "(R?)(\")", + "beginCaptures": { + "1": { + "name": "meta.encoding.c" + }, + "2": { + "name": "punctuation.definition.string.begin.assembly.c" + } + }, + "end": "(\")", + "endCaptures": { + "1": { + "name": "punctuation.definition.string.end.assembly.c" + } + }, + "patterns": [ + { + "include": "source.asm" + }, + { + "include": "source.x86" + }, + { + "include": "source.x86_64" + }, + { + "include": "source.arm" + }, + { + "include": "#backslash_escapes" + }, + { + "include": "#string_escaped_char" + } + ] + }, + { + "begin": "(\\()", + "beginCaptures": { + "1": { + "name": "punctuation.section.parens.begin.bracket.round.assembly.inner.c" + } + }, + "end": "(\\))", + "endCaptures": { + "1": { + "name": "punctuation.section.parens.end.bracket.round.assembly.inner.c" + } + }, + "patterns": [ + { + "include": "#evaluation_context" + } + ] + }, + { + "match": "\\[((?:(?:(?>\\s+)|(\\/\\*)((?>(?:[^\\*]|(?>\\*+)[^\\/])*)((?>\\*+)\\/)))+?|(?:(?:(?:(?:\\b|(?<=\\W))|(?=\\W))|\\A)|\\Z)))([a-zA-Z_]\\w*)((?:(?:(?>\\s+)|(\\/\\*)((?>(?:[^\\*]|(?>\\*+)[^\\/])*)((?>\\*+)\\/)))+?|(?:(?:(?:(?:\\b|(?<=\\W))|(?=\\W))|\\A)|\\Z)))\\]", + "captures": { + "1": { + "patterns": [ + { + "include": "#inline_comment" + } + ] + }, + "2": { + "name": "comment.block.c punctuation.definition.comment.begin.c" + }, + "3": { + "name": "comment.block.c" + }, + "4": { + "patterns": [ + { + "match": "\\*\\/", + "name": "comment.block.c punctuation.definition.comment.end.c" + }, + { + "match": "\\*", + "name": "comment.block.c" + } + ] + }, + "5": { + "name": "variable.other.asm.label.c" + }, + "6": { + "patterns": [ + { + "include": "#inline_comment" + } + ] + }, + "7": { + "name": "comment.block.c punctuation.definition.comment.begin.c" + }, + "8": { + "name": "comment.block.c" + }, + "9": { + "patterns": [ + { + "match": "\\*\\/", + "name": "comment.block.c punctuation.definition.comment.end.c" + }, + { + "match": "\\*", + "name": "comment.block.c" + } + ] + } + } + }, + { + "match": ":", + "name": "punctuation.separator.delimiter.colon.assembly.c" + }, + { + "include": "#comments_context" + }, + { + "include": "#comments" + } + ] + } + ] + } + ] + }, + "string_escaped_char": { + "patterns": [ + { + "match": "(?x)\\\\ (\n\\\\\t\t\t |\n[abefnprtv'\"?] |\n[0-3]\\d{,2}\t |\n[4-7]\\d?\t\t|\nx[a-fA-F0-9]{,2} |\nu[a-fA-F0-9]{,4} |\nU[a-fA-F0-9]{,8} )", + "name": "constant.character.escape.c" + }, + { + "match": "\\\\.", + "name": "invalid.illegal.unknown-escape.c" + } + ] + }, + "string_placeholder": { + "patterns": [ + { + "match": "(?x) %\n(\\d+\\$)?\t\t\t\t\t\t # field (argument #)\n[#0\\- +']*\t\t\t\t\t\t # flags\n[,;:_]?\t\t\t\t\t\t\t # separator character (AltiVec)\n((-?\\d+)|\\*(-?\\d+\\$)?)?\t\t # minimum field width\n(\\.((-?\\d+)|\\*(-?\\d+\\$)?)?)?\t# precision\n(hh|h|ll|l|j|t|z|q|L|vh|vl|v|hv|hl)? # length modifier\n[diouxXDOUeEfFgGaACcSspn%]\t\t # conversion type", + "name": "constant.other.placeholder.c" + }, + { + "match": "(%)(?!\"\\s*(PRI|SCN))", + "captures": { + "1": { + "name": "invalid.illegal.placeholder.c" + } + } + } + ] + }, + "strings": { + "patterns": [ + { + "begin": "\"", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.c" + } + }, + "end": "\"", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.c" + } + }, + "name": "string.quoted.double.c", + "patterns": [ + { + "include": "#string_escaped_char" + }, + { + "include": "#string_placeholder" + }, + { + "include": "#line_continuation_character" + } + ] + }, + { + "begin": "'", + "beginCaptures": { + "0": { + "name": "punctuation.definition.string.begin.c" + } + }, + "end": "'", + "endCaptures": { + "0": { + "name": "punctuation.definition.string.end.c" + } + }, + "name": "string.quoted.single.c", + "patterns": [ + { + "include": "#string_escaped_char" + }, + { + "include": "#line_continuation_character" + } + ] + } + ] + }, + "switch_conditional_parentheses": { + "name": "meta.conditional.switch.c", + "begin": "((?>(?:(?:(?>(?(?:[^\\*]|(?>\\*+)[^\\/])*)((?>\\*+)\\/)))+|(?:(?:(?:(?:\\b|(?<=\\W))|(?=\\W))|\\A)|\\Z))))(\\()", + "beginCaptures": { + "1": { + "patterns": [ + { + "include": "#inline_comment" + } + ] + }, + "2": { + "name": "comment.block.c punctuation.definition.comment.begin.c" + }, + "3": { + "name": "comment.block.c" + }, + "4": { + "patterns": [ + { + "match": "\\*\\/", + "name": "comment.block.c punctuation.definition.comment.end.c" + }, + { + "match": "\\*", + "name": "comment.block.c" + } + ] + }, + "5": { + "name": "punctuation.section.parens.begin.bracket.round.conditional.switch.c" + } + }, + "end": "(\\))", + "endCaptures": { + "1": { + "name": "punctuation.section.parens.end.bracket.round.conditional.switch.c" + } + }, + "patterns": [ + { + "include": "#evaluation_context" + }, + { + "include": "#c_conditional_context" + } + ] + }, + "switch_statement": { + "name": "meta.block.switch.c", + "begin": "(((?>(?:(?:(?>(?(?:[^\\*]|(?>\\*+)[^\\/])*)((?>\\*+)\\/)))+|(?:(?:(?:(?:\\b|(?<=\\W))|(?=\\W))|\\A)|\\Z))))((?|\\?\\?>)|(?=[;>\\[\\]=]))", + "patterns": [ + { + "name": "meta.head.switch.c", + "begin": "\\G ?", + "end": "((?:\\{|<%|\\?\\?<|(?=;)))", + "endCaptures": { + "1": { + "name": "punctuation.section.block.begin.bracket.curly.switch.c" + } + }, + "patterns": [ + { + "include": "#switch_conditional_parentheses" + }, + { + "include": "$self" + } + ] + }, + { + "name": "meta.body.switch.c", + "begin": "(?<=\\{|<%|\\?\\?<)", + "end": "(\\}|%>|\\?\\?>)", + "endCaptures": { + "1": { + "name": "punctuation.section.block.end.bracket.curly.switch.c" + } + }, + "patterns": [ + { + "include": "#default_statement" + }, + { + "include": "#case_statement" + }, + { + "include": "$self" + }, + { + "include": "#block_innards" + } + ] + }, + { + "name": "meta.tail.switch.c", + "begin": "(?<=\\}|%>|\\?\\?>)[\\s\\n]*", + "end": "[\\s\\n]*(?=;)", + "patterns": [ + { + "include": "$self" + } + ] + } + ] + }, + "vararg_ellipses": { + "match": "(? l = MimeLookup.getLookup("text/x-ext-t").lookup(Language.class); assertNotNull(l); - LanguageStorage.store(Arrays.asList(new LanguageDescription("t", "txt", FileUtil.toFile(grammar).getAbsolutePath(), null, "txt", null, false, null))); + LanguageStorage.store(Arrays.asList(new LanguageDescription("t", "txt", FileUtil.toFile(grammar).getAbsolutePath(), null, null, "txt", null, false, null))); LanguageStorage.store(Collections.emptyList()); diff --git a/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/spi/friend/LanguageConfigurationTest.java b/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/spi/friend/LanguageConfigurationTest.java new file mode 100644 index 000000000000..6439e3c87173 --- /dev/null +++ b/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/spi/friend/LanguageConfigurationTest.java @@ -0,0 +1,76 @@ +/* + * 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 org.junit.Test; +import static org.junit.Assert.*; + +public class LanguageConfigurationTest { + + public LanguageConfigurationTest() { + } + + @Test + public void testComments() { + LanguageConfiguration lc; + + lc = LanguageConfiguration.from(""" + { + "comments": { + "lineComment": "//" + } + } + """); + assertNotNull(lc.comments); + assertNotNull(lc.comments.lineComment); + assertEquals("//", lc.comments.lineComment.comment); + assertFalse(lc.comments.lineComment.noIdent); + assertNull(lc.comments.blockComment); + + lc = LanguageConfiguration.from(""" + { + "comments": { + "blockComment": ["/*", "*/"] + } + } + """); + assertNotNull(lc.comments); + assertNull(lc.comments.lineComment); + assertNotNull(lc.comments.blockComment); + assertEquals("/*", lc.comments.blockComment.first); + assertEquals("*/", lc.comments.blockComment.second); + + lc = LanguageConfiguration.from(""" + { + "comments": { + "lineComment": { + "comment": "//", + "noIndent": true + } + } + } + """); + assertNotNull(lc.comments); + assertNotNull(lc.comments.lineComment); + assertNull(lc.comments.blockComment); + assertEquals("//", lc.comments.lineComment.comment); + assertTrue(lc.comments.lineComment.noIdent); + } + +} diff --git a/nbbuild/build.properties b/nbbuild/build.properties index 2311f404f16c..2ed8ae47fd6e 100644 --- a/nbbuild/build.properties +++ b/nbbuild/build.properties @@ -176,6 +176,7 @@ config.javadoc.forwarded.devel=\ editor.lib,\ editor,\ lib.uihandler,\ + lsp.client,\ uihandler,\ spi.editor.hints,\ api.dashboard diff --git a/nbbuild/cluster.properties b/nbbuild/cluster.properties index a668f4e128e3..856220b88acc 100644 --- a/nbbuild/cluster.properties +++ b/nbbuild/cluster.properties @@ -394,6 +394,7 @@ nb.cluster.ide=\ libs.graalsdk,\ libs.graalsdk.system,\ libs.ini4j,\ + libs.jackson,\ libs.jaxb,\ libs.jcodings,\ libs.jsch.agentproxy,\ @@ -767,7 +768,6 @@ nb.cluster.enterprise=\ libs.elimpl,\ libs.fabric8,\ libs.glassfish_logging,\ - libs.jackson,\ libs.jstl,\ maven.j2ee,\ maven.jaxws,\