diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 768bfb2dd5f6..48d25d499570 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -674,6 +674,9 @@ jobs:
- name: ide/jumpto
run: ant $OPTS -f ide/jumpto test
+ - name: ide/languages.env
+ run: ant $OPTS -f ide/languages.env test
+
- name: ide/languages.hcl
run: ant $OPTS -f ide/languages.hcl test
diff --git a/ide/languages.env/build.xml b/ide/languages.env/build.xml
new file mode 100644
index 000000000000..a24da2f19c39
--- /dev/null
+++ b/ide/languages.env/build.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+ Builds, tests, and runs the project org.netbeans.modules.languages.env
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ide/languages.env/licenseinfo.xml b/ide/languages.env/licenseinfo.xml
new file mode 100644
index 000000000000..95a00da45439
--- /dev/null
+++ b/ide/languages.env/licenseinfo.xml
@@ -0,0 +1,34 @@
+
+
+
+
+ src/org/netbeans/modules/languages/env/resources/envFile.env
+ src/org/netbeans/modules/languages/env/resources/envColoring.env
+
+
+
+
+ src/org/netbeans/modules/languages/env/resources/env_file_16.png
+
+
+
+
\ No newline at end of file
diff --git a/ide/languages.env/manifest.mf b/ide/languages.env/manifest.mf
new file mode 100644
index 000000000000..472c50e6675c
--- /dev/null
+++ b/ide/languages.env/manifest.mf
@@ -0,0 +1,7 @@
+Manifest-Version: 1.0
+OpenIDE-Module: org.netbeans.modules.languages.env
+OpenIDE-Module-Layer: org/netbeans/modules/languages/env/resources/layer.xml
+OpenIDE-Module-Localizing-Bundle: org/netbeans/modules/languages/env/resources/Bundle.properties
+OpenIDE-Module-Specification-Version: 0.1
+AutoUpdate-Show-In-Client: true
+
diff --git a/ide/languages.env/nbproject/project.properties b/ide/languages.env/nbproject/project.properties
new file mode 100644
index 000000000000..4c3b5d6a165f
--- /dev/null
+++ b/ide/languages.env/nbproject/project.properties
@@ -0,0 +1,20 @@
+# 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.
+
+javac.compilerargs=-Xlint -Xlint:-serial
+spec.version.base.fatal.warning=false
+javac.release=17
diff --git a/ide/languages.env/nbproject/project.xml b/ide/languages.env/nbproject/project.xml
new file mode 100644
index 000000000000..094d7bdccd01
--- /dev/null
+++ b/ide/languages.env/nbproject/project.xml
@@ -0,0 +1,218 @@
+
+
+
+ org.netbeans.modules.apisupport.project
+
+
+ org.netbeans.modules.languages.env
+
+
+ org.netbeans.api.annotations.common
+
+
+
+ 1
+ 1.59
+
+
+
+ org.netbeans.api.templates
+
+
+
+ 1.39
+
+
+
+ org.netbeans.core.multiview
+
+
+
+ 1
+ 1.75
+
+
+
+ org.netbeans.libs.antlr4.runtime
+
+
+
+ 2
+ 1.32
+
+
+
+ org.netbeans.modules.csl.api
+
+
+
+ 2
+ 2.89
+
+
+
+ org.netbeans.modules.csl.types
+
+
+
+ 1
+ 1.31
+
+
+
+ org.netbeans.modules.lexer
+
+
+
+ 2
+ 1.94
+
+
+
+ org.netbeans.modules.lexer.antlr4
+
+
+
+ 1.13
+
+
+
+ org.netbeans.modules.options.api
+
+
+
+ 1
+ 1.76
+
+
+
+ org.netbeans.modules.parsing.api
+
+
+
+ 1
+ 9.38
+
+
+
+ org.netbeans.modules.projectapi
+
+
+
+ 1
+ 1.103
+
+
+
+ org.netbeans.modules.web.common
+
+
+
+ 1.130
+
+
+
+ org.openide.awt
+
+
+
+ 7.99
+
+
+
+ org.openide.filesystems
+
+
+
+ 9.44
+
+
+
+ org.openide.util
+
+
+
+ 9.39
+
+
+
+ org.openide.util.lookup
+
+
+
+ 8.65
+
+
+
+ org.openide.util.ui
+
+
+
+ 9.40
+
+
+
+ org.openide.windows
+
+
+
+ 6.108
+
+
+
+
+
+ unit
+
+ org.netbeans.libs.junit4
+
+
+
+ org.netbeans.modules.csl.api
+
+
+
+
+ org.netbeans.modules.csl.types
+
+
+
+ org.netbeans.modules.lexer
+
+
+
+
+ org.netbeans.modules.nbjunit
+
+
+
+
+ org.openide.util.lookup
+
+
+
+
+
+
+
+
+
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/Bundle.properties b/ide/languages.env/src/org/netbeans/modules/languages/env/Bundle.properties
new file mode 100644
index 000000000000..e023e0beadfa
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/Bundle.properties
@@ -0,0 +1,19 @@
+# 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.
+
+CTL_OnOff_CheckBox=Mark &Occurrences Of Symbol Under Caret
+MarkOccurencesPanel.keepMarksCheckBox.text=Checkbox switching mark occurrences on/off
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/EnvDeclarationFinder.java b/ide/languages.env/src/org/netbeans/modules/languages/env/EnvDeclarationFinder.java
new file mode 100644
index 000000000000..9bc5ea7e77e2
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/EnvDeclarationFinder.java
@@ -0,0 +1,93 @@
+/*
+ * 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.languages.env;
+
+import javax.swing.text.AbstractDocument;
+import javax.swing.text.Document;
+import org.netbeans.api.lexer.Token;
+import org.netbeans.api.lexer.TokenHierarchy;
+import org.netbeans.api.lexer.TokenSequence;
+import org.netbeans.modules.csl.api.DeclarationFinder;
+import org.netbeans.modules.csl.api.OffsetRange;
+import org.netbeans.modules.csl.spi.ParserResult;
+import org.netbeans.modules.languages.env.lexer.EnvTokenId;
+import org.netbeans.modules.languages.env.parser.EnvParserResult;
+import org.openide.filesystems.FileObject;
+
+public class EnvDeclarationFinder implements DeclarationFinder {
+
+ @Override
+ public OffsetRange getReferenceSpan(Document document, int caretOffset) {
+ AbstractDocument doc = (AbstractDocument) document;
+ doc.readLock();
+ try {
+ TokenHierarchy th = TokenHierarchy.get(doc);
+ TokenSequence> ts = th.tokenSequence();
+ ts.move(caretOffset);
+ ts.movePrevious();
+ ts.moveNext();
+ Token> token = ts.token();
+ if ((token.id().equals(EnvTokenId.KEY))) {
+ int start = ts.offset();
+ ts.moveNext();
+
+ if (ts.token().id().equals(EnvTokenId.INTERPOLATION_DELIMITATOR)
+ || ts.token().id().equals(EnvTokenId.INTERPOLATION_OPERATOR)) {
+ int end = ts.offset();
+ return new OffsetRange(start, end);
+ }
+
+ return OffsetRange.NONE;
+ } else {
+ return OffsetRange.NONE;
+ }
+ } finally {
+ doc.readUnlock();
+ }
+ }
+
+ @Override
+ public DeclarationLocation findDeclaration(ParserResult info, int caretOffset) {
+ EnvParserResult result = (EnvParserResult) info;
+ TokenSequence> ts = info.getSnapshot().getTokenHierarchy().tokenSequence();
+ ts.move(caretOffset);
+ ts.movePrevious();
+ ts.moveNext();
+ Token> token = ts.token();
+
+ if (!token.id().equals(EnvTokenId.KEY)) {
+ return DeclarationLocation.NONE;
+ }
+
+ String ref = String.valueOf(token.text());
+ FileObject fo = info.getSnapshot().getSource().getFileObject();
+ return findKeyDeclaration(result, ref, caretOffset, fo);
+ }
+
+ public DeclarationLocation findKeyDeclaration(EnvParserResult result, String key, int caretOffset, FileObject fo) {
+ OffsetRange position = result.getDefinedKeys().get(key);
+
+ if (position != null) {
+ EnvKeyHandle handle = new EnvKeyHandle(key, fo);
+ return new DeclarationFinder.DeclarationLocation(fo, position.getStart(), handle);
+ }
+
+ return DeclarationLocation.NONE;
+ }
+}
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/EnvFileResolver.java b/ide/languages.env/src/org/netbeans/modules/languages/env/EnvFileResolver.java
new file mode 100644
index 000000000000..ba9c66e09beb
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/EnvFileResolver.java
@@ -0,0 +1,122 @@
+/*
+ * 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.languages.env;
+
+import org.netbeans.api.annotations.common.CheckForNull;
+import org.netbeans.api.annotations.common.NonNull;
+import org.openide.awt.ActionID;
+import org.openide.awt.ActionReference;
+import org.openide.awt.ActionReferences;
+import org.openide.filesystems.FileObject;
+import org.openide.filesystems.MIMEResolver;
+import org.openide.util.lookup.ServiceProvider;
+
+@ActionReferences({
+ @ActionReference(
+ path = "Loaders/text/x-env/Actions",
+ id = @ActionID(category = "System", id = "org.openide.actions.OpenAction"),
+ position = 100,
+ separatorAfter = 200
+ ),
+ @ActionReference(
+ path = "Loaders/text/x-env/Actions",
+ id = @ActionID(category = "Edit", id = "org.openide.actions.CutAction"),
+ position = 300
+ ),
+ @ActionReference(
+ path = "Loaders/text/x-env/Actions",
+ id = @ActionID(category = "Edit", id = "org.openide.actions.CopyAction"),
+ position = 400
+ ),
+ @ActionReference(
+ path = "Loaders/text/x-env/Actions",
+ id = @ActionID(category = "Edit", id = "org.openide.actions.PasteAction"),
+ position = 500,
+ separatorAfter = 600
+ ),
+ @ActionReference(
+ path = "Loaders/text/x-env/Actions",
+ id = @ActionID(category = "Edit", id = "org.openide.actions.DeleteAction"),
+ position = 700
+ ),
+ @ActionReference(
+ path = "Loaders/text/x-env/Actions",
+ id = @ActionID(category = "System", id = "org.openide.actions.RenameAction"),
+ position = 800,
+ separatorAfter = 900
+ ),
+ @ActionReference(
+ path = "Loaders/text/x-env/Actions",
+ id = @ActionID(category = "System", id = "org.openide.actions.SaveAsTemplateAction"),
+ position = 1000,
+ separatorAfter = 1100
+ ),
+ @ActionReference(
+ path = "Loaders/text/x-env/Actions",
+ id = @ActionID(category = "System", id = "org.openide.actions.FileSystemAction"),
+ position = 1200,
+ separatorAfter = 1300
+ ),
+ @ActionReference(
+ path = "Loaders/text/x-env/Actions",
+ id = @ActionID(category = "System", id = "org.openide.actions.ToolsAction"),
+ position = 1400
+ ),
+ @ActionReference(
+ path = "Loaders/text/x-env/Actions",
+ id = @ActionID(category = "System", id = "org.openide.actions.PropertiesAction"),
+ position = 1500
+ ),
+ @ActionReference(
+ path = "Editors/text/x-env/Popup",
+ id = @ActionID(category = "Refactoring", id = "org.netbeans.modules.refactoring.api.ui.WhereUsedAction"),
+ position = 1600
+ ),}
+)
+
+@ServiceProvider(service = MIMEResolver.class)
+public class EnvFileResolver extends MIMEResolver {
+ public static final String ENV_EXT = "env"; //NOI18N
+ public static final String MIME_TYPE = "text/x-env"; //NOI18N
+
+ public EnvFileResolver() {
+ super(MIME_TYPE);
+ }
+
+ @CheckForNull
+ @Override
+ public String findMIMEType(@NonNull final FileObject fo) {
+ final String nameWithExt = fo.getNameExt().toLowerCase();
+
+ if (nameWithExt.endsWith("." + ENV_EXT)) { //NOI18N
+ return MIME_TYPE;
+ }
+
+ //Some application might use .env.example name format
+ int envPartPosition = 2;
+ String[] nameParts = nameWithExt.split("\\."); //NOI18N
+
+ //check for previous name part
+ if (nameParts.length >= envPartPosition && nameParts[nameParts.length - envPartPosition].equals(ENV_EXT)) {
+ return MIME_TYPE;
+ }
+
+ return null;
+ }
+}
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/EnvKeyHandle.java b/ide/languages.env/src/org/netbeans/modules/languages/env/EnvKeyHandle.java
new file mode 100644
index 000000000000..4c42bc81e646
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/EnvKeyHandle.java
@@ -0,0 +1,82 @@
+/*
+ * 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.languages.env;
+
+import java.util.Collections;
+import java.util.Set;
+import org.netbeans.modules.csl.api.ElementHandle;
+import org.netbeans.modules.csl.api.ElementKind;
+import org.netbeans.modules.csl.api.Modifier;
+import org.netbeans.modules.csl.api.OffsetRange;
+import org.netbeans.modules.csl.spi.ParserResult;
+import static org.netbeans.modules.languages.env.EnvFileResolver.MIME_TYPE;
+import org.openide.filesystems.FileObject;
+
+public class EnvKeyHandle implements ElementHandle {
+ private final String name;
+ private final ElementKind kind;
+ private final FileObject fo;
+
+ public EnvKeyHandle(final String name, final FileObject fo) {
+ this.name = name;
+ this.kind = ElementKind.CONSTANT;
+ this.fo = fo;
+ }
+
+ @Override
+ public FileObject getFileObject() {
+ return fo;
+ }
+
+ @Override
+ public String getMimeType() {
+ return MIME_TYPE;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String getIn() {
+ return null;
+ }
+
+ @Override
+ public ElementKind getKind() {
+ return kind;
+ }
+
+ @Override
+ public Set getModifiers() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public boolean signatureEquals(ElementHandle handle) {
+ return name.equals(handle.getName());
+ }
+
+ @Override
+ public OffsetRange getOffsetRange(ParserResult result) {
+ return OffsetRange.NONE;
+ }
+
+}
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/EnvLanguage.java b/ide/languages.env/src/org/netbeans/modules/languages/env/EnvLanguage.java
new file mode 100644
index 000000000000..7c43fd879e14
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/EnvLanguage.java
@@ -0,0 +1,136 @@
+/*
+ * 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.languages.env;
+
+import org.netbeans.api.lexer.Language;
+import org.netbeans.core.spi.multiview.MultiViewElement;
+import org.netbeans.core.spi.multiview.text.MultiViewEditorElement;
+import org.netbeans.modules.csl.api.CodeCompletionHandler;
+import org.netbeans.modules.csl.api.DeclarationFinder;
+import org.netbeans.modules.csl.api.HintsProvider;
+import org.netbeans.modules.csl.api.OccurrencesFinder;
+import org.netbeans.modules.csl.spi.DefaultLanguageConfig;
+import org.netbeans.modules.csl.spi.LanguageRegistration;
+import static org.netbeans.modules.languages.env.EnvFileResolver.MIME_TYPE;
+import org.netbeans.modules.languages.env.completion.EnvCompletionHandler;
+import org.netbeans.modules.languages.env.hints.EnvHintsProvider;
+import org.netbeans.modules.languages.env.lexer.EnvLexer;
+import org.netbeans.modules.languages.env.lexer.EnvTokenId;
+import org.netbeans.modules.languages.env.lexer.EnvTokenId.EnvLanguageHierarchy;
+import org.netbeans.modules.languages.env.parser.EnvParser;
+import org.netbeans.modules.languages.env.parser.EnvParserResult;
+import org.netbeans.modules.parsing.spi.Parser;
+import org.netbeans.spi.lexer.Lexer;
+import org.netbeans.spi.lexer.LexerRestartInfo;
+import org.openide.util.*;
+import org.openide.windows.TopComponent;
+
+@LanguageRegistration(mimeType = "text/x-env", useMultiview = true)
+public class EnvLanguage extends DefaultLanguageConfig {
+
+ @NbBundle.Messages("Source=&Source")
+ @MultiViewElement.Registration(displayName = "#Source",
+ iconBase = "org/netbeans/modules/languages/env/resources/env_file_16.png",
+ persistenceType = TopComponent.PERSISTENCE_ONLY_OPENED,
+ preferredID = "env.source",
+ mimeType = MIME_TYPE,
+ position = 2)
+ public static MultiViewEditorElement createMultiViewEditorElement(Lookup context) {
+ return new MultiViewEditorElement(context);
+ }
+
+ public EnvLanguage() {
+ super();
+ }
+
+ @Override
+ public Language getLexerLanguage() {
+ return language;
+ }
+
+ @Override
+ public String getDisplayName() {
+ return "Env"; // NOI18N
+ }
+
+ @Override
+ public String getPreferredExtension() {
+ return "env"; // NOI18N
+ }
+
+ @Override
+ public boolean isIdentifierChar(char c) {
+ return super.isIdentifierChar(c) || c == '_' || c == '.' || c == '-' || c == '@'; // NOI18N
+ }
+
+ @Override
+ public String getLineCommentPrefix() {
+ return "#"; // NOI18N
+ }
+
+ @Override
+ public Parser getParser() {
+ return new EnvParser();
+ }
+
+ @Override
+ public OccurrencesFinder getOccurrencesFinder() {
+ return new EnvOccurencesFinder();
+ }
+
+ @Override
+ public boolean hasOccurrencesFinder() {
+ return true;
+ }
+
+ @Override
+ public DeclarationFinder getDeclarationFinder() {
+ return new EnvDeclarationFinder();
+ }
+
+ @Override
+ public CodeCompletionHandler getCompletionHandler() {
+ return new EnvCompletionHandler();
+ }
+
+ @Override
+ public boolean hasHintsProvider() {
+ return true;
+ }
+
+ @Override
+ public HintsProvider getHintsProvider() {
+ return new EnvHintsProvider();
+ }
+
+ private static final Language language
+ = new EnvLanguageHierarchy() {
+
+ @Override
+ protected String mimeType() {
+ return MIME_TYPE;
+ }
+
+ @Override
+ protected Lexer createLexer(LexerRestartInfo info) {
+ return new EnvLexer(info);
+ }
+
+ }.language();
+}
\ No newline at end of file
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/EnvOccurencesFinder.java b/ide/languages.env/src/org/netbeans/modules/languages/env/EnvOccurencesFinder.java
new file mode 100644
index 000000000000..dd8adb460b6d
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/EnvOccurencesFinder.java
@@ -0,0 +1,116 @@
+/*
+ * 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.languages.env;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import org.netbeans.api.lexer.Token;
+import org.netbeans.api.lexer.TokenHierarchy;
+import org.netbeans.api.lexer.TokenSequence;
+import org.netbeans.modules.csl.api.ColoringAttributes;
+import org.netbeans.modules.csl.api.OccurrencesFinder;
+import org.netbeans.modules.csl.api.OffsetRange;
+import org.netbeans.modules.languages.env.lexer.EnvTokenId;
+import org.netbeans.modules.languages.env.parser.EnvParserResult;
+import org.netbeans.modules.parsing.spi.Scheduler;
+import org.netbeans.modules.parsing.spi.SchedulerEvent;
+
+public class EnvOccurencesFinder extends OccurrencesFinder {
+
+ private int caretPosition;
+ private boolean cancelled;
+ private final Map occurrences = new HashMap<>();
+
+ public EnvOccurencesFinder() {
+ }
+
+ @Override
+ public void setCaretPosition(int position) {
+ caretPosition = position;
+ }
+
+ @Override
+ public Map getOccurrences() {
+ return Collections.unmodifiableMap(occurrences);
+ }
+
+ @Override
+ public void run(EnvParserResult result, SchedulerEvent event) {
+ occurrences.clear();
+ if (checkAndResetCancel()) {
+ return;
+ }
+ computeOccurrences(result);
+ }
+
+ @Override
+ public int getPriority() {
+ return 200;
+ }
+
+ @Override
+ public Class extends Scheduler> getSchedulerClass() {
+ return Scheduler.CURSOR_SENSITIVE_TASK_SCHEDULER;
+ }
+
+ @Override
+ public void cancel() {
+ this.cancelled = true;
+ }
+
+ @Override
+ public boolean isKeepMarks() {
+ return true;
+ }
+
+ @Override
+ public boolean isMarkOccurrencesEnabled() {
+ return true;
+ }
+
+ private boolean checkAndResetCancel() {
+ if (cancelled) {
+ cancelled = false;
+ return true;
+ }
+ return false;
+ }
+
+ private void computeOccurrences(EnvParserResult result) {
+ TokenHierarchy> tokenHierarchy = result.getSnapshot().getTokenHierarchy();
+ TokenSequence> ts = tokenHierarchy.tokenSequence();
+ ts.move(caretPosition);
+ if (ts.movePrevious()) {
+ Token> token = ts.token();
+
+ if (!token.id().equals(EnvTokenId.KEY)) {
+ ts.moveNext();
+ token = ts.token();
+ }
+
+ if (token.id().equals(EnvTokenId.KEY)) {
+ String tokenText = token.text().toString();
+ for (OffsetRange occurance : result.getOccurrences(tokenText)) {
+ occurrences.put(occurance, ColoringAttributes.MARK_OCCURRENCES);
+ }
+ }
+ }
+ }
+}
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/MarkOccurencesOptionsPanelController.java b/ide/languages.env/src/org/netbeans/modules/languages/env/MarkOccurencesOptionsPanelController.java
new file mode 100644
index 000000000000..5b6473cba003
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/MarkOccurencesOptionsPanelController.java
@@ -0,0 +1,95 @@
+/*
+ * 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.languages.env;
+
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import javax.swing.JComponent;
+import org.netbeans.spi.options.OptionsPanelController;
+import org.openide.util.HelpCtx;
+import org.openide.util.Lookup;
+
+public final class MarkOccurencesOptionsPanelController extends OptionsPanelController {
+
+ private MarkOccurencesPanel panel;
+
+ private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
+ private boolean changed;
+
+ @Override
+ public void update() {
+ getPanel().load(this);
+ }
+
+ @Override
+ public void applyChanges() {
+ getPanel().store();
+ }
+
+ @Override
+ public void cancel() {
+ // need not do anything special, if no changes have been persisted yet
+ }
+
+ @Override
+ public boolean isValid() {
+ return true; // Always valid
+ }
+
+ @Override
+ public boolean isChanged() {
+ return getPanel().changed();
+ }
+
+ @Override
+ public HelpCtx getHelpCtx() {
+ return new HelpCtx("netbeans.optionsDialog.env.markoccurrences"); // NOI18N
+ }
+
+ @Override
+ public synchronized JComponent getComponent(Lookup masterLookup) {
+ return getPanel();
+ }
+
+ public synchronized MarkOccurencesPanel getPanel() {
+ if (panel == null) {
+ panel = new MarkOccurencesPanel(this);
+ }
+ return panel;
+ }
+
+ @Override
+ public void addPropertyChangeListener(PropertyChangeListener l) {
+ pcs.addPropertyChangeListener(l);
+ }
+
+ @Override
+ public void removePropertyChangeListener(PropertyChangeListener l) {
+ pcs.removePropertyChangeListener(l);
+ }
+
+ void changed() {
+ if (!changed) {
+ changed = true;
+ pcs.firePropertyChange(OptionsPanelController.PROP_CHANGED, false, true);
+ }
+ pcs.firePropertyChange(OptionsPanelController.PROP_VALID, null, null);
+ }
+
+}
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/MarkOccurencesPanel.form b/ide/languages.env/src/org/netbeans/modules/languages/env/MarkOccurencesPanel.form
new file mode 100644
index 000000000000..1bb86405ec5b
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/MarkOccurencesPanel.form
@@ -0,0 +1,102 @@
+
+
+
+
+
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/MarkOccurencesPanel.java b/ide/languages.env/src/org/netbeans/modules/languages/env/MarkOccurencesPanel.java
new file mode 100644
index 000000000000..7201736e1b77
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/MarkOccurencesPanel.java
@@ -0,0 +1,197 @@
+/*
+ * 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.languages.env;
+
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.prefs.BackingStoreException;
+import java.util.prefs.Preferences;
+import javax.swing.JCheckBox;
+import javax.swing.JPanel;
+import org.openide.util.Exceptions;
+
+public class MarkOccurencesPanel extends JPanel {
+
+ private static final boolean DEFAULT_VALUE = true;
+ private List boxes;
+ private MarkOccurencesOptionsPanelController controller;
+
+ /* Creates new form MarkOccurencesPanel */
+ public MarkOccurencesPanel(MarkOccurencesOptionsPanelController controller) {
+ initComponents();
+ fillBoxes();
+ addListeners();
+ load(controller);
+ }
+
+ public void load(MarkOccurencesOptionsPanelController controller) {
+ this.controller = controller;
+
+ Preferences node = MarkOccurencesSettings.getCurrentNode();
+
+ for (JCheckBox box : boxes) {
+ box.setSelected(node.getBoolean(box.getActionCommand(), DEFAULT_VALUE));
+ }
+
+ componentsSetEnabled();
+
+ }
+
+ public void store() {
+ Preferences node = MarkOccurencesSettings.getCurrentNode();
+ for (javax.swing.JCheckBox box : boxes) {
+ boolean value = box.isSelected();
+ boolean original = node.getBoolean(box.getActionCommand(),
+ DEFAULT_VALUE);
+
+ if (value != original) {
+ node.putBoolean(box.getActionCommand(), value);
+ }
+ }
+ try {
+ node.flush();
+ } catch (BackingStoreException ex) {
+ Exceptions.printStackTrace(ex);
+ }
+ }
+
+ public boolean changed() {
+ Preferences node = MarkOccurencesSettings.getCurrentNode();
+ for (JCheckBox box : boxes) {
+ boolean value = box.isSelected();
+ boolean original = node.getBoolean(box.getActionCommand(), DEFAULT_VALUE);
+ if (value != original) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+
+ /** This method is called from within the constructor to
+ * initialize the form.
+ * WARNING: Do NOT modify this code. The content of this method is
+ * always regenerated by the Form Editor.
+ */
+ // //GEN-BEGIN:initComponents
+ private void initComponents() {
+ java.awt.GridBagConstraints gridBagConstraints;
+
+ onOffCheckBox = new javax.swing.JCheckBox();
+ keepMarksCheckBox = new javax.swing.JCheckBox();
+
+ setBorder(javax.swing.BorderFactory.createEmptyBorder(8, 8, 8, 8));
+ setFocusCycleRoot(true);
+ setFocusTraversalPolicy(new java.awt.FocusTraversalPolicy() {
+ public java.awt.Component getDefaultComponent(java.awt.Container focusCycleRoot){
+ return onOffCheckBox;
+ }//end getDefaultComponent
+
+ public java.awt.Component getFirstComponent(java.awt.Container focusCycleRoot){
+ return onOffCheckBox;
+ }//end getFirstComponent
+
+ public java.awt.Component getLastComponent(java.awt.Container focusCycleRoot){
+ return onOffCheckBox;
+ }//end getLastComponent
+
+ public java.awt.Component getComponentAfter(java.awt.Container focusCycleRoot, java.awt.Component aComponent){
+ return onOffCheckBox;//end getComponentAfter
+ }
+ public java.awt.Component getComponentBefore(java.awt.Container focusCycleRoot, java.awt.Component aComponent){
+ return onOffCheckBox;//end getComponentBefore
+
+ }}
+ );
+ setLayout(new java.awt.GridBagLayout());
+
+ org.openide.awt.Mnemonics.setLocalizedText(onOffCheckBox, org.openide.util.NbBundle.getMessage(MarkOccurencesPanel.class, "CTL_OnOff_CheckBox")); // NOI18N
+ onOffCheckBox.setBorder(javax.swing.BorderFactory.createEmptyBorder(0, 0, 0, 0));
+ gridBagConstraints = new java.awt.GridBagConstraints();
+ gridBagConstraints.gridx = 0;
+ gridBagConstraints.gridy = 0;
+ gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER;
+ gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
+ gridBagConstraints.weightx = 1.0;
+ gridBagConstraints.insets = new java.awt.Insets(0, 0, 12, 0);
+ add(onOffCheckBox, gridBagConstraints);
+ onOffCheckBox.getAccessibleContext().setAccessibleName(org.openide.util.NbBundle.getMessage(MarkOccurencesPanel.class, "MarkOccurrencesPanel.onOffCheckBox.AccessibleContext.accessibleName")); // NOI18N
+ onOffCheckBox.getAccessibleContext().setAccessibleDescription(org.openide.util.NbBundle.getMessage(MarkOccurencesPanel.class, "ACSD_OnOff_CB")); // NOI18N
+
+ keepMarksCheckBox.setMnemonic('s');
+ org.openide.awt.Mnemonics.setLocalizedText(keepMarksCheckBox, org.openide.util.NbBundle.getMessage(MarkOccurencesPanel.class, "MarkOccurencesPanel.keepMarksCheckBox.text")); // NOI18N
+ gridBagConstraints = new java.awt.GridBagConstraints();
+ gridBagConstraints.gridx = 0;
+ gridBagConstraints.gridy = 1;
+ gridBagConstraints.gridwidth = java.awt.GridBagConstraints.REMAINDER;
+ gridBagConstraints.gridheight = java.awt.GridBagConstraints.REMAINDER;
+ gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
+ gridBagConstraints.weighty = 1.0;
+ gridBagConstraints.insets = new java.awt.Insets(0, 20, 8, 0);
+ add(keepMarksCheckBox, gridBagConstraints);
+ keepMarksCheckBox.getAccessibleContext().setAccessibleName(org.openide.util.NbBundle.getMessage(MarkOccurencesPanel.class, "MarkOccurencesPanel.keepMarksCheckBox.text")); // NOI18N
+
+ getAccessibleContext().setAccessibleName(org.openide.util.NbBundle.getMessage(MarkOccurencesPanel.class, "MarkOccurrencesPanel.AccessibleContext.accessibleName")); // NOI18N
+ getAccessibleContext().setAccessibleDescription(org.openide.util.NbBundle.getMessage(MarkOccurencesPanel.class, "MarkOccurrencesPanel.AccessibleContext.accessibleDescription")); // NOI18N
+ }// //GEN-END:initComponents
+
+
+ // Variables declaration - do not modify//GEN-BEGIN:variables
+ private javax.swing.JCheckBox keepMarksCheckBox;
+ private javax.swing.JCheckBox onOffCheckBox;
+ // End of variables declaration//GEN-END:variables
+ // End of variables declaration
+
+ private void fillBoxes() {
+ boxes = new ArrayList<>();
+ boxes.add(onOffCheckBox);
+ boxes.add(keepMarksCheckBox);
+ onOffCheckBox.setActionCommand(MarkOccurencesSettings.ON_OFF);
+ keepMarksCheckBox.setActionCommand(MarkOccurencesSettings.KEEP_MARKS);
+ }
+
+ private void addListeners() {
+ ItemListener itemListener = new CheckItemListener();
+ for (JCheckBox box : boxes) {
+ box.addItemListener(itemListener);
+ }
+ }
+
+ private void componentsSetEnabled() {
+ for (int i = 1; i < boxes.size(); i++) {
+ boxes.get(i).setEnabled(onOffCheckBox.isSelected()); // Switch off the other boxes
+ }
+ }
+
+ private class CheckItemListener implements ItemListener {
+
+ @Override
+ public void itemStateChanged(ItemEvent evt) {
+ if (evt.getSource() == onOffCheckBox) {
+ componentsSetEnabled();
+ }
+ controller.changed();
+ }
+
+ }
+
+}
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/MarkOccurencesSettings.java b/ide/languages.env/src/org/netbeans/modules/languages/env/MarkOccurencesSettings.java
new file mode 100644
index 000000000000..e193fbdce1a9
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/MarkOccurencesSettings.java
@@ -0,0 +1,42 @@
+/*
+ * 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.languages.env;
+
+import java.util.prefs.Preferences;
+import org.openide.util.NbPreferences;
+
+public final class MarkOccurencesSettings {
+
+ private static final String MARK_OCCURENCES = "MarkOccurences"; // NOI18N
+ public static final String ON_OFF = "OnOff"; // NOI18N
+ public static final String KEEP_MARKS = "KeepMarks"; // NOI18N
+
+ private MarkOccurencesSettings() {
+ }
+
+ public static Preferences getCurrentNode() {
+ Preferences preferences = NbPreferences.forModule(MarkOccurencesOptionsPanelController.class);
+ return preferences.node("env").node(MARK_OCCURENCES).node(getCurrentProfileId());
+ }
+
+ private static String getCurrentProfileId() {
+ return "default"; // NOI18N
+ }
+
+}
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/SimpleHandle.java b/ide/languages.env/src/org/netbeans/modules/languages/env/SimpleHandle.java
new file mode 100644
index 000000000000..faaf7ae3dddb
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/SimpleHandle.java
@@ -0,0 +1,79 @@
+/*
+ * 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.languages.env;
+
+import java.util.Collections;
+import java.util.Set;
+import org.netbeans.modules.csl.api.ElementHandle;
+import org.netbeans.modules.csl.api.ElementKind;
+import org.netbeans.modules.csl.api.Modifier;
+import org.netbeans.modules.csl.api.OffsetRange;
+import org.netbeans.modules.csl.spi.ParserResult;
+import org.openide.filesystems.FileObject;
+
+public class SimpleHandle implements ElementHandle {
+ private final String name;
+ private final ElementKind kind;
+
+ public SimpleHandle(final String name, final ElementKind kind) {
+ this.name = name;
+ this.kind = kind;
+ }
+
+ @Override
+ public FileObject getFileObject() {
+ return null;
+ }
+
+ @Override
+ public String getMimeType() {
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public String getIn() {
+ return null;
+ }
+
+ @Override
+ public ElementKind getKind() {
+ return kind;
+ }
+
+ @Override
+ public Set getModifiers() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public boolean signatureEquals(ElementHandle handle) {
+ return name.equals(handle.getName());
+ }
+
+ @Override
+ public OffsetRange getOffsetRange(ParserResult result) {
+ return OffsetRange.NONE;
+ }
+
+}
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/completion/EnvCompletionHandler.java b/ide/languages.env/src/org/netbeans/modules/languages/env/completion/EnvCompletionHandler.java
new file mode 100644
index 000000000000..0d2f332886e3
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/completion/EnvCompletionHandler.java
@@ -0,0 +1,200 @@
+/*
+ * 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.languages.env.completion;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import javax.swing.text.Document;
+import javax.swing.text.AbstractDocument;
+import javax.swing.text.JTextComponent;
+import org.netbeans.api.annotations.common.NonNull;
+import org.netbeans.api.lexer.Token;
+import org.netbeans.api.lexer.TokenHierarchy;
+import org.netbeans.api.lexer.TokenSequence;
+import org.netbeans.modules.csl.api.CodeCompletionContext;
+import org.netbeans.modules.csl.api.CodeCompletionHandler2;
+import org.netbeans.modules.csl.api.CodeCompletionResult;
+import org.netbeans.modules.csl.api.CompletionProposal;
+import org.netbeans.modules.csl.api.Documentation;
+import org.netbeans.modules.csl.api.ElementHandle;
+import org.netbeans.modules.csl.api.OffsetRange;
+import org.netbeans.modules.csl.api.ParameterInfo;
+import org.netbeans.modules.csl.spi.DefaultCompletionResult;
+import org.netbeans.modules.csl.spi.ParserResult;
+import org.netbeans.modules.csl.spi.support.CancelSupport;
+import org.netbeans.modules.languages.env.EnvKeyHandle;
+import org.netbeans.modules.languages.env.lexer.EnvTokenId;
+import org.netbeans.modules.languages.env.parser.EnvParserResult;
+import org.openide.filesystems.FileObject;
+
+public class EnvCompletionHandler implements CodeCompletionHandler2 {
+
+ @Override
+ public Documentation documentElement(ParserResult info, ElementHandle element, Callable cancel) {
+ return null;
+ }
+
+ @Override
+ public CodeCompletionResult complete(CodeCompletionContext context) {
+
+ if (CancelSupport.getDefault().isCancelled()) {
+ return CodeCompletionResult.NONE;
+ }
+
+ if (!(context.getParserResult() instanceof EnvParserResult)) {
+ return CodeCompletionResult.NONE;
+ }
+
+ EnvParserResult parserResult = (EnvParserResult) context.getParserResult();
+
+ //only interpolation are relevant
+ if (parserResult.getInterpolationOccurences().isEmpty()) {
+ return CodeCompletionResult.NONE;
+ }
+
+ boolean isInInterpolationContext = false;
+
+ for (Map.Entry> entry : parserResult.getInterpolationOccurences().entrySet()) {
+ for (OffsetRange range : entry.getValue()) {
+ if (range.containsInclusive(context.getCaretOffset())) {
+ isInInterpolationContext = true;
+ break;
+ }
+ }
+ }
+
+ if (!isInInterpolationContext) {
+ return CodeCompletionResult.NONE;
+ }
+
+ int offset = context.getCaretOffset();
+ FileObject fo = parserResult.getSnapshot().getSource().getFileObject();
+
+ final List completionProposals = new ArrayList<>();
+
+ String contextPrefix = context.getPrefix();
+
+ for (Map.Entry entry : parserResult.getDefinedKeys().entrySet()) {
+ if (entry.getValue().getEnd() > offset) {
+ continue;
+ }
+ if (entry.getKey().startsWith(contextPrefix)) {
+ int anchorOffset = computeAnchorOffset(contextPrefix, offset);
+ EnvKeyHandle handle = new EnvKeyHandle(entry.getKey(), fo);
+ completionProposals.add(new KeyCompletionProposal(handle, anchorOffset));
+ }
+ }
+
+ return new DefaultCompletionResult(completionProposals, false);
+ }
+
+ @Override
+ public String document(ParserResult info, ElementHandle element) {
+ return null;
+ }
+
+ @Override
+ public ElementHandle resolveLink(String link, ElementHandle originalHandle) {
+ return null;
+ }
+
+ @Override
+ public String getPrefix(ParserResult info, int caretOffset, boolean upToOffset) {
+ return PrefixResolver.create(info, caretOffset).resolve();
+ }
+
+ @Override
+ public QueryType getAutoQuery(JTextComponent component, String typedText) {
+ QueryType result = QueryType.ALL_COMPLETION;
+ if (typedText.length() == 0 || typedText.isBlank()) {
+ result = QueryType.NONE;
+ }
+
+ return result;
+ }
+
+ @Override
+ public String resolveTemplateVariable(String variable, ParserResult info, int caretOffset, String name, Map parameters) {
+ return null;
+ }
+
+ @Override
+ public Set getApplicableTemplates(Document doc, int selectionBegin, int selectionEnd) {
+ return null;
+ }
+
+ @Override
+ public ParameterInfo parameters(ParserResult info, int caretOffset, CompletionProposal proposal) {
+ return ParameterInfo.NONE;
+ }
+
+ private int computeAnchorOffset(@NonNull String prefix, int offset) {
+ return offset - prefix.length();
+ }
+
+ private static final class PrefixResolver {
+
+ private final ParserResult info;
+ private final int offset;
+
+ static PrefixResolver create(ParserResult info, int offset) {
+ return new PrefixResolver(info, offset);
+ }
+
+ private PrefixResolver(ParserResult info, int offset) {
+ this.info = info;
+ this.offset = offset;
+ }
+
+ private String resolve() {
+ AbstractDocument doc = (AbstractDocument) info.getSnapshot().getSource().getDocument(false);
+ doc.readLock();
+ try {
+ TokenHierarchy th = TokenHierarchy.get(doc);
+ TokenSequence> ts = th.tokenSequence();
+ ts.move(offset);
+ ts.movePrevious();
+ ts.moveNext();
+ Token> token = ts.token();
+ String tokenText = token.text().toString();
+
+ if (token.id().equals(EnvTokenId.INTERPOLATION_OPERATOR)) {
+ if (tokenText.equals("{")) { //NOI18N
+ ts.moveNext();
+ token = ts.token();
+ } else {
+ ts.movePrevious();
+ token = ts.token();
+ }
+
+ if (token.id().equals(EnvTokenId.INTERPOLATION_OPERATOR)) {
+ return token.text().toString();
+ }
+ }
+
+ return null;
+ } finally {
+ doc.readUnlock();
+ }
+ }
+ }
+}
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/completion/KeyCompletionProposal.java b/ide/languages.env/src/org/netbeans/modules/languages/env/completion/KeyCompletionProposal.java
new file mode 100644
index 000000000000..83ba1a277353
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/completion/KeyCompletionProposal.java
@@ -0,0 +1,72 @@
+/*
+ * 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.languages.env.completion;
+
+import org.netbeans.modules.csl.api.ElementHandle;
+import org.netbeans.modules.csl.api.ElementKind;
+import org.netbeans.modules.csl.api.HtmlFormatter;
+import org.netbeans.modules.csl.spi.DefaultCompletionProposal;
+import org.netbeans.modules.languages.env.EnvKeyHandle;
+
+public class KeyCompletionProposal extends DefaultCompletionProposal {
+
+ private final ElementHandle element;
+
+ public KeyCompletionProposal(EnvKeyHandle element, int anchorOffset) {
+ this.element = element;
+ this.anchorOffset = anchorOffset;
+ }
+
+ @Override
+ public ElementHandle getElement() {
+ return element;
+ }
+
+ @Override
+ public String getName() {
+ return element.getName();
+ }
+
+ @Override
+ public String getSortText() {
+ return getName();
+ }
+
+ @Override
+ public int getAnchorOffset() {
+ return anchorOffset;
+ }
+
+ @Override
+ public ElementKind getKind() {
+ return element.getKind();
+ }
+
+ @Override
+ public String getLhsHtml(HtmlFormatter formatter) {
+ formatter.name(getKind(), true);
+ formatter.appendHtml(""); // NOI18N
+ formatter.appendHtml(""); // NOI18N
+ formatter.appendText(getName());
+ formatter.appendHtml(""); // NOI18N
+ formatter.appendHtml(""); // NOI18N
+ formatter.name(getKind(), false); // NOI18N
+ return formatter.getText();
+ }
+}
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/grammar/antlr4/coloring/.gitignore b/ide/languages.env/src/org/netbeans/modules/languages/env/grammar/antlr4/coloring/.gitignore
new file mode 100644
index 000000000000..0d262341919a
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/grammar/antlr4/coloring/.gitignore
@@ -0,0 +1 @@
+EnvAntlr*.java
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/grammar/antlr4/coloring/EnvAntlrColoringLexer.g4 b/ide/languages.env/src/org/netbeans/modules/languages/env/grammar/antlr4/coloring/EnvAntlrColoringLexer.g4
new file mode 100644
index 000000000000..307f7f57c557
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/grammar/antlr4/coloring/EnvAntlrColoringLexer.g4
@@ -0,0 +1,180 @@
+/*
+ * 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.
+ */
+lexer grammar EnvAntlrColoringLexer;
+
+@header{package org.netbeans.modules.languages.env.grammar.antlr4.coloring;}
+
+tokens {
+ NL,
+ WS,
+ COMMENT,
+ STRING,
+ VALUE,
+ KEY,
+ KEYWORD,
+ OPERATOR,
+ ASSIGN_OPERATOR,
+ DOLLAR,
+ DELIMITATOR
+}
+
+options {
+ superClass = LexerAdaptor;
+ caseInsensitive = true;
+}
+
+fragment Esc
+ : '\\'
+ ;
+
+fragment SQuote
+ : '\''
+ ;
+
+fragment DQuote
+ : '"'
+ ;
+
+fragment BackTickQuote
+ : '`'
+ ;
+
+fragment SQuoteLiteral
+ : SQuote (Esc [btnfr"'\\] | ~ ['\\])* SQuote
+ ;
+
+fragment NewLine
+ : [\r\n]
+ ;
+
+fragment NewLineComment
+ : '#' ~ [\r\n]* (NL | EOF)
+ ;
+
+fragment Identifier
+ : [a-z_\u0080-\ufffe][a-z0-9_.\u0080-\ufffe-]*;
+
+fragment KeyIdentiifier
+ : [a-z_]+[a-z0-9_]*
+ ;
+
+KEY
+ : KeyIdentiifier
+ ;
+COMMENT
+ : NewLineComment
+ ;
+ASSIGN_OPERATOR
+ : ('=' | ':')->pushMode(VarAssign)
+ ;
+NL
+ : NewLine+
+ ;
+WS
+ : [ \t]+ ->skip
+ ;
+ERROR
+ : .
+ ;
+mode VarAssign;
+DB_STRING_OPEN
+ : DQuote ->type(STRING),pushMode(DbQuoteString)
+ ;
+
+B_STRING_OPEN
+ : BackTickQuote ->type(STRING),pushMode(BackQuotedString)
+ ;
+
+SG_STRING_OPEN
+ : SQuoteLiteral ->type(STRING)
+ ;
+EXIT_COMMENT
+ : (' ')+ NewLineComment->type(COMMENT), popMode
+ ;
+DELIMITATOR_VAR
+ : (',' | '|' | ':') ->type(DELIMITATOR)
+ ;
+KEYWORD_VAR
+ : (
+ 'true' | 'false' | 'null' | 'on' | '?'+
+ | 'prod' | 'production' | 'live'
+ | 'development' | 'local' | 'test'
+ ) {this._input.LA(1) == '\n'}?->type(KEYWORD)
+ ;
+INTERPOLATED_VAR
+ : '$' {this._input.LA(1) == '{'}?
+ ->type(DOLLAR),pushMode(StringInterpolation)
+;
+
+//greedy identifier matching
+IDENTIFIER_VAR
+ : Identifier {this._input.LA(1) == '\n'}?
+ ->type(VALUE)
+ ;
+EXIT_VAR_ASSING : NewLine->type(NL), popMode;
+INLINE_WS : [ \t]+->skip;
+ANY_VALUE : . ->type(VALUE);
+
+mode DbQuoteString;
+
+DBQ_TEXT : (Esc [btnfr"'\\] | ~ [$"\r\n\\])+->type(STRING);
+DBQ_INTERPOLATED_VAR
+ : '$' {this._input.LA(1) == '{'}?
+ ->type(DOLLAR),pushMode(StringInterpolation)
+;
+DBQ_STRING_CLOSE : DQuote ->type(STRING),popMode;
+ANY_DBQ_TEXT : . ->type(STRING);
+
+mode BackQuotedString;
+
+BQ_TEXT : (Esc [btnfr"'`\\] | ~ [$`\r\n\\])+->type(STRING);
+BQ_INTERPOLATED_VAR
+ : '$' {this._input.LA(1) == '{'}?
+ ->type(DOLLAR),pushMode(StringInterpolation)
+;
+BQ_STRING_CLOSE : BackTickQuote ->type(STRING),popMode;
+ANY_BQ_TEXT : . ->type(STRING);
+
+mode StringInterpolation;
+
+CURLY_OPEN
+ : '{' {this.resetInterpolationKeyAdded();}
+ ;
+CURLY_CLOSE
+ : '}' ->popMode
+ ;
+INTERPOLATION_VAR
+ : {!this.keyTokenAdded()}? KeyIdentiifier {this.consumeKeyToken();}->type(KEY)
+ ;
+
+/*
+from https://dotenvx.com/docs/env-file
+${VAR:-default} -> value of VAR if set and non-empty, otherwise default
+${VAR-default} -> value of VAR if set, otherwise default
+
+${VAR:+alternate} -> value of alternate if VAR is set and non-empty, otherwise empty ''
+${VAR+alternate} -> value of alternate if VAR is set and non-empty, otherwise empty ''
+*/
+INTERPOLATION_OPERATOR
+ : (':' ('+' | '-')? | '?' | '+' | '-')
+ ;
+
+VALUE_INTERPOLATION
+ : . ->type(VALUE)
+ ;
\ No newline at end of file
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/grammar/antlr4/coloring/LexerAdaptor.java b/ide/languages.env/src/org/netbeans/modules/languages/env/grammar/antlr4/coloring/LexerAdaptor.java
new file mode 100644
index 000000000000..af37d1f0486c
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/grammar/antlr4/coloring/LexerAdaptor.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.languages.env.grammar.antlr4.coloring;
+
+import org.antlr.v4.runtime.CharStream;
+import org.antlr.v4.runtime.Lexer;
+
+public abstract class LexerAdaptor extends Lexer {
+
+ private boolean interpolationKeyAdded = false;
+
+ public LexerAdaptor(CharStream input) {
+ super(input);
+ }
+
+ public void setInterpolationKeyAddedState(boolean state) {
+ interpolationKeyAdded = state;
+ }
+
+ public void consumeKeyToken() {
+ interpolationKeyAdded = true;
+ }
+
+ public void resetInterpolationKeyAdded() {
+ interpolationKeyAdded = false;
+ }
+
+ public boolean keyTokenAdded() {
+ return interpolationKeyAdded;
+ }
+}
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/grammar/antlr4/parser/.gitignore b/ide/languages.env/src/org/netbeans/modules/languages/env/grammar/antlr4/parser/.gitignore
new file mode 100644
index 000000000000..0d262341919a
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/grammar/antlr4/parser/.gitignore
@@ -0,0 +1 @@
+EnvAntlr*.java
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/grammar/antlr4/parser/EnvAntlrLexer.g4 b/ide/languages.env/src/org/netbeans/modules/languages/env/grammar/antlr4/parser/EnvAntlrLexer.g4
new file mode 100644
index 000000000000..f4e1def01c43
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/grammar/antlr4/parser/EnvAntlrLexer.g4
@@ -0,0 +1,174 @@
+/*
+ * 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.
+ */
+lexer grammar EnvAntlrLexer;
+
+@header{package org.netbeans.modules.languages.env.grammar.antlr4.parser;}
+
+tokens {
+ NL,
+ DOLLAR
+}
+
+options {
+ superClass = LexerAdaptor;
+ caseInsensitive = true;
+}
+
+fragment Esc
+ : '\\'
+ ;
+
+fragment SQuote
+ : '\''
+ ;
+
+fragment DQuote
+ : '"'
+ ;
+
+fragment BackTickQuote
+ : '`'
+ ;
+
+fragment SQuoteLiteral
+ : SQuote (Esc [btnfr"'\\] | ~ ['\\])* SQuote
+ ;
+
+fragment NewLine
+ : [\r\n]
+ ;
+
+fragment NewLineComment
+ : '#' ~ [\r\n]* (NL | EOF)
+ ;
+
+fragment Identifier
+ : [a-z_\u0080-\ufffe][a-z0-9_.\u0080-\ufffe-]*
+ ;
+
+fragment KeyIdentiifier
+ : [a-z_]+[a-z0-9_]*
+ ;
+
+KEY
+ : KeyIdentiifier
+ ;
+COMMENT
+ : NewLineComment->skip
+ ;
+
+ASSIGN_OPERATOR
+ : ('=' | ':')->pushMode(VarAssign)
+ ;
+
+NL
+ : NewLine+
+ ;
+
+WS
+ : [ \t]+ ->skip
+ ;
+ERROR
+ : .
+ ;
+
+mode VarAssign;
+
+DB_STRING_OPEN
+ : DQuote ->skip,pushMode(DbQuoteString)
+ ;
+
+B_STRING_OPEN
+ : BackTickQuote ->skip,pushMode(BacktickQuotedString)
+ ;
+
+
+SG_STRING_OPEN
+ : SQuoteLiteral ->skip
+ ;
+
+INTERPOLATED_VAR
+ : '$' {this._input.LA(1) == '{'}?
+ ->type(DOLLAR),pushMode(StringInterpolation)
+ ;
+
+//greedy identifier matching
+IDENTIFIER_VAR
+ : Identifier {this._input.LA(1) == '\n'}? ->skip
+ ;
+
+EXIT_VAR_ASSING : NewLine->type(NL), popMode;
+INLINE_WS : [ \t]->skip;
+ANY_VALUE : . ->skip;
+
+mode DbQuoteString;
+
+DBQ_TEXT
+ : (Esc [btnfr"'\\] | ~ [$"\r\n\\])+->skip
+ ;
+
+DBQ_INTERPOLATED_VAR
+ : '$' {this._input.LA(1) == '{'}?
+ ->type(DOLLAR),pushMode(StringInterpolation)
+ ;
+
+DBQ_STRING_CLOSE
+ : DQuote ->skip,popMode
+ ;
+
+ANY_DBQ_TEXT : . ->skip;
+
+mode BacktickQuotedString;
+
+BQ_TEXT
+ : (Esc [btnfr"'`\\] | ~ [$`\r\n\\])+->skip
+ ;
+
+BQ_INTERPOLATED_VAR
+ : '$' {this._input.LA(1) == '{'}?
+ ->type(DOLLAR),pushMode(StringInterpolation)
+ ;
+
+BQ_STRING_CLOSE
+ : BackTickQuote ->skip,popMode
+ ;
+
+ANY_BQ_TEXT : . ->skip;
+
+mode StringInterpolation;
+
+CURLY_OPEN
+ : '{' {this.resetInterpolationKeyAdded();}
+ ;
+
+CURLY_CLOSE
+ : '}' ->popMode
+ ;
+
+INTERPOLATION_VAR
+ : {!this.keyTokenAdded()}? KeyIdentiifier {this.consumeKeyToken();} ->type(KEY)
+ ;
+
+INTERPOLATION_OPERATOR
+ : (':' ('+' | '-' | '?')? | '?' | '+' | '-')
+ ;
+
+VALUE_INTERPOLATION
+ : . ->skip
+ ;
\ No newline at end of file
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/grammar/antlr4/parser/EnvAntlrParser.g4 b/ide/languages.env/src/org/netbeans/modules/languages/env/grammar/antlr4/parser/EnvAntlrParser.g4
new file mode 100644
index 000000000000..f765448f43d7
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/grammar/antlr4/parser/EnvAntlrParser.g4
@@ -0,0 +1,51 @@
+/*
+ * 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.
+ */
+parser grammar EnvAntlrParser;
+
+@header{package org.netbeans.modules.languages.env.grammar.antlr4.parser;}
+
+options {
+ superClass = ParserAdaptor;
+ tokenVocab = EnvAntlrLexer;
+ }
+
+envFile
+ : line* EOF
+ ;
+
+line
+ : varAssign | newLine
+ ;
+
+newLine
+ : NL
+ ;
+
+varAssign
+ : KEY assignOperator interpolatedKey* (newLine | EOF)
+ ;
+
+assignOperator
+ : ASSIGN_OPERATOR
+ ;
+
+interpolatedKey
+ : DOLLAR
+ CURLY_OPEN keyName=KEY INTERPOLATION_OPERATOR? CURLY_CLOSE
+ ;
\ No newline at end of file
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/grammar/antlr4/parser/LexerAdaptor.java b/ide/languages.env/src/org/netbeans/modules/languages/env/grammar/antlr4/parser/LexerAdaptor.java
new file mode 100644
index 000000000000..63585a38873f
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/grammar/antlr4/parser/LexerAdaptor.java
@@ -0,0 +1,43 @@
+/*
+ * 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.languages.env.grammar.antlr4.parser;
+
+import org.antlr.v4.runtime.CharStream;
+import org.antlr.v4.runtime.Lexer;
+
+public abstract class LexerAdaptor extends Lexer {
+
+ private boolean interpolationKeyAdded = false;
+
+ public LexerAdaptor(CharStream input) {
+ super(input);
+ }
+
+ public void consumeKeyToken() {
+ interpolationKeyAdded = true;
+ }
+
+ public void resetInterpolationKeyAdded() {
+ interpolationKeyAdded = false;
+ }
+
+ public boolean keyTokenAdded() {
+ return interpolationKeyAdded;
+ }
+}
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/grammar/antlr4/parser/ParserAdaptor.java b/ide/languages.env/src/org/netbeans/modules/languages/env/grammar/antlr4/parser/ParserAdaptor.java
new file mode 100644
index 000000000000..1239f2b1f7ac
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/grammar/antlr4/parser/ParserAdaptor.java
@@ -0,0 +1,39 @@
+/*
+ * 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.languages.env.grammar.antlr4.parser;
+
+import org.antlr.v4.runtime.Parser;
+import org.antlr.v4.runtime.TokenStream;
+
+public abstract class ParserAdaptor extends Parser {
+
+ public static enum ParserContext {
+ STANDARD
+ }
+
+ protected ParserContext parserContext = ParserContext.STANDARD;
+
+ public ParserAdaptor(TokenStream input) {
+ super(input);
+ }
+
+ public void setEnvParserContext(ParserContext context){
+ this.parserContext = context;
+ }
+}
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/hints/Bundle.properties b/ide/languages.env/src/org/netbeans/modules/languages/env/hints/Bundle.properties
new file mode 100644
index 000000000000..a2a320cd6f65
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/hints/Bundle.properties
@@ -0,0 +1,22 @@
+# 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.
+
+DuplicateKeyHintMsg=Duplicate Key Assignment
+AST_Rule_DuplicateKey=Duplicate Key Assignment
+AST_Rule_DuplicateKeyeDescription=Duplicate Key assignment detected.
+
+csl-hints/text/x-env/hints/duplicate_key_assignment=Duplicate key assignment
\ No newline at end of file
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/hints/DuplicateKeyAssignment.java b/ide/languages.env/src/org/netbeans/modules/languages/env/hints/DuplicateKeyAssignment.java
new file mode 100644
index 000000000000..4b29d984b9d0
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/hints/DuplicateKeyAssignment.java
@@ -0,0 +1,77 @@
+/*
+ * 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.languages.env.hints;
+
+import java.util.Collections;
+import java.util.Set;
+import java.util.prefs.Preferences;
+import javax.swing.JComponent;
+import org.netbeans.modules.csl.api.HintSeverity;
+import org.netbeans.modules.csl.api.Rule;
+import org.netbeans.modules.csl.api.RuleContext;
+import static org.netbeans.modules.languages.env.hints.EnvHintsProvider.ENV_HINTS_GROUP_KIND;
+import org.openide.util.NbBundle;
+
+public class DuplicateKeyAssignment implements Rule.AstRule {
+
+ @Override
+ public boolean getDefaultEnabled() {
+ return true;
+ }
+
+ @Override
+ public JComponent getCustomizer(Preferences node) {
+ return null;
+ }
+
+ @Override
+ public boolean appliesTo(RuleContext context) {
+ return context instanceof EnvHintsProvider.EnvRuleContext;
+ }
+
+ @Override
+ public boolean showInTasklist() {
+ return true;
+ }
+
+ @Override
+ public HintSeverity getDefaultSeverity() {
+ return HintSeverity.WARNING;
+ }
+
+ @Override
+ public Set> getKinds() {
+ return Collections.singleton(ENV_HINTS_GROUP_KIND);
+ }
+
+ @Override
+ public String getId() {
+ return "env.hint.duplicate_key_assignment"; //NOI18N
+ }
+
+ @Override
+ public String getDescription() {
+ return NbBundle.getMessage(DuplicateKeyAssignment.class, "AST_Rule_DuplicateKey"); //NOI18N
+ }
+
+ @Override
+ public String getDisplayName() {
+ return NbBundle.getMessage(DuplicateKeyAssignment.class, "AST_Rule_DuplicateKeyeDescription"); //NOI18N
+ }
+}
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/hints/EnvHintsProvider.java b/ide/languages.env/src/org/netbeans/modules/languages/env/hints/EnvHintsProvider.java
new file mode 100644
index 000000000000..90ff188663b9
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/hints/EnvHintsProvider.java
@@ -0,0 +1,152 @@
+/*
+ * 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.languages.env.hints;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import org.netbeans.api.options.OptionsDisplayer;
+import org.netbeans.modules.csl.api.Error;
+import org.netbeans.modules.csl.api.Hint;
+import org.netbeans.modules.csl.api.HintFix;
+import org.netbeans.modules.csl.api.HintsProvider;
+import org.netbeans.modules.csl.api.OffsetRange;
+import org.netbeans.modules.csl.api.Rule;
+import org.netbeans.modules.csl.api.RuleContext;
+import org.netbeans.modules.languages.env.EnvFileResolver;
+import org.netbeans.modules.languages.env.parser.EnvParserResult;
+import org.openide.util.NbBundle;
+
+public class EnvHintsProvider implements HintsProvider {
+
+ public static final String ENV_HINTS_GROUP_KIND = "env.option.duplicatekey.hints"; //NOI18N
+
+ @Override
+ public void computeHints(HintsManager manager, RuleContext context, List hints) {
+ if (!(context.parserResult instanceof EnvParserResult)) {
+ return;
+ }
+
+ EnvParserResult parserResult = (EnvParserResult) context.parserResult;
+
+ Map, List extends Rule.AstRule>> allHints = manager.getHints(false, context);
+ List extends Rule.AstRule> hintRules = allHints.get(ENV_HINTS_GROUP_KIND);
+
+ if (hintRules == null) {
+ return;
+ }
+
+ for (Rule.AstRule astRule : hintRules) {
+ if (!manager.isEnabled(astRule)) {
+ continue;
+ }
+ if (astRule instanceof DuplicateKeyAssignment) {
+ for (Map.Entry> entry : parserResult.getKeyDefinitions().entrySet()) {
+ int listSize = entry.getValue().size();
+
+ if (listSize == 1) {
+ continue;
+ }
+
+ for (OffsetRange range : entry.getValue().subList(1, listSize)) {
+ hints.add(new Hint(astRule,
+ NbBundle.getMessage(EnvHintsProvider.class, "DuplicateKeyHintMsg"), //NOI18N
+ context.parserResult.getSnapshot().getSource().getFileObject(),
+ range,
+ configHintsFixList(),
+ 10));
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void computeSuggestions(HintsManager manager, RuleContext context, List suggestions, int caretOffset) {
+
+ }
+
+ @Override
+ public void computeSelectionHints(HintsManager manager, RuleContext context, List suggestions, int start, int end) {
+
+ }
+
+ @Override
+ public void computeErrors(HintsManager manager, RuleContext context, List hints, List unhandled) {
+ unhandled.addAll(context.parserResult.getDiagnostics());
+ }
+
+ @Override
+ public void cancel() {
+
+ }
+
+ @Override
+ public List getBuiltinRules() {
+ return null;
+ }
+
+ @Override
+ public RuleContext createRuleContext() {
+ return new EnvRuleContext();
+ }
+
+ private List configHintsFixList() {
+ List fixes = new LinkedList<>();
+
+ fixes.add(new ConfigHintFix());
+
+ return fixes;
+ }
+
+ public class EnvRuleContext extends RuleContext {
+
+ public boolean isCancelled() {
+ return false;
+ }
+
+ }
+
+ private static class ConfigHintFix implements HintFix {
+
+ public ConfigHintFix() {
+ }
+
+ @Override
+ public String getDescription() {
+ return "Configure Hints"; //NOI18N
+ }
+
+ @Override
+ public void implement() throws Exception {
+ OptionsDisplayer displayer = OptionsDisplayer.getDefault();
+ displayer.open("Editor/Hints/" + EnvFileResolver.MIME_TYPE); //NOI18N
+ }
+
+ @Override
+ public boolean isSafe() {
+ return true;
+ }
+
+ @Override
+ public boolean isInteractive() {
+ return false;
+ }
+ }
+}
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/hints/HintsControllerFactory.java b/ide/languages.env/src/org/netbeans/modules/languages/env/hints/HintsControllerFactory.java
new file mode 100644
index 000000000000..e77f7746ae16
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/hints/HintsControllerFactory.java
@@ -0,0 +1,43 @@
+/*
+ * 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.languages.env.hints;
+
+import org.netbeans.modules.csl.api.HintsProvider;
+import org.netbeans.modules.languages.env.EnvFileResolver;
+import org.netbeans.spi.options.OptionsPanelController;
+import org.openide.util.NbBundle;
+
+public class HintsControllerFactory {
+
+ public HintsControllerFactory() {
+ }
+
+ @OptionsPanelController.SubRegistration(
+ id = "EnvFileHints",
+ location = "Env/Hints",
+ displayName = "#HintsControllerFactory.name"
+ )
+ @NbBundle.Messages("HintsControllerFactory.name=Env File Hints")
+ public static OptionsPanelController createOptions() {
+ HintsProvider.HintsManager manager = HintsProvider.HintsManager.getManagerForMimeType(EnvFileResolver.MIME_TYPE);
+ assert manager != null;
+
+ return manager.getOptionsController();
+ }
+}
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/lexer/EnvLexer.java b/ide/languages.env/src/org/netbeans/modules/languages/env/lexer/EnvLexer.java
new file mode 100644
index 000000000000..fd486adbccac
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/lexer/EnvLexer.java
@@ -0,0 +1,70 @@
+/*
+ * 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.languages.env.lexer;
+
+import org.netbeans.api.lexer.Token;
+import org.netbeans.spi.lexer.LexerRestartInfo;
+import org.netbeans.spi.lexer.antlr4.AbstractAntlrLexerBridge;
+import org.netbeans.modules.languages.env.grammar.antlr4.coloring.EnvAntlrColoringLexer;
+import static org.netbeans.modules.languages.env.grammar.antlr4.coloring.EnvAntlrColoringLexer.*;
+
+public class EnvLexer extends AbstractAntlrLexerBridge {
+ public EnvLexer(LexerRestartInfo info) {
+ super(info, EnvAntlrColoringLexer::new);
+ }
+
+ @Override
+ public Object state() {
+ return new State(lexer);
+ }
+
+ @Override
+ protected Token mapToken(org.antlr.v4.runtime.Token antlrToken) {
+ return switch (antlrToken.getType()) {
+ case COMMENT -> groupToken(EnvTokenId.COMMENT, COMMENT);
+ case KEY -> token(EnvTokenId.KEY);
+ case KEYWORD -> token(EnvTokenId.KEYWORD);
+ case STRING -> groupToken(EnvTokenId.STRING, STRING);
+ case VALUE -> groupToken(EnvTokenId.VALUE, VALUE);
+ case OPERATOR, ASSIGN_OPERATOR -> token(EnvTokenId.OPERATOR);
+ case CURLY_OPEN, CURLY_CLOSE -> token(EnvTokenId.INTERPOLATION_DELIMITATOR);
+ case INTERPOLATION_OPERATOR -> token(EnvTokenId.INTERPOLATION_OPERATOR);
+ case DELIMITATOR -> token(EnvTokenId.DELIMITATOR);
+ case DOLLAR -> token(EnvTokenId.DOLLAR);
+ case WS -> groupToken(EnvTokenId.WS, WS);
+ case NL -> groupToken(EnvTokenId.WS, NL);
+ default -> groupToken(EnvTokenId.ERROR, ERROR);
+ };
+ }
+
+ private static class State extends AbstractAntlrLexerBridge.LexerState {
+ final boolean interpolationKeyAdded;
+
+ public State(EnvAntlrColoringLexer lexer) {
+ super(lexer);
+ this.interpolationKeyAdded = lexer.keyTokenAdded();
+ }
+
+ @Override
+ public void restore(EnvAntlrColoringLexer lexer) {
+ super.restore(lexer);
+ lexer.setInterpolationKeyAddedState(interpolationKeyAdded);
+ }
+ }
+}
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/lexer/EnvTokenId.java b/ide/languages.env/src/org/netbeans/modules/languages/env/lexer/EnvTokenId.java
new file mode 100644
index 000000000000..b731072adf24
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/lexer/EnvTokenId.java
@@ -0,0 +1,69 @@
+/*
+ * 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.languages.env.lexer;
+
+import java.util.Collection;
+import java.util.EnumSet;
+import org.netbeans.api.lexer.InputAttributes;
+import org.netbeans.api.lexer.LanguagePath;
+import org.netbeans.api.lexer.Token;
+import org.netbeans.api.lexer.TokenId;
+import org.netbeans.spi.lexer.LanguageEmbedding;
+import org.netbeans.spi.lexer.LanguageHierarchy;
+
+public enum EnvTokenId implements TokenId {
+ COMMENT("comment"),
+ KEY("key"),
+ STRING("string"),
+ KEYWORD("keyword"),
+ VALUE("value"),
+ DELIMITATOR("delimitator"),
+ DOLLAR("dollar"),
+ OPERATOR("operator"),
+ INTERPOLATION_DELIMITATOR("operator"),
+ INTERPOLATION_OPERATOR("interpolation_operator"),
+ WS("whitespace"),
+ ERROR("error");
+ private final String primaryCategory;
+
+ EnvTokenId(String category) {
+ this.primaryCategory = category;
+ }
+
+ @Override
+
+ public String primaryCategory() {
+ return primaryCategory;
+ }
+
+ public static abstract class EnvLanguageHierarchy extends LanguageHierarchy {
+
+ @Override
+ protected Collection createTokenIds() {
+ return EnumSet.allOf(EnvTokenId.class);
+ }
+
+ @Override
+ protected LanguageEmbedding extends TokenId> embedding(Token token,
+ LanguagePath languagePath, InputAttributes inputAttributes) {
+
+ return null;
+ }
+ }
+}
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/parser/EnvParser.java b/ide/languages.env/src/org/netbeans/modules/languages/env/parser/EnvParser.java
new file mode 100644
index 000000000000..f38e206be9e3
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/parser/EnvParser.java
@@ -0,0 +1,51 @@
+/*
+ * 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.languages.env.parser;
+
+import javax.swing.event.ChangeListener;
+import org.netbeans.modules.parsing.api.Snapshot;
+import org.netbeans.modules.parsing.api.Task;
+import org.netbeans.modules.parsing.spi.ParseException;
+import org.netbeans.modules.parsing.spi.SourceModificationEvent;
+
+public class EnvParser extends org.netbeans.modules.parsing.spi.Parser {
+
+ private EnvParserResult lastResult;
+
+ @Override
+ public void parse(Snapshot snapshot, Task task, SourceModificationEvent event) throws ParseException {
+ lastResult = new EnvParserResult(snapshot).get();
+ }
+
+ @Override
+ public Result getResult(Task task) throws ParseException {
+ return lastResult;
+ }
+
+ @Override
+ public void addChangeListener(ChangeListener changeListener) {
+
+ }
+
+ @Override
+ public void removeChangeListener(ChangeListener changeListener) {
+
+ }
+
+}
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/parser/EnvParserResult.java b/ide/languages.env/src/org/netbeans/modules/languages/env/parser/EnvParserResult.java
new file mode 100644
index 000000000000..b65f1479b50e
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/parser/EnvParserResult.java
@@ -0,0 +1,169 @@
+/*
+ * 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.languages.env.parser;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.antlr.v4.runtime.ANTLRErrorListener;
+import org.antlr.v4.runtime.BaseErrorListener;
+import org.antlr.v4.runtime.CharStreams;
+import org.antlr.v4.runtime.CommonTokenStream;
+import org.antlr.v4.runtime.ConsoleErrorListener;
+import org.antlr.v4.runtime.RecognitionException;
+import org.antlr.v4.runtime.Recognizer;
+import org.antlr.v4.runtime.Token;
+import org.netbeans.modules.csl.api.Error;
+import org.netbeans.modules.csl.api.OffsetRange;
+import org.netbeans.modules.csl.api.Severity;
+import org.netbeans.modules.csl.spi.DefaultError;
+import org.netbeans.modules.csl.spi.ParserResult;
+import org.netbeans.modules.languages.env.grammar.antlr4.parser.EnvAntlrLexer;
+import org.netbeans.modules.languages.env.grammar.antlr4.parser.EnvAntlrParser;
+import org.netbeans.modules.languages.env.grammar.antlr4.parser.EnvAntlrParserBaseListener;
+import org.netbeans.modules.parsing.api.Snapshot;
+import org.openide.filesystems.FileObject;
+
+public class EnvParserResult extends ParserResult {
+
+ private final List errors = new ArrayList<>();
+ private final Map definedKeys = new HashMap<>();
+ private final Map> keyDefintions = new HashMap<>();
+ private final Map> interpolationOccurences = new HashMap<>();
+ volatile boolean finished = false;
+
+ public EnvParserResult(Snapshot snapshot) {
+ super(snapshot);
+ }
+
+ public EnvParserResult get() {
+ if (!finished) {
+ EnvAntlrLexer lexer = new EnvAntlrLexer(CharStreams.fromString(String.valueOf(getSnapshot().getText())));
+ EnvAntlrParser parser = new EnvAntlrParser(new CommonTokenStream(lexer));
+
+ parser.setBuildParseTree(false);//faster parser
+ parser.addErrorListener(createErrorListener());
+ parser.addParseListener(new EnvKeyListener());
+ parser.removeErrorListener(ConsoleErrorListener.INSTANCE);
+ parser.envFile();
+ finished = true;
+ }
+ return this;
+ }
+
+ @Override
+ public List extends Error> getDiagnostics() {
+ return errors;
+ }
+
+ @Override
+ protected void invalidate() {
+
+ }
+
+ private ANTLRErrorListener createErrorListener() {
+ return new BaseErrorListener() {
+ @Override
+ public void syntaxError(Recognizer, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e) {
+ int errorPosition = 0;
+ if (offendingSymbol instanceof Token) {
+ Token offendingToken = (Token) offendingSymbol;
+ errorPosition = offendingToken.getStartIndex();
+ }
+ errors.add(new BadgingDefaultError(null, msg, null, getFileObject(), errorPosition, errorPosition, Severity.ERROR, true));
+ }
+
+ };
+ }
+
+ public final List extends OffsetRange> getOccurrences(String refName) {
+ ArrayList ret = new ArrayList<>();
+ if (keyDefintions.containsKey(refName)) {
+ ret.add(definedKeys.get(refName));
+ }
+ if (interpolationOccurences.containsKey(refName)) {
+ ret.addAll(interpolationOccurences.get(refName));
+ }
+ return ret;
+ }
+
+ private class EnvKeyListener extends EnvAntlrParserBaseListener {
+
+ @Override
+ public void exitVarAssign(EnvAntlrParser.VarAssignContext ctx) {
+ Token keyToken = ctx.start;
+
+ if (keyToken == null) {
+ return;
+ }
+
+ String keyName = keyToken.getText();
+ OffsetRange range = new OffsetRange(keyToken.getStartIndex(), keyToken.getStopIndex() + 1);
+ definedKeys.putIfAbsent(keyName, range);
+ keyDefintions.computeIfAbsent(keyName, s -> new ArrayList<>()).add(range);
+ }
+
+ @Override
+ public void exitInterpolatedKey(EnvAntlrParser.InterpolatedKeyContext ctx) {
+ Token keyToken = ctx.keyName;
+
+ if (keyToken == null) {
+ return;
+ }
+
+ OffsetRange range = new OffsetRange(keyToken.getStartIndex(), keyToken.getStopIndex() + 1);
+ interpolationOccurences.computeIfAbsent(keyToken.getText(), s -> new ArrayList<>()).add(range);
+ }
+ }
+
+ public Map getDefinedKeys() {
+ return Collections.unmodifiableMap(definedKeys);
+ }
+
+ public Map> getInterpolationOccurences() {
+ return Collections.unmodifiableMap(interpolationOccurences);
+ }
+
+ public Map> getKeyDefinitions() {
+ return Collections.unmodifiableMap(keyDefintions);
+ }
+
+ protected final FileObject getFileObject() {
+ return getSnapshot().getSource().getFileObject();
+ }
+
+ private class BadgingDefaultError extends DefaultError implements Error.Badging {
+
+ private boolean badging;
+
+ public BadgingDefaultError(String key, String displayName, String description, FileObject file, int start, int end, Severity severity, boolean badging) {
+ super(key, displayName, description, file, start, end, severity);
+ this.badging = badging;
+ }
+
+ @Override
+ public boolean showExplorerBadge() {
+ return badging;
+ }
+
+
+ }
+}
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/project/EnvFileImpl.java b/ide/languages.env/src/org/netbeans/modules/languages/env/project/EnvFileImpl.java
new file mode 100644
index 000000000000..8c1c6dc16fbf
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/project/EnvFileImpl.java
@@ -0,0 +1,57 @@
+/*
+ * 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.languages.env.project;
+
+import java.util.Collection;
+import javax.swing.event.ChangeListener;
+import org.netbeans.api.project.Project;
+import org.netbeans.modules.web.common.spi.ImportantFilesImplementation;
+import org.netbeans.modules.web.common.spi.ImportantFilesSupport;
+import org.netbeans.spi.project.LookupProvider;
+import org.netbeans.spi.project.ProjectServiceProvider;
+
+@ProjectServiceProvider(service = ImportantFilesImplementation.class, projectTypes = {
+ @LookupProvider.Registration.ProjectType(id = "org-netbeans-modules-web-clientproject"),
+ @LookupProvider.Registration.ProjectType(id = "org-netbeans-modules-php-project"),
+})
+public class EnvFileImpl implements ImportantFilesImplementation {
+
+ private final ImportantFilesSupport support;
+
+ public EnvFileImpl(Project project) {
+ assert project != null;
+ support = ImportantFilesSupport.create(project.getProjectDirectory(), ".env"); // NOI18N
+ }
+
+ @Override
+ public Collection getFiles() {
+ return support.getFiles(null);
+ }
+
+ @Override
+ public void addChangeListener(ChangeListener listener) {
+ support.addChangeListener(listener);
+ }
+
+ @Override
+ public void removeChangeListener(ChangeListener listener) {
+ support.removeChangeListener(listener);
+ }
+
+}
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/resources/Bundle.properties b/ide/languages.env/src/org/netbeans/modules/languages/env/resources/Bundle.properties
new file mode 100644
index 000000000000..58683ba01a8e
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/resources/Bundle.properties
@@ -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.
+
+OpenIDE-Module-Name=Env Files Editor Support
+OpenIDE-Module-Display-Category=Editing
+OpenIDE-Module-Short-Description=Support for editing Env files.
+OpenIDE-Module-Long-Description=Support for editing Env files.
+
+Editors/text/x-env=Env
+
+text/x-env=Env
+
+comment=Comment
+string=String
+keyword=Keyword (null, true ...)
+operator=Operator
+delimitator=Value delimitator (',', '|', ':')
+dollar=Dollar
+interpolation_operator=Interpolation operator
+key=Key
+value=Value
+whitespace=Whitespace
+error=Error
\ No newline at end of file
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/resources/FontAndColors-bluetheme.xml b/ide/languages.env/src/org/netbeans/modules/languages/env/resources/FontAndColors-bluetheme.xml
new file mode 100644
index 000000000000..5287f15c36cb
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/resources/FontAndColors-bluetheme.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/resources/FontAndColors-citylights.xml b/ide/languages.env/src/org/netbeans/modules/languages/env/resources/FontAndColors-citylights.xml
new file mode 100644
index 000000000000..5287f15c36cb
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/resources/FontAndColors-citylights.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/resources/FontAndColors.xml b/ide/languages.env/src/org/netbeans/modules/languages/env/resources/FontAndColors.xml
new file mode 100644
index 000000000000..aec59c5f56d3
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/resources/FontAndColors.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/resources/TemplateHelp.html b/ide/languages.env/src/org/netbeans/modules/languages/env/resources/TemplateHelp.html
new file mode 100644
index 000000000000..33ef012129d4
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/resources/TemplateHelp.html
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+ Creates a new Env configuration file. You can edit the file in the IDE's Source Editor. To change this template, choose Tools | Template Manager and open the template in the editor.
+
+
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/resources/envColoring.env b/ide/languages.env/src/org/netbeans/modules/languages/env/resources/envColoring.env
new file mode 100644
index 000000000000..9031e58f9801
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/resources/envColoring.env
@@ -0,0 +1,26 @@
+#COMMENT
+KEY=VALUE
+MY_OTHER_KEY=MY_OTHER_VALUE
+KEY_STR = "Multiline
+text"
+
+SINGLE_QUOTED_TEXT='single quoted'
+BACKTICK_QUOTED_TEXT=`backtick quoted text`
+
+INTERPOLATED_KEY = "${KEY_STR}"
+
+INTERPOLATED_NAME_DEF1="${VAR:-default}"
+INTERPOLATED_NAME_DEF2="${VAR-default}"
+
+INTERPOLATED_NAME_ERR1="${VAR:?error}"
+
+ENUMERATION=value1, value2, value3
+
+URL_PATH =https://my.demo.com
+
+PATH=/my/path
+
+#
+KEYWORD=true
+KEYWORD_NULL=true
+NO_VALUE=??
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/resources/envFile.env b/ide/languages.env/src/org/netbeans/modules/languages/env/resources/envFile.env
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/resources/env_file_16.png b/ide/languages.env/src/org/netbeans/modules/languages/env/resources/env_file_16.png
new file mode 100644
index 000000000000..837aeaa20ea0
Binary files /dev/null and b/ide/languages.env/src/org/netbeans/modules/languages/env/resources/env_file_16.png differ
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/resources/layer.xml b/ide/languages.env/src/org/netbeans/modules/languages/env/resources/layer.xml
new file mode 100644
index 000000000000..4225206f124b
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/resources/layer.xml
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ide/languages.env/src/org/netbeans/modules/languages/env/resources/package-info.java b/ide/languages.env/src/org/netbeans/modules/languages/env/resources/package-info.java
new file mode 100644
index 000000000000..0cef6cc7c27e
--- /dev/null
+++ b/ide/languages.env/src/org/netbeans/modules/languages/env/resources/package-info.java
@@ -0,0 +1,29 @@
+/*
+ * 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.
+ */
+
+@TemplateRegistration(folder = "Other",
+ displayName = "Env file",
+ content = "envFile.env",
+ position = 671,
+ category = "simple-files",
+ description = "TemplateHelp.html"
+)
+package org.netbeans.modules.languages.env.resources;
+
+import org.netbeans.api.templates.TemplateRegistration;
diff --git a/ide/languages.env/test/unit/data/testfiles/antlr4/lexer/env01.env b/ide/languages.env/test/unit/data/testfiles/antlr4/lexer/env01.env
new file mode 100644
index 000000000000..fa6590d2601e
--- /dev/null
+++ b/ide/languages.env/test/unit/data/testfiles/antlr4/lexer/env01.env
@@ -0,0 +1,27 @@
+APP_NAME='my file'
+TEXT_VALUE="Lorem impsum
+ipsum lorem.
+"
+
+NO_VALUE=
+
+INTERPOLATED_NAME="${APP_NAME}"
+
+INTERPOLATED_NAME_DEF1="${VAR:-default}"
+INTERPOLATED_NAME_DEF2="${VAR-default}"
+
+INTERPOLATED_NAME_ERR1="${VAR:?error}"
+INTERPOLATED_NAME_ERR2="${VAR?error}"
+
+INTERPOLATED_NAME_REPLACEMENT1="${VAR:+alternate}"
+INTERPOLATED_NAME_REPLACEMENT2="${VAR+alternate}"
+
+# With spaces and comments
+DATABASE_URL = postgres://user:pass@host:port/db # Development DB
+
+# With interpolation
+API_HOST=api.myproject.com
+
+# Using quotes for values with spaces or special chars
+API_KEY="api_test_..."
+MESSAGE='Hello, World!'
diff --git a/ide/languages.env/test/unit/data/testfiles/antlr4/lexer/env01.env.lexer b/ide/languages.env/test/unit/data/testfiles/antlr4/lexer/env01.env.lexer
new file mode 100644
index 000000000000..3e0400965ead
--- /dev/null
+++ b/ide/languages.env/test/unit/data/testfiles/antlr4/lexer/env01.env.lexer
@@ -0,0 +1,84 @@
+Token #3 KEY [APP_NAME]
+Token #5 ASSIGN_OPERATOR [=]
+Token #1 NL [\n]
+Token #3 KEY [TEXT_VALUE]
+Token #5 ASSIGN_OPERATOR [=]
+Token #1 NL [\n]
+Token #1 NL [\n]
+Token #3 KEY [NO_VALUE]
+Token #5 ASSIGN_OPERATOR [=]
+Token #1 NL [\n]
+Token #1 NL [\n]
+Token #3 KEY [INTERPOLATED_NAME]
+Token #5 ASSIGN_OPERATOR [=]
+Token #2 DOLLAR [$]
+Token #20 '{' [{]
+Token #3 KEY [APP_NAME]
+Token #21 '}' [}]
+Token #1 NL [\n]
+Token #1 NL [\n]
+Token #3 KEY [INTERPOLATED_NAME_DEF1]
+Token #5 ASSIGN_OPERATOR [=]
+Token #2 DOLLAR [$]
+Token #20 '{' [{]
+Token #3 KEY [VAR]
+Token #22 INTERPOLATION_OPERATOR [:-]
+Token #21 '}' [}]
+Token #1 NL [\n]
+Token #3 KEY [INTERPOLATED_NAME_DEF2]
+Token #5 ASSIGN_OPERATOR [=]
+Token #2 DOLLAR [$]
+Token #20 '{' [{]
+Token #3 KEY [VAR]
+Token #22 INTERPOLATION_OPERATOR [-]
+Token #21 '}' [}]
+Token #1 NL [\n]
+Token #1 NL [\n]
+Token #3 KEY [INTERPOLATED_NAME_ERR1]
+Token #5 ASSIGN_OPERATOR [=]
+Token #2 DOLLAR [$]
+Token #20 '{' [{]
+Token #3 KEY [VAR]
+Token #22 INTERPOLATION_OPERATOR [:?]
+Token #21 '}' [}]
+Token #1 NL [\n]
+Token #3 KEY [INTERPOLATED_NAME_ERR2]
+Token #5 ASSIGN_OPERATOR [=]
+Token #2 DOLLAR [$]
+Token #20 '{' [{]
+Token #3 KEY [VAR]
+Token #22 INTERPOLATION_OPERATOR [?]
+Token #21 '}' [}]
+Token #1 NL [\n]
+Token #1 NL [\n]
+Token #3 KEY [INTERPOLATED_NAME_REPLACEMENT1]
+Token #5 ASSIGN_OPERATOR [=]
+Token #2 DOLLAR [$]
+Token #20 '{' [{]
+Token #3 KEY [VAR]
+Token #22 INTERPOLATION_OPERATOR [:+]
+Token #21 '}' [}]
+Token #1 NL [\n]
+Token #3 KEY [INTERPOLATED_NAME_REPLACEMENT2]
+Token #5 ASSIGN_OPERATOR [=]
+Token #2 DOLLAR [$]
+Token #20 '{' [{]
+Token #3 KEY [VAR]
+Token #22 INTERPOLATION_OPERATOR [+]
+Token #21 '}' [}]
+Token #1 NL [\n]
+Token #1 NL [\n]
+Token #3 KEY [DATABASE_URL]
+Token #5 ASSIGN_OPERATOR [=]
+Token #1 NL [\n]
+Token #1 NL [\n]
+Token #3 KEY [API_HOST]
+Token #5 ASSIGN_OPERATOR [=]
+Token #1 NL [\n]
+Token #1 NL [\n]
+Token #3 KEY [API_KEY]
+Token #5 ASSIGN_OPERATOR [=]
+Token #1 NL [\n]
+Token #3 KEY [MESSAGE]
+Token #5 ASSIGN_OPERATOR [=]
+Token #1 NL [\n]
diff --git a/ide/languages.env/test/unit/data/testfiles/lexer/env01.env b/ide/languages.env/test/unit/data/testfiles/lexer/env01.env
new file mode 100644
index 000000000000..fa6590d2601e
--- /dev/null
+++ b/ide/languages.env/test/unit/data/testfiles/lexer/env01.env
@@ -0,0 +1,27 @@
+APP_NAME='my file'
+TEXT_VALUE="Lorem impsum
+ipsum lorem.
+"
+
+NO_VALUE=
+
+INTERPOLATED_NAME="${APP_NAME}"
+
+INTERPOLATED_NAME_DEF1="${VAR:-default}"
+INTERPOLATED_NAME_DEF2="${VAR-default}"
+
+INTERPOLATED_NAME_ERR1="${VAR:?error}"
+INTERPOLATED_NAME_ERR2="${VAR?error}"
+
+INTERPOLATED_NAME_REPLACEMENT1="${VAR:+alternate}"
+INTERPOLATED_NAME_REPLACEMENT2="${VAR+alternate}"
+
+# With spaces and comments
+DATABASE_URL = postgres://user:pass@host:port/db # Development DB
+
+# With interpolation
+API_HOST=api.myproject.com
+
+# Using quotes for values with spaces or special chars
+API_KEY="api_test_..."
+MESSAGE='Hello, World!'
diff --git a/ide/languages.env/test/unit/data/testfiles/lexer/env01.env.lexer b/ide/languages.env/test/unit/data/testfiles/lexer/env01.env.lexer
new file mode 100644
index 000000000000..9752449f0cde
--- /dev/null
+++ b/ide/languages.env/test/unit/data/testfiles/lexer/env01.env.lexer
@@ -0,0 +1,111 @@
+Token #0 KEY [APP_NAME]
+Token #1 OPERATOR [=]
+Token #2 STRING ['my file']
+Token #3 WS [\n]
+Token #4 KEY [TEXT_VALUE]
+Token #5 OPERATOR [=]
+Token #6 STRING ["Lorem impsum\nipsum lorem.\n"]
+Token #7 WS [\n\n]
+Token #8 KEY [NO_VALUE]
+Token #9 OPERATOR [=]
+Token #10 WS [\n\n]
+Token #11 KEY [INTERPOLATED_NAME]
+Token #12 OPERATOR [=]
+Token #13 STRING ["]
+Token #14 DOLLAR [$]
+Token #15 INTERPOLATION_DELIMITATOR [{]
+Token #16 KEY [APP_NAME]
+Token #17 INTERPOLATION_DELIMITATOR [}]
+Token #18 STRING ["]
+Token #19 WS [\n\n]
+Token #20 KEY [INTERPOLATED_NAME_DEF1]
+Token #21 OPERATOR [=]
+Token #22 STRING ["]
+Token #23 DOLLAR [$]
+Token #24 INTERPOLATION_DELIMITATOR [{]
+Token #25 KEY [VAR]
+Token #26 INTERPOLATION_OPERATOR [:-]
+Token #27 VALUE [default]
+Token #28 INTERPOLATION_DELIMITATOR [}]
+Token #29 STRING ["]
+Token #30 WS [\n]
+Token #31 KEY [INTERPOLATED_NAME_DEF2]
+Token #32 OPERATOR [=]
+Token #33 STRING ["]
+Token #34 DOLLAR [$]
+Token #35 INTERPOLATION_DELIMITATOR [{]
+Token #36 KEY [VAR]
+Token #37 INTERPOLATION_OPERATOR [-]
+Token #38 VALUE [default]
+Token #39 INTERPOLATION_DELIMITATOR [}]
+Token #40 STRING ["]
+Token #41 WS [\n\n]
+Token #42 KEY [INTERPOLATED_NAME_ERR1]
+Token #43 OPERATOR [=]
+Token #44 STRING ["]
+Token #45 DOLLAR [$]
+Token #46 INTERPOLATION_DELIMITATOR [{]
+Token #47 KEY [VAR]
+Token #48 INTERPOLATION_OPERATOR [:]
+Token #49 INTERPOLATION_OPERATOR [?]
+Token #50 VALUE [error]
+Token #51 INTERPOLATION_DELIMITATOR [}]
+Token #52 STRING ["]
+Token #53 WS [\n]
+Token #54 KEY [INTERPOLATED_NAME_ERR2]
+Token #55 OPERATOR [=]
+Token #56 STRING ["]
+Token #57 DOLLAR [$]
+Token #58 INTERPOLATION_DELIMITATOR [{]
+Token #59 KEY [VAR]
+Token #60 INTERPOLATION_OPERATOR [?]
+Token #61 VALUE [error]
+Token #62 INTERPOLATION_DELIMITATOR [}]
+Token #63 STRING ["]
+Token #64 WS [\n\n]
+Token #65 KEY [INTERPOLATED_NAME_REPLACEMENT1]
+Token #66 OPERATOR [=]
+Token #67 STRING ["]
+Token #68 DOLLAR [$]
+Token #69 INTERPOLATION_DELIMITATOR [{]
+Token #70 KEY [VAR]
+Token #71 INTERPOLATION_OPERATOR [:+]
+Token #72 VALUE [alternate]
+Token #73 INTERPOLATION_DELIMITATOR [}]
+Token #74 STRING ["]
+Token #75 WS [\n]
+Token #76 KEY [INTERPOLATED_NAME_REPLACEMENT2]
+Token #77 OPERATOR [=]
+Token #78 STRING ["]
+Token #79 DOLLAR [$]
+Token #80 INTERPOLATION_DELIMITATOR [{]
+Token #81 KEY [VAR]
+Token #82 INTERPOLATION_OPERATOR [+]
+Token #83 VALUE [alternate]
+Token #84 INTERPOLATION_DELIMITATOR [}]
+Token #85 STRING ["]
+Token #86 WS [\n\n]
+Token #87 COMMENT [# With spaces and comments\n]
+Token #88 KEY [DATABASE_URL]
+Token #89 OPERATOR [ =]
+Token #90 VALUE [ postgres]
+Token #91 DELIMITATOR [:]
+Token #92 VALUE [//user]
+Token #93 DELIMITATOR [:]
+Token #94 VALUE [pass@host]
+Token #95 DELIMITATOR [:]
+Token #96 VALUE [port/db]
+Token #97 COMMENT [ # Development DB\n\n# With interpolation\n]
+Token #98 KEY [API_HOST]
+Token #99 OPERATOR [=]
+Token #100 VALUE [api.myproject.com]
+Token #101 WS [\n\n]
+Token #102 COMMENT [# Using quotes for values with spaces or special chars\n]
+Token #103 KEY [API_KEY]
+Token #104 OPERATOR [=]
+Token #105 STRING ["api_test_..."]
+Token #106 WS [\n]
+Token #107 KEY [MESSAGE]
+Token #108 OPERATOR [=]
+Token #109 STRING ['Hello, World!']
+Token #110 WS [\n]
diff --git a/ide/languages.env/test/unit/src/org/netbeans/modules/languages/env/EnvTestBase.java b/ide/languages.env/test/unit/src/org/netbeans/modules/languages/env/EnvTestBase.java
new file mode 100644
index 000000000000..5533db475fb7
--- /dev/null
+++ b/ide/languages.env/test/unit/src/org/netbeans/modules/languages/env/EnvTestBase.java
@@ -0,0 +1,51 @@
+/*
+ * 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.languages.env;
+
+import org.netbeans.lib.lexer.test.TestLanguageProvider;
+import org.netbeans.modules.csl.api.test.CslTestBase;
+import org.netbeans.modules.csl.spi.DefaultLanguageConfig;
+import org.openide.util.lookup.Lookups;
+import org.openide.util.test.MockLookup;
+
+public abstract class EnvTestBase extends CslTestBase {
+
+ public EnvTestBase(String name) {
+ super(name);
+ MockLookup.setLookup(Lookups.singleton(new TestLanguageProvider()));
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ MockLookup.init();
+ MockLookup.setInstances(
+ new TestLanguageProvider());
+ super.setUp();
+ }
+
+ @Override
+ protected DefaultLanguageConfig getPreferredLanguage() {
+ return new EnvLanguage();
+ }
+
+ @Override
+ protected String getPreferredMimeType() {
+ return EnvFileResolver.MIME_TYPE;
+ }
+}
diff --git a/ide/languages.env/test/unit/src/org/netbeans/modules/languages/env/EnvTestUtils.java b/ide/languages.env/test/unit/src/org/netbeans/modules/languages/env/EnvTestUtils.java
new file mode 100644
index 000000000000..15ccc22e7f51
--- /dev/null
+++ b/ide/languages.env/test/unit/src/org/netbeans/modules/languages/env/EnvTestUtils.java
@@ -0,0 +1,34 @@
+/*
+ * 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.languages.env;
+
+public class EnvTestUtils {
+
+ private EnvTestUtils() {
+
+ }
+
+ public static String replaceLinesAndTabs(String input) {
+ String escapedString = input;
+ escapedString = escapedString.replaceAll("\n", "\\\\n"); // NOI18N
+ escapedString = escapedString.replaceAll("\r", "\\\\r"); // NOI18N
+ escapedString = escapedString.replaceAll("\t", "\\\\t"); // NOI18N
+ return escapedString;
+ }
+}
diff --git a/ide/languages.env/test/unit/src/org/netbeans/modules/languages/env/grammar/antlr4/lexer/EnvAntlrLexerTestBase.java b/ide/languages.env/test/unit/src/org/netbeans/modules/languages/env/grammar/antlr4/lexer/EnvAntlrLexerTestBase.java
new file mode 100644
index 000000000000..56fb05137197
--- /dev/null
+++ b/ide/languages.env/test/unit/src/org/netbeans/modules/languages/env/grammar/antlr4/lexer/EnvAntlrLexerTestBase.java
@@ -0,0 +1,67 @@
+/*
+ * 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.languages.env.grammar.antlr4.lexer;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import org.antlr.v4.runtime.CharStream;
+import org.antlr.v4.runtime.CharStreams;
+import org.antlr.v4.runtime.Token;
+import org.antlr.v4.runtime.Vocabulary;
+import org.netbeans.modules.languages.env.EnvTestBase;
+import org.netbeans.modules.languages.env.EnvTestUtils;
+import org.netbeans.modules.languages.env.grammar.antlr4.parser.EnvAntlrLexer;
+
+public abstract class EnvAntlrLexerTestBase extends EnvTestBase {
+
+ public EnvAntlrLexerTestBase(String name) {
+ super(name);
+ }
+
+ public void checkLexer(final String filePath) throws Exception {
+ File testFile = new File(getDataDir(), filePath);
+ BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(testFile), StandardCharsets.UTF_8));
+
+ CharStream stream = CharStreams.fromReader(reader);
+ EnvAntlrLexer lexer = new EnvAntlrLexer(stream);
+ StringBuilder result = new StringBuilder();
+ Vocabulary vocabulary = lexer.getVocabulary();
+ for (Token token = lexer.nextToken(); token != null && token.getType() != Token.EOF; token = lexer.nextToken()) {
+ int type = token.getType();
+ result.append("Token #");
+ result.append(type);
+ result.append(" ");
+ result.append(vocabulary.getDisplayName(type));
+
+ String content = EnvTestUtils.replaceLinesAndTabs(token.getText());
+ if (!content.isEmpty()) {
+ result.append(" ");
+ result.append("[");
+ result.append(content);
+ result.append("]");
+ }
+ result.append("\n");
+ }
+
+ assertDescriptionMatches(filePath, result.toString(), false, ".lexer");
+ }
+}
diff --git a/ide/languages.env/test/unit/src/org/netbeans/modules/languages/env/grammar/antlr4/lexer/EnvLexerTest.java b/ide/languages.env/test/unit/src/org/netbeans/modules/languages/env/grammar/antlr4/lexer/EnvLexerTest.java
new file mode 100644
index 000000000000..a60049f5c8db
--- /dev/null
+++ b/ide/languages.env/test/unit/src/org/netbeans/modules/languages/env/grammar/antlr4/lexer/EnvLexerTest.java
@@ -0,0 +1,30 @@
+/*
+ * 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.languages.env.grammar.antlr4.lexer;
+
+public class EnvLexerTest extends EnvAntlrLexerTestBase {
+
+ public EnvLexerTest(String name) {
+ super(name);
+ }
+
+ public void testEnv01() throws Exception {
+ checkLexer("testfiles/antlr4/lexer/env01.env");
+ }
+}
diff --git a/ide/languages.env/test/unit/src/org/netbeans/modules/languages/env/lexer/EnvLexerTest.java b/ide/languages.env/test/unit/src/org/netbeans/modules/languages/env/lexer/EnvLexerTest.java
new file mode 100644
index 000000000000..1ed6036b5b8e
--- /dev/null
+++ b/ide/languages.env/test/unit/src/org/netbeans/modules/languages/env/lexer/EnvLexerTest.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.languages.env.lexer;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import static junit.framework.TestCase.assertNotNull;
+import org.netbeans.api.lexer.Language;
+import org.netbeans.api.lexer.TokenHierarchy;
+import org.netbeans.api.lexer.TokenSequence;
+import org.netbeans.lib.lexer.test.LexerTestUtilities;
+import org.netbeans.modules.languages.env.EnvLanguage;
+import org.netbeans.modules.languages.env.EnvTestBase;
+import org.netbeans.modules.languages.env.EnvTestUtils;
+
+public class EnvLexerTest extends EnvTestBase {
+
+ public EnvLexerTest(String name) {
+ super(name);
+ }
+
+ @Override
+ public void setUp() throws Exception {
+ LexerTestUtilities.setTesting(true);
+ }
+
+ public void testLexer_01() throws Exception {
+ checkLexer("testfiles/lexer/env01.env");
+ }
+
+ private void checkLexer(final String filePath) throws Exception {
+ String fileContent = Files.readString(new File(getDataDir(), filePath).toPath(), StandardCharsets.UTF_8);
+ EnvLanguage langSettings = new EnvLanguage();
+ Language language;
+ language = langSettings.getLexerLanguage();
+ TokenHierarchy> th = TokenHierarchy.create(fileContent, language);
+ TokenSequence extends EnvTokenId> ts = th.tokenSequence(language);
+ assertNotNull("Can not obtain token sequence for file: " + filePath, ts);
+ StringBuilder result = new StringBuilder();
+ while (ts.moveNext()) {
+ EnvTokenId tokenId = ts.token().id();
+ CharSequence tokenText = ts.token().text();
+ result.append("Token #");
+ result.append(ts.index());
+ result.append(" ");
+ result.append(tokenId.name());
+ String token = EnvTestUtils.replaceLinesAndTabs(tokenText.toString());
+ if (!token.isEmpty()) {
+ result.append(" ");
+ result.append("[");
+ result.append(token);
+ result.append("]");
+ }
+ result.append("\n");
+ }
+
+ assertDescriptionMatches(filePath, result.toString(), false, ".lexer");
+ }
+}
diff --git a/ide/web.common/nbproject/project.xml b/ide/web.common/nbproject/project.xml
index ef5d687077d1..ff37fe6b55b7 100644
--- a/ide/web.common/nbproject/project.xml
+++ b/ide/web.common/nbproject/project.xml
@@ -237,6 +237,7 @@
org.netbeans.modules.javascript2.nodejs
org.netbeans.modules.javascript2.requirejs
org.netbeans.modules.languages.apacheconf
+ org.netbeans.modules.languages.env
org.netbeans.modules.languages.ini
org.netbeans.modules.languages.neon
org.netbeans.modules.maven
diff --git a/nbbuild/cluster.properties b/nbbuild/cluster.properties
index 58775c1762c0..f4f3795bbc01 100644
--- a/nbbuild/cluster.properties
+++ b/nbbuild/cluster.properties
@@ -373,6 +373,7 @@ nb.cluster.ide=\
jumpto,\
languages,\
languages.diff,\
+ languages.env,\
languages.go,\
languages.hcl,\
languages.jflex,\