You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@netbeans.apache.org by db...@apache.org on 2022/05/20 06:47:20 UTC

[netbeans] branch master updated: LSP: Format Document and Format Selection actions implemented. (#4128)

This is an automated email from the ASF dual-hosted git repository.

dbalek pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/netbeans.git


The following commit(s) were added to refs/heads/master by this push:
     new bb9fd66fec LSP: Format Document and Format Selection actions implemented. (#4128)
bb9fd66fec is described below

commit bb9fd66fecacd71d8062527c86e77ee0ae0c7d78
Author: Dusan Balek <du...@oracle.com>
AuthorDate: Fri May 20 08:47:14 2022 +0200

    LSP: Format Document and Format Selection actions implemented. (#4128)
---
 groovy/groovy.editor/apichanges.xml                |  14 +
 groovy/groovy.editor/manifest.mf                   |   2 +-
 .../groovy/editor/api/lexer/LexUtilities.java      |  89 ++-
 .../groovy/editor/language/GroovyFormatter.java    | 191 +++--
 .../java/completion/JavaCompletionTask.java        |   7 +-
 .../netbeans/modules/java/lsp/server/Utils.java    |  12 +
 .../lsp/server/protocol/OptionsExportModel.java    | 769 +++++++++++++++++++++
 .../modules/java/lsp/server/protocol/Server.java   |  10 +-
 .../server/protocol/TextDocumentServiceImpl.java   | 266 ++++++-
 .../lsp/server/protocol/WorkspaceServiceImpl.java  |  41 +-
 .../java/lsp/server/protocol/ServerTest.java       | 163 +++++
 java/java.lsp.server/vscode/package.json           |   5 +
 java/java.lsp.server/vscode/src/extension.ts       |   5 +-
 .../modules/java/source/save/Reformatter.java      |   3 +
 14 files changed, 1465 insertions(+), 112 deletions(-)

diff --git a/groovy/groovy.editor/apichanges.xml b/groovy/groovy.editor/apichanges.xml
index 31187a7186..b730b8fa9a 100644
--- a/groovy/groovy.editor/apichanges.xml
+++ b/groovy/groovy.editor/apichanges.xml
@@ -84,6 +84,20 @@ is the proper place.
     <!-- ACTUAL CHANGES BEGIN HERE: -->
 
 <changes>
+    <change id="LexUtilities.LineDocument.methods">
+        <api name="groovy-parsing"/>
+        <summary>Variants of the exisitng methods taking LineDocument as argument added.</summary>
+        <version major="1" minor="85"/>
+        <date day="18" month="5" year="2022"/>
+        <author login="dbalek"/>
+        <compatibility addition="yes" binary="compatible" semantic="compatible" />
+        <description>
+            <p>
+                Variants of the exisitng methods taking LineDocument as argument added.
+            </p>
+        </description>
+        <class package="org.netbeans.modules.groovy.editor.api.lexer" name="LexUtilities"/>
+    </change>
     <change id="ASTPath.outer">
         <api name="groovy-parsing"/>
         <summary>Alternative construction for path more suitable for expressions, API to resolve types</summary>
diff --git a/groovy/groovy.editor/manifest.mf b/groovy/groovy.editor/manifest.mf
index 813047c4d5..44d57ad4fa 100644
--- a/groovy/groovy.editor/manifest.mf
+++ b/groovy/groovy.editor/manifest.mf
@@ -3,4 +3,4 @@ AutoUpdate-Show-In-Client: false
 OpenIDE-Module: org.netbeans.modules.groovy.editor/3
 OpenIDE-Module-Layer: org/netbeans/modules/groovy/editor/resources/layer.xml
 OpenIDE-Module-Localizing-Bundle: org/netbeans/modules/groovy/editor/Bundle.properties
-OpenIDE-Module-Specification-Version: 1.84
+OpenIDE-Module-Specification-Version: 1.85
diff --git a/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/api/lexer/LexUtilities.java b/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/api/lexer/LexUtilities.java
index 602f80079a..dc7aa95862 100644
--- a/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/api/lexer/LexUtilities.java
+++ b/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/api/lexer/LexUtilities.java
@@ -26,6 +26,7 @@ import java.util.Set;
 import javax.swing.text.BadLocationException;
 import javax.swing.text.Document;
 import org.netbeans.api.annotations.common.CheckForNull;
+import org.netbeans.api.editor.document.LineDocument;
 import org.netbeans.api.lexer.Token;
 import org.netbeans.api.lexer.TokenHierarchy;
 import org.netbeans.api.lexer.TokenId;
@@ -163,12 +164,16 @@ public final class LexUtilities {
     /** Find the Groovy token sequence (in case it's embedded in something else at the top level. */
     @SuppressWarnings("unchecked")
     public static TokenSequence<GroovyTokenId> getGroovyTokenSequence(Document doc, int offset) {
-        final BaseDocument baseDocument = (BaseDocument) doc;
+        final BaseDocument baseDocument = doc instanceof BaseDocument ? (BaseDocument) doc : null;
         try {
-            baseDocument.readLock();
+            if (baseDocument != null) {
+                baseDocument.readLock();
+            }
             return getGroovyTokenSequence(TokenHierarchy.get(doc), offset);
         } finally {
-            baseDocument.readUnlock();
+            if (baseDocument != null) {
+                baseDocument.readUnlock();
+            }
         }
     }
 
@@ -236,6 +241,10 @@ public final class LexUtilities {
         return getPositionedSequence(doc, offset, true);
     }
 
+    public static TokenSequence<GroovyTokenId> getPositionedSequence(LineDocument doc, int offset) {
+        return getPositionedSequence(doc, offset, true);
+    }
+
     public static TokenSequence<GroovyTokenId> getPositionedSequence(BaseDocument doc, int offset, boolean lookBack) {
         TokenSequence<GroovyTokenId> ts = getGroovyTokenSequence(doc, offset);
 
@@ -264,6 +273,34 @@ public final class LexUtilities {
         return null;
     }
 
+    public static TokenSequence<GroovyTokenId> getPositionedSequence(LineDocument doc, int offset, boolean lookBack) {
+        TokenSequence<GroovyTokenId> ts = getGroovyTokenSequence(doc, offset);
+
+        if (ts != null) {
+            try {
+                ts.move(offset);
+            } catch (AssertionError e) {
+                DataObject dobj = (DataObject) doc.getProperty(Document.StreamDescriptionProperty);
+
+                if (dobj != null) {
+                    Exceptions.attachMessage(e, FileUtil.getFileDisplayName(dobj.getPrimaryFile()));
+                }
+
+                throw e;
+            }
+
+            if (!lookBack && !ts.moveNext()) {
+                return null;
+            } else if (lookBack && !ts.moveNext() && !ts.movePrevious()) {
+                return null;
+            }
+
+            return ts;
+        }
+
+        return null;
+    }
+
     public static Token<GroovyTokenId> getToken(BaseDocument doc, int offset) {
         TokenSequence<GroovyTokenId> ts = getGroovyTokenSequence(doc, offset);
 
@@ -292,6 +329,34 @@ public final class LexUtilities {
         return null;
     }
 
+    public static Token<GroovyTokenId> getToken(LineDocument doc, int offset) {
+        TokenSequence<GroovyTokenId> ts = getGroovyTokenSequence(doc, offset);
+
+        if (ts != null) {
+            try {
+                ts.move(offset);
+            } catch (AssertionError e) {
+                DataObject dobj = (DataObject) doc.getProperty(Document.StreamDescriptionProperty);
+
+                if (dobj != null) {
+                    Exceptions.attachMessage(e, FileUtil.getFileDisplayName(dobj.getPrimaryFile()));
+                }
+
+                throw e;
+            }
+
+            if (!ts.moveNext() && !ts.movePrevious()) {
+                return null;
+            }
+
+            Token<GroovyTokenId> token = ts.token();
+
+            return token;
+        }
+
+        return null;
+    }
+
     public static char getTokenChar(BaseDocument doc, int offset) {
         Token<GroovyTokenId> token = getToken(doc, offset);
 
@@ -446,6 +511,15 @@ public final class LexUtilities {
         return END_PAIRS.contains(id);
     }
 
+    /**
+     * Return true iff the given token is a token that should be matched
+     * with a corresponding "end" token, such as "begin", "def", "module",
+     * etc.
+     */
+    public static boolean isBeginToken(TokenId id, LineDocument doc, int offset) {
+        return END_PAIRS.contains(id);
+    }
+
     /**
      * Return true iff the given token is a token that should be matched
      * with a corresponding "end" token, such as "begin", "def", "module",
@@ -455,6 +529,15 @@ public final class LexUtilities {
         return END_PAIRS.contains(id);
     }
 
+    /**
+     * Return true iff the given token is a token that should be matched
+     * with a corresponding "end" token, such as "begin", "def", "module",
+     * etc.
+     */
+    public static boolean isBeginToken(TokenId id, LineDocument doc, TokenSequence<GroovyTokenId> ts) {
+        return END_PAIRS.contains(id);
+    }
+
     /**
      * Return true iff the given token is a token that indents its content,
      * such as the various begin tokens as well as "else", "when", etc.
diff --git a/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/language/GroovyFormatter.java b/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/language/GroovyFormatter.java
index 1c4915acf1..95a66c220a 100644
--- a/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/language/GroovyFormatter.java
+++ b/groovy/groovy.editor/src/org/netbeans/modules/groovy/editor/language/GroovyFormatter.java
@@ -22,13 +22,13 @@ import java.util.ArrayList;
 import java.util.List;
 import javax.swing.text.BadLocationException;
 import javax.swing.text.Document;
+import org.netbeans.api.editor.document.AtomicLockDocument;
+import org.netbeans.api.editor.document.LineDocument;
+import org.netbeans.api.editor.document.LineDocumentUtils;
 import org.netbeans.api.lexer.Token;
 import org.netbeans.api.lexer.TokenId;
 import org.netbeans.api.lexer.TokenSequence;
-import org.netbeans.editor.BaseDocument;
-import org.netbeans.editor.Utilities;
 import org.netbeans.modules.csl.api.Formatter;
-import org.netbeans.modules.csl.api.OffsetRange;
 import org.netbeans.modules.csl.spi.GsfUtilities;
 import org.netbeans.modules.csl.spi.ParserResult;
 import org.netbeans.modules.editor.indent.api.IndentUtils;
@@ -77,7 +77,7 @@ public class GroovyFormatter implements Formatter {
     }
 
     /** Compute the initial balance of brackets at the given offset. */
-    private int getFormatStableStart(BaseDocument doc, int offset) {
+    private int getFormatStableStart(Document doc, int offset) {
         TokenSequence<GroovyTokenId> ts = LexUtilities.getGroovyTokenSequence(doc, offset);
         if (ts == null) {
             return 0;
@@ -104,7 +104,7 @@ public class GroovyFormatter implements Formatter {
     }
 
     private int getTokenBalanceDelta(TokenId id, Token<GroovyTokenId> token,
-            BaseDocument doc, TokenSequence<GroovyTokenId> ts, boolean includeKeywords) {
+            LineDocument doc, TokenSequence<GroovyTokenId> ts, boolean includeKeywords) {
         if (id == GroovyTokenId.IDENTIFIER) {
             // In some cases, the [ shows up as an identifier, for example in this expression:
             //  for k, v in sort{|a1, a2| a1[0].id2name <=> a2[0].id2name}
@@ -134,7 +134,7 @@ public class GroovyFormatter implements Formatter {
     }
 
     // TODO RHTML - there can be many discontiguous sections, I've gotta process all of them on the given line
-    private int getTokenBalance(BaseDocument doc, int begin, int end, boolean includeKeywords) {
+    private int getTokenBalance(LineDocument doc, int begin, int end, boolean includeKeywords) {
         int balance = 0;
 
         TokenSequence<GroovyTokenId> ts = LexUtilities.getGroovyTokenSequence(doc, begin);
@@ -159,8 +159,8 @@ public class GroovyFormatter implements Formatter {
     }
 
     // This method will indent lines beginning with * by 1 space
-    private boolean isJavaDocComment(BaseDocument doc, int offset, int endOfLine) throws BadLocationException {
-        int pos = Utilities.getRowFirstNonWhite(doc, offset);
+    private boolean isJavaDocComment(LineDocument doc, int offset, int endOfLine) throws BadLocationException {
+        int pos = LineDocumentUtils.getLineFirstNonWhitespace(doc, offset);
         if (pos != -1) {
             Token<GroovyTokenId> token = LexUtilities.getToken(doc, pos);
             if (token != null) {
@@ -176,7 +176,7 @@ public class GroovyFormatter implements Formatter {
         return false;
     }
 
-    private boolean isInLiteral(BaseDocument doc, int offset) throws BadLocationException {
+    private boolean isInLiteral(LineDocument doc, int offset) throws BadLocationException {
         // TODO: Handle arrays better
         // %w(January February March April May June July
         //    August September October November December)
@@ -185,7 +185,7 @@ public class GroovyFormatter implements Formatter {
         // Can't reformat these at the moment because reindenting a line
         // that is a continued string array causes incremental lexing errors
         // (which further screw up formatting)
-        int pos = Utilities.getRowFirstNonWhite(doc, offset);
+        int pos = LineDocumentUtils.getLineFirstNonWhitespace(doc, offset);
         //int pos = offset;
 
         if (pos != -1) {
@@ -226,8 +226,8 @@ public class GroovyFormatter implements Formatter {
         return false;
     }
 
-    private boolean isEndIndent(BaseDocument doc, int offset) throws BadLocationException {
-        int lineBegin = Utilities.getRowFirstNonWhite(doc, offset);
+    private boolean isEndIndent(LineDocument doc, int offset) throws BadLocationException {
+        int lineBegin = LineDocumentUtils.getLineFirstNonWhitespace(doc, offset);
 
         if (lineBegin != -1) {
             Token<GroovyTokenId> token = LexUtilities.getToken(doc, lineBegin);
@@ -248,8 +248,8 @@ public class GroovyFormatter implements Formatter {
         return false;
     }
 
-    private boolean isLineContinued(BaseDocument doc, int offset, int bracketBalance) throws BadLocationException {
-        offset = Utilities.getRowLastNonWhite(doc, offset);
+    private boolean isLineContinued(LineDocument doc, int offset, int bracketBalance) throws BadLocationException {
+        offset = LineDocumentUtils.getLineLastNonWhitespace(doc, offset);
         if (offset == -1) {
             return false;
         }
@@ -296,7 +296,7 @@ public class GroovyFormatter implements Formatter {
                 //    alias eql? ==
                 // or
                 //    def ==
-                token = LexUtilities.getToken(doc, Utilities.getRowFirstNonWhite(doc, offset));
+                token = LexUtilities.getToken(doc, LineDocumentUtils.getLineFirstNonWhitespace(doc, offset));
                 if (token != null) {
                     id = token.id();
                     if (id == GroovyTokenId.LBRACE) {
@@ -315,97 +315,94 @@ public class GroovyFormatter implements Formatter {
         Document document = context.document();
         final int endOffset = Math.min(context.endOffset(), document.getLength());
 
-        try {
-            final BaseDocument doc = (BaseDocument) document;
-
-            final int startOffset = Utilities.getRowStart(doc, context.startOffset());
-            final int lineStart = startOffset;
-            int initialOffset = 0;
-            int initialIndent = 0;
-            if (startOffset > 0) {
-                int prevOffset = Utilities.getRowStart(doc, startOffset - 1);
-                initialOffset = getFormatStableStart(doc, prevOffset);
-                initialIndent = GsfUtilities.getLineIndent(doc, initialOffset);
-            }
+        final LineDocument doc = (LineDocument) document;
 
-            // Build up a set of offsets and indents for lines where I know I need
-            // to adjust the offset. I will then go back over the document and adjust
-            // lines that are different from the intended indent. By doing piecemeal
-            // replacements in the document rather than replacing the whole thing,
-            // a lot of things will work better: breakpoints and other line annotations
-            // will be left in place, semantic coloring info will not be temporarily
-            // damaged, and the caret will stay roughly where it belongs.
-            final List<Integer> offsets = new ArrayList<Integer>();
-            final List<Integer> indents = new ArrayList<Integer>();
-
-            // When we're formatting sections, include whitespace on empty lines; this
-            // is used during live code template insertions for example. However, when
-            // wholesale formatting a whole document, leave these lines alone.
-            boolean indentEmptyLines = (startOffset != 0 || endOffset != doc.getLength());
-
-            boolean includeEnd = endOffset == doc.getLength() || indentOnly;
-
-            // TODO - remove initialbalance etc.
-            computeIndents(doc, initialIndent, initialOffset, endOffset, info, offsets, indents, indentEmptyLines, includeEnd, indentOnly);
-
-            doc.runAtomic(new Runnable() {
-                @Override
-                public void run() {
-                    try {
-                        // Iterate in reverse order such that offsets are not affected by our edits
-                        assert indents.size() == offsets.size();
-                        for (int i = indents.size() - 1; i >= 0; i--) {
-                            int indent = indents.get(i);
-                            int lineBegin = offsets.get(i);
-
-                            if (lineBegin < lineStart) {
-                                // We're now outside the region that the user wanted reformatting;
-                                // these offsets were computed to get the correct continuation context etc.
-                                // for the formatter
-                                break;
-                            }
+        final int startOffset = LineDocumentUtils.getLineStart(doc, context.startOffset());
+        final int lineStart = startOffset;
+        int initialOffset = 0;
+        int initialIndent = 0;
+        if (startOffset > 0) {
+            int prevOffset = LineDocumentUtils.getLineStart(doc, startOffset - 1);
+            initialOffset = getFormatStableStart(doc, prevOffset);
+            initialIndent = GsfUtilities.getLineIndent(doc, initialOffset);
+        }
+
+        // Build up a set of offsets and indents for lines where I know I need
+        // to adjust the offset. I will then go back over the document and adjust
+        // lines that are different from the intended indent. By doing piecemeal
+        // replacements in the document rather than replacing the whole thing,
+        // a lot of things will work better: breakpoints and other line annotations
+        // will be left in place, semantic coloring info will not be temporarily
+        // damaged, and the caret will stay roughly where it belongs.
+        final List<Integer> offsets = new ArrayList<Integer>();
+        final List<Integer> indents = new ArrayList<Integer>();
+
+        // When we're formatting sections, include whitespace on empty lines; this
+        // is used during live code template insertions for example. However, when
+        // wholesale formatting a whole document, leave these lines alone.
+        boolean indentEmptyLines = (startOffset != 0 || endOffset != doc.getLength());
+
+        boolean includeEnd = endOffset == doc.getLength() || indentOnly;
+
+        // TODO - remove initialbalance etc.
+        computeIndents(doc, initialIndent, initialOffset, endOffset, info, offsets, indents, indentEmptyLines, includeEnd, indentOnly);
+
+        AtomicLockDocument bdoc = LineDocumentUtils.as(doc, AtomicLockDocument.class);
+        bdoc.runAtomic(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    // Iterate in reverse order such that offsets are not affected by our edits
+                    assert indents.size() == offsets.size();
+                    for (int i = indents.size() - 1; i >= 0; i--) {
+                        int indent = indents.get(i);
+                        int lineBegin = offsets.get(i);
+
+                        if (lineBegin < lineStart) {
+                            // We're now outside the region that the user wanted reformatting;
+                            // these offsets were computed to get the correct continuation context etc.
+                            // for the formatter
+                            break;
+                        }
 
-                            if (lineBegin == lineStart && i > 0) {
-                                // Look at the previous line, and see how it's indented
-                                // in the buffer.  If it differs from the computed position,
-                                // offset my computed position (thus, I'm only going to adjust
-                                // the new line position relative to the existing editing.
-                                // This avoids the situation where you're inserting a newline
-                                // in the middle of "incorrectly" indented code (e.g. different
-                                // size than the IDE is using) and the newline position ending
-                                // up "out of sync"
-                                int prevOffset = offsets.get(i - 1);
-                                int prevIndent = indents.get(i - 1);
-                                int actualPrevIndent = GsfUtilities.getLineIndent(doc, prevOffset);
-                                if (actualPrevIndent != prevIndent) {
-                                    // For blank lines, indentation may be 0, so don't adjust in that case
-                                    if (!(Utilities.isRowEmpty(doc, prevOffset) || Utilities.isRowWhite(doc, prevOffset))) {
-                                        indent = actualPrevIndent + (indent - prevIndent);
-                                        if (indent < 0) {
-                                            indent = 0;
-                                        }
+                        if (lineBegin == lineStart && i > 0) {
+                            // Look at the previous line, and see how it's indented
+                            // in the buffer.  If it differs from the computed position,
+                            // offset my computed position (thus, I'm only going to adjust
+                            // the new line position relative to the existing editing.
+                            // This avoids the situation where you're inserting a newline
+                            // in the middle of "incorrectly" indented code (e.g. different
+                            // size than the IDE is using) and the newline position ending
+                            // up "out of sync"
+                            int prevOffset = offsets.get(i - 1);
+                            int prevIndent = indents.get(i - 1);
+                            int actualPrevIndent = GsfUtilities.getLineIndent(doc, prevOffset);
+                            if (actualPrevIndent != prevIndent) {
+                                // For blank lines, indentation may be 0, so don't adjust in that case
+                                if (!(LineDocumentUtils.isLineEmpty(doc, prevOffset) || LineDocumentUtils.isLineWhitespace(doc, prevOffset))) {
+                                    indent = actualPrevIndent + (indent - prevIndent);
+                                    if (indent < 0) {
+                                        indent = 0;
                                     }
                                 }
                             }
+                        }
 
-                            // Adjust the indent at the given line (specified by offset) to the given indent
-                            int currentIndent = GsfUtilities.getLineIndent(doc, lineBegin);
+                        // Adjust the indent at the given line (specified by offset) to the given indent
+                        int currentIndent = GsfUtilities.getLineIndent(doc, lineBegin);
 
-                            if (currentIndent != indent) {
-                                context.modifyIndent(lineBegin, indent);
-                            }
+                        if (currentIndent != indent) {
+                            context.modifyIndent(lineBegin, indent);
                         }
-                    } catch (BadLocationException ble) {
-                        Exceptions.printStackTrace(ble);
                     }
+                } catch (BadLocationException ble) {
+                    Exceptions.printStackTrace(ble);
                 }
-            });
-        } catch (BadLocationException ble) {
-            Exceptions.printStackTrace(ble);
-        }
+            }
+        });
     }
 
-    private void computeIndents(BaseDocument doc, int initialIndent, int startOffset, int endOffset, ParserResult info,
+    private void computeIndents(LineDocument doc, int initialIndent, int startOffset, int endOffset, ParserResult info,
             List<Integer> offsets,
             List<Integer> indents,
             boolean indentEmptyLines, boolean includeEnd, boolean indentOnly
@@ -429,7 +426,7 @@ public class GroovyFormatter implements Formatter {
             // This can be used either to reformat the buffer, or indent a new line.
 
             // State:
-            int offset = Utilities.getRowStart(doc, startOffset); // The line's offset
+            int offset = LineDocumentUtils.getLineStart(doc, startOffset); // The line's offset
             int end = endOffset;
 
             int indentSize = IndentUtils.indentLevelSize(doc);
@@ -466,7 +463,7 @@ public class GroovyFormatter implements Formatter {
                     indent = balance * indentSize + hangingIndent + initialIndent;
                 }
 
-                int endOfLine = Utilities.getRowEnd(doc, offset) + 1;
+                int endOfLine = LineDocumentUtils.getLineEnd(doc, offset) + 1;
 
                 if (isJavaDocComment(doc, offset, endOfLine)) {
                     indent++;
@@ -476,7 +473,7 @@ public class GroovyFormatter implements Formatter {
                     indent = 0;
                 }
 
-                int lineBegin = Utilities.getRowFirstNonWhite(doc, offset);
+                int lineBegin = LineDocumentUtils.getLineFirstNonWhitespace(doc, offset);
 
                 // Insert whitespace on empty lines too -- needed for abbreviations expansion
                 if (lineBegin != -1 || indentEmptyLines) {
diff --git a/java/java.completion/src/org/netbeans/modules/java/completion/JavaCompletionTask.java b/java/java.completion/src/org/netbeans/modules/java/completion/JavaCompletionTask.java
index 55ab99bd7c..cbc0350e5e 100644
--- a/java/java.completion/src/org/netbeans/modules/java/completion/JavaCompletionTask.java
+++ b/java/java.completion/src/org/netbeans/modules/java/completion/JavaCompletionTask.java
@@ -4193,10 +4193,9 @@ public final class JavaCompletionTask<T> extends BaseTask {
                 while ((idx = qName.lastIndexOf('.')) > 0) {
                     if (sName == null) {
                         sName = qName.substring(idx + 1);
-                        if (sName.length() <= 0 || !startsWith(env, sName, prefix)) {
-                            break;
+                        if (sName.length() > 0 && startsWith(env, sName, prefix)) {
+                            results.add(itemFactory.createTypeItem(name, kinds, anchorOffset, env.getReferencesCount(), controller.getSnapshot().getSource(), env.isInsideNew(), env.isInsideNew() || env.isInsideClass(), env.isAfterExtends()));
                         }
-                        results.add(itemFactory.createTypeItem(name, kinds, anchorOffset, env.getReferencesCount(), controller.getSnapshot().getSource(), env.isInsideNew(), env.isInsideNew() || env.isInsideClass(), env.isAfterExtends()));
                     }
                     qName = qName.substring(0, idx);
                     doNotRemove.add(qName);
@@ -4218,7 +4217,7 @@ public final class JavaCompletionTask<T> extends BaseTask {
             Set<ElementHandle<TypeElement>> declaredTypes = controller.getClasspathInfo().getClassIndex().getDeclaredTypes(subwordsPattern != null ? subwordsPattern : prefix != null ? prefix : EMPTY, kind, EnumSet.allOf(ClassIndex.SearchScope.class));
             results.ensureCapacity(results.size() + declaredTypes.size());
             for (ElementHandle<TypeElement> name : declaredTypes) {
-                if (excludeHandles != null && excludeHandles.contains(name) || isAnnonInner(name)) {
+                if (!kinds.contains(name.getKind()) || excludeHandles != null && excludeHandles.contains(name) || isAnnonInner(name)) {
                     continue;
                 }
                 results.add(itemFactory.createTypeItem(name, kinds, anchorOffset, env.getReferencesCount(), controller.getSnapshot().getSource(), env.isInsideNew(), env.isInsideNew() || env.isInsideClass(), env.isAfterExtends()));
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/Utils.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/Utils.java
index 0b1afccd52..b05ab5d9b6 100644
--- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/Utils.java
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/Utils.java
@@ -44,6 +44,7 @@ import javax.lang.model.element.VariableElement;
 import javax.lang.model.type.ArrayType;
 import javax.lang.model.type.TypeKind;
 import javax.lang.model.type.TypeMirror;
+import javax.swing.text.BadLocationException;
 import javax.swing.text.StyledDocument;
 import org.eclipse.lsp4j.Position;
 import org.eclipse.lsp4j.Range;
@@ -273,6 +274,17 @@ public class Utils {
         }
     }
 
+    public static Position createPosition(LineDocument doc, int offset) {
+        try {
+            int line = LineDocumentUtils.getLineIndex(doc, offset);
+            int column = offset - LineDocumentUtils.getLineStart(doc, offset);
+
+            return new Position(line, column);
+        } catch (BadLocationException ex) {
+            throw new IllegalStateException(ex);
+        }
+    }
+
     public static int getOffset(LineDocument doc, Position pos) {
         return LineDocumentUtils.getLineStartFromIndex(doc, pos.getLine()) + pos.getCharacter();
     }
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/OptionsExportModel.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/OptionsExportModel.java
new file mode 100644
index 0000000000..82d62bf543
--- /dev/null
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/OptionsExportModel.java
@@ -0,0 +1,769 @@
+/*
+ * 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.java.lsp.server.protocol;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.SyncFailedException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+import org.openide.filesystems.*;
+import org.openide.modules.Places;
+import org.openide.util.EditableProperties;
+import org.openide.util.Exceptions;
+import org.openide.util.NbBundle;
+
+final class OptionsExportModel {
+
+    private static final Logger LOGGER = Logger.getLogger(OptionsExportModel.class.getName());
+    /** Folder in layer file system where provider are searched for. */
+    private static final String OPTIONS_EXPORT_FOLDER = "OptionsExport"; //NOI18N
+    /** Pattern used to get names of option profiles. */
+    private static final String GROUP_PATTERN = "([^/]*)";  //NOI18N
+    private static final List<String> ENABLED_CATEGORIES = Collections.singletonList("Formatting"); //NOI18N
+
+    private static OptionsExportModel SINGLETON = new OptionsExportModel();
+
+    /** Target userdir for import. */
+    private final File targetUserdir = Places.getUserDirectory();
+    /** Source of export/import (zip file or userdir). */
+    private File source;
+    /** List of categories. */
+    private List<Category> categories;
+    /** Cache of paths relative to source root. */
+    List<String> relativePaths;
+    /** Include patterns. */
+    private Set<String> includePatterns;
+    /** Exclude patterns. */
+    private Set<String> excludePatterns;
+    /** Properties currently being copied. */
+    private EditableProperties currentProperties;
+    /** List of ignored folders in userdir. It speeds up folder scanning. */
+    private static final List<String> IGNORED_FOLDERS = Arrays.asList("var/cache");  // NOI18N
+
+    /** Returns instance of export options model.
+     * @param source source of export/import. It is either zip file or userdir
+     * @return instance of export options model
+     */
+    private OptionsExportModel() {
+    }
+
+    static OptionsExportModel get() {
+        return SINGLETON;
+    }
+
+    void doImport(File source) throws IOException {
+        LOGGER.log(Level.FINE, "Copying from: {0}\n    to: {1}", new Object[]{source, targetUserdir});  //NOI18N
+        this.source = source;
+        this.relativePaths = null;
+        try (ZipFile zipFile = new ZipFile(source)) {
+            // Enumerate each entry
+            Enumeration<? extends ZipEntry> entries = zipFile.entries();
+            while (entries.hasMoreElements()) {
+                ZipEntry zipEntry = entries.nextElement();
+                if (!zipEntry.isDirectory()) {
+                    copyFile(zipEntry.getName());
+                }
+            }
+        }
+    }
+
+    void clean() throws IOException {
+        this.source = null;
+        this.relativePaths = null;
+        for (String relativePath : getRelativePaths()) {
+            clearFile(relativePath);
+        }
+    }
+
+    private List<Category> getCategories() {
+        if (categories == null) {
+            loadCategories();
+        }
+        return categories;
+    }
+
+    /** Copies files from source (zip file or userdir) to target dir according
+     * to current state of model, i.e. only include/exclude patterns from
+     * enabled items are considered.
+     * @param targetUserdir target userdir
+     */
+    private static enum ParserState {
+
+        START,
+        IN_KEY_PATTERN,
+        AFTER_KEY_PATTERN,
+        IN_BLOCK
+    }
+
+    /** Parses given compound string pattern into set of single patterns.
+     * @param pattern compound pattern in form filePattern1#keyPattern1#|filePattern2#keyPattern2#|filePattern3
+     * @return set of single patterns containing just one # (e.g. [filePattern1#keyPattern1, filePattern2#keyPattern2, filePattern3])
+     */
+    static Set<String> parsePattern(String pattern) {
+        Set<String> patterns = new HashSet<String>();
+        if (pattern.contains("#")) {  //NOI18N
+            StringBuilder partPattern = new StringBuilder();
+            ParserState state = ParserState.START;
+            int blockLevel = 0;
+            for (int i = 0; i < pattern.length(); i++) {
+                char c = pattern.charAt(i);
+                switch(state) {
+                    case START:
+                        if (c == '#') {
+                            state = ParserState.IN_KEY_PATTERN;
+                            partPattern.append(c);
+                        } else if (c == '(') {
+                            state = ParserState.IN_BLOCK;
+                            blockLevel++;
+                            partPattern.append(c);
+                        } else if (c == '|') {
+                            patterns.add(partPattern.toString());
+                            partPattern = new StringBuilder();
+                        } else {
+                            partPattern.append(c);
+                        }
+                        break;
+                    case IN_KEY_PATTERN:
+                        if (c == '#') {
+                            state = ParserState.AFTER_KEY_PATTERN;
+                        } else {
+                            partPattern.append(c);
+                        }
+                        break;
+                    case AFTER_KEY_PATTERN:
+                        if (c == '|') {
+                            state = ParserState.START;
+                            patterns.add(partPattern.toString());
+                            partPattern = new StringBuilder();
+                        } else {
+                            assert false : "Wrong OptionsExport pattern " + pattern + ". Only format like filePattern1#keyPattern#|filePattern2 is supported.";  //NOI18N
+                        }
+                        break;
+                    case IN_BLOCK:
+                        partPattern.append(c);
+                        if (c == ')') {
+                            blockLevel--;
+                            if (blockLevel == 0) {
+                                state = ParserState.START;
+                            }
+                        }
+                        break;
+                }
+            }
+            patterns.add(partPattern.toString());
+        } else {
+            patterns.add(pattern);
+        }
+        return patterns;
+    }
+
+    /** Returns set of include patterns. */
+    private synchronized Set<String> getIncludePatterns() {
+        if (includePatterns == null) {
+            Set<String> patterns = new HashSet<>();
+            for (OptionsExportModel.Category category : getCategories()) {
+                for (OptionsExportModel.Item item : category.getItems()) {
+                    if (item.isEnabled()) {
+                        String include = item.getInclude();
+                        if (include != null && include.length() > 0) {
+                            patterns.addAll(parsePattern(include));
+                        }
+                    }
+                }
+            }
+            includePatterns = patterns;
+        }
+        return includePatterns;
+    }
+
+    /** Returns set of exclude patterns. */
+    private synchronized Set<String> getExcludePatterns() {
+        if (excludePatterns == null) {
+            Set<String> patterns = new HashSet<>();
+            for (OptionsExportModel.Category category : getCategories()) {
+                for (OptionsExportModel.Item item : category.getItems()) {
+                    if (item.isEnabled()) {
+                        String exclude = item.getExclude();
+                        if (exclude != null && exclude.length() > 0) {
+                            patterns.addAll(parsePattern(exclude));
+                        }
+                    }
+                }
+            }
+            excludePatterns = patterns;
+        }
+        return excludePatterns;
+    }
+
+    /** Represents one item and hold include/exclude patterns. */
+    private class Item {
+
+        private final String include;
+        private final String exclude;
+        private boolean enabled = false;
+
+        private Item(String include, String exclude) {
+            this.include = include;
+            this.exclude = exclude;
+            assert assertIgnoredFolders(include);
+        }
+
+        private String getInclude() {
+            return include;
+        }
+
+        private String getExclude() {
+            return exclude;
+        }
+
+        private boolean isEnabled() {
+            return enabled;
+        }
+
+        private void setEnabled(boolean newState) {
+            if (enabled != newState) {
+                enabled = newState;
+                // reset cached patterns
+                includePatterns = null;
+                excludePatterns = null;
+            }
+        }
+
+        /** Check that IGNORED_FOLDERS doesn't contain given pattern. */
+        private boolean assertIgnoredFolders(String pattern) {
+            boolean result = true;
+            for (String folder : IGNORED_FOLDERS) {
+                assert result = !pattern.contains(folder) : "Pattern " + pattern + " matches ignored folder " + folder;
+            }
+            return result;
+        }
+    }
+
+    /** Represents category holding several items. */
+    private class Category {
+
+        //xml entry names
+        private static final String INCLUDE = "include"; // NOI18N
+        private static final String EXCLUDE = "exclude"; // NOI18N
+        private final FileObject categoryFO;
+        private List<Item> items;
+
+        private Category(FileObject fo) {
+            this.categoryFO = fo;
+        }
+
+        private void addItem(String includes, String excludes) {
+            items.add(new Item(includes, excludes));
+        }
+
+        private void resolveGroups(String include, String exclude) {
+            LOGGER.log(Level.FINE, "resolveGroups include={0}", include);  //NOI18N
+            List<String> applicablePaths = getApplicablePaths(
+                    Collections.singleton(include),
+                    Collections.singleton(exclude));
+            Set<String> groups = new HashSet<>();
+            Pattern p = Pattern.compile(include);
+            for (String path : applicablePaths) {
+                Matcher m = p.matcher(path);
+                m.matches();
+                if (m.groupCount() == 1) {
+                    String group = m.group(1);
+                    if (group != null) {
+                        groups.add(group);
+                    }
+                }
+            }
+            LOGGER.log(Level.FINE, "GROUPS={0}", groups);  //NOI18N
+            for (String group : groups) {
+                // add additional items according to groups
+                addItem(include.replace(GROUP_PATTERN, group), exclude);
+            }
+        }
+
+        private List<Item> getItems() {
+            if (items == null) {
+                items = Collections.synchronizedList(new ArrayList<>());
+                FileObject[] itemsFOs = categoryFO.getChildren();
+                // respect ordering defined in layers
+                List<FileObject> sortedItems = FileUtil.getOrder(Arrays.asList(itemsFOs), false);
+                itemsFOs = sortedItems.toArray(new FileObject[0]);
+                for (FileObject itemFO : itemsFOs) {
+                    String include = (String) itemFO.getAttribute(INCLUDE);
+                    if (include == null) {
+                        include = "";  //NOI18N
+                    }
+                    String exclude = (String) itemFO.getAttribute(EXCLUDE);
+                    if (exclude == null) {
+                        exclude = "";  //NOI18N
+                    }
+                    if (include.contains(GROUP_PATTERN)) {
+                        resolveGroups(include, exclude);
+                    } else {
+                        addItem(include, exclude);
+                    }
+                }
+            }
+            return items;
+        }
+
+        private String getName() {
+            return categoryFO.getNameExt();
+        }
+
+        private void setEnabled(boolean enabled) {
+            for (Item item : getItems()) {
+                item.setEnabled(enabled);
+            }
+        }
+    } // end of Category
+
+    /** Load categories from filesystem. */
+    private void loadCategories() {
+        FileObject[] categoryFOs = FileUtil.getConfigFile(OPTIONS_EXPORT_FOLDER).getChildren();
+        // respect ordering defined in layers
+        List<FileObject> sortedCats = FileUtil.getOrder(Arrays.asList(categoryFOs), false);
+        categories = new ArrayList<>(sortedCats.size());
+        for (FileObject curFO : sortedCats) {
+            Category category = new Category(curFO);
+            if (ENABLED_CATEGORIES.contains(category.getName())) {
+                category.setEnabled(true);
+            }
+            categories.add(category);
+        }
+    }
+
+    /** Filters relative paths of current source and returns only ones which match given
+     * include/exclude patterns.
+     * @param includePatterns include patterns
+     * @param excludePatterns exclude patterns
+     * @return relative patsh which match include/exclude patterns
+     */
+    private List<String> getApplicablePaths(Set<String> includePatterns, Set<String> excludePatterns) {
+        List<String> applicablePaths = new ArrayList<>();
+        for (String relativePath : getRelativePaths()) {
+            if (matches(relativePath, includePatterns, excludePatterns)) {
+                applicablePaths.add(relativePath);
+            }
+        }
+        return applicablePaths;
+    }
+
+    private List<String> getRelativePaths() {
+        if (relativePaths == null) {
+            if (source != null && source.isFile()) {
+                try {
+                    // zip file
+                    relativePaths = listZipFile(source);
+                } catch (IOException ex) {
+                    Exceptions.attachLocalizedMessage(ex, NbBundle.getMessage(OptionsExportModel.class, "OptionsExportModel.invalid.zipfile", source));
+                    Exceptions.printStackTrace(ex);
+                    relativePaths = Collections.emptyList();
+                }
+            } else {
+                // userdir
+                File root = FileUtil.toFile(FileUtil.getConfigRoot());
+                relativePaths = getRelativePaths(Places.getUserDirectory());
+            }
+            LOGGER.fine("relativePaths=" + relativePaths);  //NOI18N
+        }
+        return relativePaths;
+    }
+
+    /** Returns list of file path relative to given source root. It scans
+     * sub folders recursively.
+     * @param sourceRoot source root
+     * @return list of file path relative to given source root
+     */
+    private static List<String> getRelativePaths(File sourceRoot) {
+        return getRelativePaths(sourceRoot, sourceRoot);
+    }
+
+    private static List<String> getRelativePaths(File root, File file) {
+        String relativePath = getRelativePath(root, file);
+        List<String> result = new ArrayList<>();
+        if (file.isDirectory()) {
+            if (IGNORED_FOLDERS.contains(relativePath)) {
+                return result;
+            }
+            File[] children = file.listFiles();
+            if (children == null) {
+                return Collections.emptyList();
+            }
+            for (File child : children) {
+                result.addAll(getRelativePaths(root, child));
+            }
+        } else {
+            result.add(relativePath);
+        }
+        return result;
+    }
+
+    /** Returns slash separated path relative to given root. */
+    private static String getRelativePath(File root, File file) {
+        String result = file.getAbsolutePath().substring(root.getAbsolutePath().length());
+        result = result.replace('\\', '/');  //NOI18N
+        if (result.startsWith("/") && !result.startsWith("//")) {  //NOI18N
+            result = result.substring(1);
+        }
+        return result;
+    }
+
+    /** Returns true if given relative path matches at least one of given include
+     * patterns and doesn't match all exclude patterns.
+     * @param relativePath relative path
+     * @param includePatterns include patterns
+     * @param excludePatterns exclude patterns
+     * @return true if given relative path matches at least one of given include
+     * patterns and doesn't match all exclude patterns, false otherwise
+     */
+    private static boolean matches(String relativePath, Set<String> includePatterns, Set<String> excludePatterns) {
+        boolean include = false;
+        for (String pattern : includePatterns) {
+            if (matches(relativePath, pattern)) {
+                include = true;
+                break;
+            }
+        }
+        if (include) {
+            // check excludes
+            for (String pattern : excludePatterns) {
+                if (!pattern.contains("#") && matches(relativePath, pattern)) {
+                    return false;
+                }
+            }
+        }
+        return include;
+    }
+
+    /** Returns true if given relative path matches pattern.
+     * @param relativePath relative path
+     * @param pattern regex pattern. If contains #, only part before # is taken
+     * into account
+     * @return true if given relative path matches pattern.
+     */
+    private static boolean matches(String relativePath, String pattern) {
+        if (pattern.contains("#")) {  //NOI18N
+            pattern = pattern.split("#", 2)[0];  //NOI18N
+        }
+        return relativePath.matches(pattern);
+    }
+
+    /** Returns set of keys matching given pattern.
+     * @param relativePath path relative to sourceRoot
+     * @param propertiesPattern pattern like file.properties#keyPattern
+     * @return set of matching keys, never null
+     * @throws IOException if properties cannot be loaded
+     */
+    private Set<String> matchingKeys(String relativePath, String propertiesPattern) throws IOException {
+        Set<String> matchingKeys = new HashSet<String>();
+        String[] patterns = propertiesPattern.split("#", 2);
+        String filePattern = patterns[0];
+        String keyPattern = patterns[1];
+        if (relativePath.matches(filePattern)) {
+            if (currentProperties == null) {
+                currentProperties = getProperties(relativePath);
+            }
+            for (String key : currentProperties.keySet()) {
+                if (key.matches(keyPattern)) {
+                    matchingKeys.add(key);
+                }
+            }
+        }
+        return matchingKeys;
+    }
+
+    /** Copy file given by relative path from source zip to target userdir.
+     * It creates necessary sub folders.
+     * @param relativePath relative path
+     * @throws java.io.IOException if copying fails
+     */
+    private void copyFile(String relativePath) throws IOException {
+        currentProperties = null;
+        boolean includeFile = false;  // include? entire file
+        Set<String> includeKeys = new HashSet<>();
+        Set<String> excludeKeys = new HashSet<>();
+        for (String pattern : getIncludePatterns()) {
+            if (pattern.contains("#")) {  //NOI18N
+                includeKeys.addAll(matchingKeys(relativePath, pattern));
+            } else {
+                if (relativePath.matches(pattern)) {
+                    includeFile = true;
+                    includeKeys.clear();  // include entire file
+                    break;
+                }
+            }
+        }
+        if (includeFile || !includeKeys.isEmpty()) {
+            // check excludes
+            for (String pattern : getExcludePatterns()) {
+                if (pattern.contains("#")) {  //NOI18N
+                    excludeKeys.addAll(matchingKeys(relativePath, pattern));
+                } else {
+                    if (relativePath.matches(pattern)) {
+                        includeFile = false;
+                        includeKeys.clear();  // exclude entire file
+                        break;
+                    }
+                }
+            }
+        }
+        LOGGER.log(Level.FINEST, "{0}, includeFile={1}, includeKeys={2}, excludeKeys={3}", new Object[]{relativePath, includeFile, includeKeys, excludeKeys});  //NOI18N
+        if (!includeFile && includeKeys.isEmpty()) {
+            // nothing matches
+            return;
+        }
+
+        File targetFile = new File(targetUserdir, relativePath);
+        File origFile = new File(targetUserdir, relativePath + ".orig");
+        if (!origFile.exists()) {
+            // copy original file
+            try (OutputStream out = createOutputStream(origFile)) {
+                copyFile(relativePath, out);
+            }
+        }
+        LOGGER.log(Level.FINE, "Path: {0}", relativePath);  //NOI18N
+        if (includeKeys.isEmpty() && excludeKeys.isEmpty()) {
+            // copy entire file
+            try (OutputStream out = createOutputStream(targetFile)) {
+                copyFile(relativePath, out);
+            }
+        } else {
+            mergeProperties(relativePath, includeKeys, excludeKeys);
+        }
+    }
+
+    /** Clears file given by relative path in target userdir.
+     * @param relativePath relative path
+     * @throws java.io.IOException if clear fails
+     */
+    private void clearFile(String relativePath) throws IOException {
+        boolean includeFile = false;  // include? entire file
+        Set<String> includeKeys = new HashSet<>();
+        Set<String> excludeKeys = new HashSet<>();
+        for (String pattern : getIncludePatterns()) {
+            if (pattern.contains("#")) {  //NOI18N
+                includeKeys.addAll(matchingKeys(relativePath, pattern));
+            } else {
+                if (relativePath.matches(pattern)) {
+                    includeFile = true;
+                    includeKeys.clear();  // include entire file
+                    break;
+                }
+            }
+        }
+        if (includeFile || !includeKeys.isEmpty()) {
+            // check excludes
+            for (String pattern : getExcludePatterns()) {
+                if (pattern.contains("#")) {  //NOI18N
+                    excludeKeys.addAll(matchingKeys(relativePath, pattern));
+                } else {
+                    if (relativePath.matches(pattern)) {
+                        includeFile = false;
+                        includeKeys.clear();  // exclude entire file
+                        break;
+                    }
+                }
+            }
+        }
+        LOGGER.log(Level.FINEST, "{0}, includeFile={1}, includeKeys={2}, excludeKeys={3}", new Object[]{relativePath, includeFile, includeKeys, excludeKeys});  //NOI18N
+        if (!includeFile && includeKeys.isEmpty()) {
+            // nothing matches
+            return;
+        }
+
+        LOGGER.log(Level.FINE, "Path: {0}", relativePath);  //NOI18N
+        File targetFile = new File(targetUserdir, relativePath);
+        File origFile = new File(targetUserdir, relativePath + ".orig");
+        if (origFile.exists()) {
+            // copy original file
+            try (OutputStream out = createOutputStream(targetFile)) {
+                copyFile(relativePath + ".orig", out);
+            } catch (IOException ioe) {
+                Exceptions.printStackTrace(ioe);
+            }
+            origFile.delete();
+        }
+    }
+
+    /** Merge source properties to existing target properties.
+     * @param relativePath relative path
+     * @param includeKeys keys to include
+     * @param excludeKeys keys to exclude
+     * @throws IOException if I/O fails
+     */
+    private void mergeProperties(String relativePath, Set<String> includeKeys, Set<String> excludeKeys) throws IOException {
+        if (!includeKeys.isEmpty()) {
+            currentProperties.keySet().retainAll(includeKeys);
+        }
+        currentProperties.keySet().removeAll(excludeKeys);
+        LOGGER.log(Level.FINE, "  Keys merged with existing properties: {0}", currentProperties.keySet());  //NOI18N
+        if (currentProperties.isEmpty()) {
+            return;
+        }
+        EditableProperties targetProperties = new EditableProperties(false);
+        InputStream in = null;
+        File targetFile = new File(targetUserdir, relativePath);
+        try {
+            if (targetFile.exists()) {
+                in = new FileInputStream(targetFile);
+                targetProperties.load(in);
+            }
+        } finally {
+            if (in != null) {
+                in.close();
+            }
+        }
+        for (Entry<String, String> entry : currentProperties.entrySet()) {
+            targetProperties.put(entry.getKey(), entry.getValue());
+        }
+        try (OutputStream out = createOutputStream(targetFile)) {
+            targetProperties.store(out);
+        }
+    }
+
+    /** Returns properties from relative path in zip or userdir.
+     * @param relativePath relative path
+     * @return properties from relative path in zip or userdir.
+     * @throws IOException if cannot open stream
+     */
+    private EditableProperties getProperties(String relativePath) throws IOException {
+        EditableProperties properties = new EditableProperties(false);
+        InputStream in = null;
+        try {
+            in = getInputStream(relativePath);
+            properties.load(in);
+        } finally {
+            if (in != null) {
+                in.close();
+            }
+        }
+        return properties;
+    }
+
+    /** Returns InputStream from relative path in zip file or userdir.
+     * @param relativePath relative path
+     * @return InputStream from relative path in zip file or userdir.
+     * @throws IOException if stream cannot be open
+     */
+    private InputStream getInputStream(String relativePath) throws IOException {
+        if (source != null && source.isFile()) {
+            //zip file
+            ZipFile zipFile = new ZipFile(source);
+            ZipEntry zipEntry = zipFile.getEntry(relativePath);
+            return zipFile.getInputStream(zipEntry);
+        } else {
+            // userdir
+            return new FileInputStream(new File(Places.getUserDirectory(), relativePath));
+        }
+    }
+
+    /** Copy file from relative path in zip file or userdir to target OutputStream.
+     * @param relativePath relative path
+     * @param out output stream
+     * @throws java.io.IOException if copying fails
+     */
+    private void copyFile(String relativePath, OutputStream out) throws IOException {
+        try (InputStream in = getInputStream(relativePath)) {
+            FileUtil.copy(in, out);
+        }
+    }
+
+    /** Creates parent of given file, if doesn't exist. */
+    private static void ensureParent(File file) throws IOException {
+        final File parent = file.getParentFile();
+        if (parent != null && !parent.exists()) {
+            if (!parent.mkdirs()) {
+                throw new IOException("Cannot create folder: " + parent.getAbsolutePath());  //NOI18N
+            }
+        }
+    }
+
+    /** Returns list of paths from given zip file.
+     * @param file zip file
+     * @return list of paths from given zip file
+     * @throws java.io.IOException
+     */
+    private static List<String> listZipFile(File file) throws IOException {
+        List<String> relativePaths = new ArrayList<>();
+        // Open the ZIP file
+        ZipFile zipFile = new ZipFile(file);
+        // Enumerate each entry
+        Enumeration<? extends ZipEntry> entries = zipFile.entries();
+        while (entries.hasMoreElements()) {
+            ZipEntry zipEntry = (ZipEntry) entries.nextElement();
+            if (!zipEntry.isDirectory()) {
+                relativePaths.add(zipEntry.getName());
+            }
+        }
+        return relativePaths;
+    }
+
+    private static OutputStream createOutputStream(File file) throws IOException {
+        if (containsConfig(file)) {
+            file = file.getCanonicalFile();
+            File root = FileUtil.toFile(FileUtil.getConfigRoot());
+            String filePath = file.getPath();
+            String rootPath = root.getPath();
+            if (filePath.startsWith(rootPath)) {
+                String res = filePath.substring(rootPath.length()).replace(File.separatorChar, '/');
+                FileObject fo;
+		try {
+		    fo = FileUtil.createData(FileUtil.getConfigRoot(), res);
+		    if (fo != null) {
+			return fo.getOutputStream();
+		    }
+		} catch (SyncFailedException ex) {
+		    LOGGER.log(Level.INFO, "File already exists: {0}", filePath);  //NOI18N
+		} catch (IOException ex) {
+		    LOGGER.log(Level.INFO, "IOException while getting output stream: {0}", filePath);  //NOI18N
+		}
+            }
+        }
+        ensureParent(file);
+        return new FileOutputStream(file);
+    }
+    private static boolean containsConfig(File file) {
+        for (;;) {
+            if (file == null) {
+                return false;
+            }
+            if (file.getName().equals("config")) {
+                return true;
+            }
+            file = file.getParentFile();
+        }
+    }
+}
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java
index b1e26e645b..3b31b21b04 100644
--- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/Server.java
@@ -18,7 +18,6 @@
  */
 package org.netbeans.modules.java.lsp.server.protocol;
 
-import com.google.gson.InstanceCreator;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -308,6 +307,7 @@ public final class Server {
 
     public static class LanguageServerImpl implements LanguageServer, LanguageClientAware, LspServerState, NbLanguageServer {
 
+        private static final String NETBEANS_FORMAT = "netbeans.format";
         private static final String NETBEANS_JAVA_IMPORTS = "netbeans.java.imports";
 
         // change to a greater throughput if the initialization waits on more processes than just (serialized) project open.
@@ -713,6 +713,8 @@ public final class Server {
                 capabilities.setTypeDefinitionProvider(true);
                 capabilities.setImplementationProvider(true);
                 capabilities.setDocumentHighlightProvider(true);
+                capabilities.setDocumentFormattingProvider(true);
+                capabilities.setDocumentRangeFormattingProvider(true);
                 capabilities.setReferencesProvider(true);
                 
                 CallHierarchyRegistrationOptions chOpts = new CallHierarchyRegistrationOptions();
@@ -804,6 +806,12 @@ public final class Server {
                     ConfigurationItem item = new ConfigurationItem();
                     FileObject fo = projects[0].getProjectDirectory();
                     item.setScopeUri(Utils.toUri(fo));
+                    item.setSection(NETBEANS_FORMAT);
+                    client.configuration(new ConfigurationParams(Collections.singletonList(item))).thenAccept(c -> {
+                        if (c != null && !c.isEmpty() && c.get(0) instanceof JsonObject) {
+                            workspaceService.updateJavaFormatPreferences(fo, (JsonObject) c.get(0));
+                        }
+                    });
                     item.setSection(NETBEANS_JAVA_IMPORTS);
                     client.configuration(new ConfigurationParams(Collections.singletonList(item))).thenAccept(c -> {
                         if (c != null && !c.isEmpty() && c.get(0) instanceof JsonObject) {
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImpl.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImpl.java
index 843da8014e..4db5958cc3 100644
--- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImpl.java
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/TextDocumentServiceImpl.java
@@ -31,6 +31,8 @@ import com.sun.source.util.TreePath;
 import com.sun.source.util.TreePathScanner;
 import com.sun.source.util.Trees;
 import com.vladsch.flexmark.html2md.converter.FlexmarkHtmlConverter;
+import java.awt.Color;
+import java.awt.Font;
 import java.io.FileNotFoundException;
 import java.net.URI;
 import java.net.URL;
@@ -73,8 +75,12 @@ import javax.lang.model.type.TypeKind;
 import javax.lang.model.type.TypeMirror;
 import javax.swing.event.DocumentEvent;
 import javax.swing.event.DocumentListener;
+import javax.swing.event.UndoableEditListener;
+import javax.swing.text.AttributeSet;
 import javax.swing.text.BadLocationException;
 import javax.swing.text.Document;
+import javax.swing.text.Segment;
+import javax.swing.text.Style;
 import javax.swing.text.StyledDocument;
 import org.eclipse.lsp4j.CallHierarchyIncomingCall;
 import org.eclipse.lsp4j.CallHierarchyIncomingCallsParams;
@@ -155,6 +161,8 @@ import org.eclipse.lsp4j.services.LanguageClient;
 import org.eclipse.lsp4j.services.LanguageClientAware;
 import org.eclipse.lsp4j.services.TextDocumentService;
 import org.netbeans.api.annotations.common.CheckForNull;
+import org.netbeans.api.editor.document.AtomicLockDocument;
+import org.netbeans.api.editor.document.AtomicLockListener;
 import org.netbeans.api.editor.document.LineDocument;
 import org.netbeans.api.editor.document.LineDocumentUtils;
 import org.netbeans.api.editor.mimelookup.MimeLookup;
@@ -226,6 +234,7 @@ import org.netbeans.modules.refactoring.spi.RefactoringCommit;
 import org.netbeans.modules.refactoring.spi.RefactoringElementImplementation;
 import org.netbeans.modules.refactoring.spi.Transaction;
 import org.netbeans.api.lsp.StructureElement;
+import org.netbeans.modules.editor.indent.api.Reformat;
 import org.netbeans.spi.editor.hints.ErrorDescription;
 import org.netbeans.spi.editor.hints.Fix;
 import org.netbeans.spi.lsp.CallHierarchyProvider;
@@ -1182,13 +1191,46 @@ public class TextDocumentServiceImpl implements TextDocumentService, LanguageCli
     }
 
     @Override
-    public CompletableFuture<List<? extends TextEdit>> formatting(DocumentFormattingParams arg0) {
-        throw new UnsupportedOperationException("Not supported yet.");
+    public CompletableFuture<List<? extends TextEdit>> formatting(DocumentFormattingParams params) {
+        String uri = params.getTextDocument().getUri();
+        Document doc = server.getOpenedDocuments().getDocument(uri);
+        return format((LineDocument) doc, 0, doc.getLength());
     }
 
     @Override
-    public CompletableFuture<List<? extends TextEdit>> rangeFormatting(DocumentRangeFormattingParams arg0) {
-        throw new UnsupportedOperationException("Not supported yet.");
+    public CompletableFuture<List<? extends TextEdit>> rangeFormatting(DocumentRangeFormattingParams params) {
+        String uri = params.getTextDocument().getUri();
+        LineDocument lDoc = LineDocumentUtils.as(server.getOpenedDocuments().getDocument(uri), LineDocument.class);
+        if (lDoc != null) {
+            Range range = params.getRange();
+            return format(lDoc, Utils.getOffset(lDoc, range.getStart()), Utils.getOffset(lDoc, range.getEnd()));
+        }
+        return CompletableFuture.completedFuture(Collections.emptyList());
+    }
+
+    private CompletableFuture<List<? extends TextEdit>> format(Document doc, int startOffset, int endOffset) {
+        CompletableFuture<List<? extends TextEdit>> result = new CompletableFuture<>();
+        StyledDocument sDoc = LineDocumentUtils.as(doc, StyledDocument.class);
+        if (sDoc != null) {
+            FormatterDocument formDoc = new FormatterDocument(sDoc);
+            Reformat reformat = Reformat.get(formDoc);
+            if (reformat != null) {
+                reformat.lock();
+                try {
+                    reformat.reformat(startOffset, endOffset);
+                    result.complete(formDoc.getEdits());
+                } catch (BadLocationException ex) {
+                    result.completeExceptionally(ex);
+                } finally {
+                    reformat.unlock();
+                }
+            } else {
+                result.complete(Collections.emptyList());
+            }
+        } else {
+            result.complete(Collections.emptyList());
+        }
+        return result;
     }
 
     @Override
@@ -2243,4 +2285,220 @@ public class TextDocumentServiceImpl implements TextDocumentService, LanguageCli
         };
         return t.processRequest();
     }
+
+    private static class FormatterDocument implements StyledDocument, LineDocument, AtomicLockDocument {
+
+        private final StyledDocument doc;
+        private final List<TextEdit> edits = new ArrayList<>();
+        private TextEdit last = null;
+
+        private FormatterDocument(StyledDocument lineDocument) {
+            this.doc = lineDocument;
+        }
+
+        private List<TextEdit> getEdits() {
+            return edits;
+        }
+
+        @Override
+        public Style addStyle(String nm, Style parent) {
+            return doc.addStyle(nm, parent);
+        }
+
+        @Override
+        public void removeStyle(String nm) {
+            doc.removeStyle(nm);
+        }
+
+        @Override
+        public Style getStyle(String nm) {
+            return doc.getStyle(nm);
+        }
+
+        @Override
+        public void setCharacterAttributes(int offset, int length, AttributeSet s, boolean replace) {
+            doc.setCharacterAttributes(offset, length, s, replace);
+        }
+
+        @Override
+        public void setParagraphAttributes(int offset, int length, AttributeSet s, boolean replace) {
+            doc.setParagraphAttributes(offset, length, s, replace);
+        }
+
+        @Override
+        public void setLogicalStyle(int pos, Style s) {
+            doc.setLogicalStyle(pos, s);
+        }
+
+        @Override
+        public Style getLogicalStyle(int p) {
+            return doc.getLogicalStyle(p);
+        }
+
+        @Override
+        public javax.swing.text.Element getParagraphElement(int pos) {
+            return doc.getParagraphElement(pos);
+        }
+
+        @Override
+        public javax.swing.text.Element getCharacterElement(int pos) {
+            return doc.getCharacterElement(pos);
+        }
+
+        @Override
+        public Color getForeground(AttributeSet attr) {
+            return doc.getForeground(attr);
+        }
+
+        @Override
+        public Color getBackground(AttributeSet attr) {
+            return doc.getBackground(attr);
+        }
+
+        @Override
+        public Font getFont(AttributeSet attr) {
+            return doc.getFont(attr);
+        }
+
+        @Override
+        public int getLength() {
+            return doc.getLength();
+        }
+
+        @Override
+        public void addDocumentListener(DocumentListener listener) {
+            doc.addDocumentListener(listener);
+        }
+
+        @Override
+        public void removeDocumentListener(DocumentListener listener) {
+            doc.removeDocumentListener(listener);
+        }
+
+        @Override
+        public void addUndoableEditListener(UndoableEditListener listener) {
+            doc.addUndoableEditListener(listener);
+        }
+
+        @Override
+        public void removeUndoableEditListener(UndoableEditListener listener) {
+            doc.removeUndoableEditListener(listener);
+        }
+
+        @Override
+        public Object getProperty(Object key) {
+            return doc.getProperty(key);
+        }
+
+        @Override
+        public void putProperty(Object key, Object value) {
+        }
+
+        @Override
+        public void remove(int offs, int len) throws BadLocationException {
+            LineDocument ldoc = LineDocumentUtils.as(doc, LineDocument.class);
+            Position pos = Utils.createPosition(ldoc, offs);
+            if (last != null && pos.equals(last.getRange().getStart()) && pos.equals(last.getRange().getEnd())) {
+                last.getRange().setEnd(Utils.createPosition(ldoc, offs + len));
+            } else {
+                last = new TextEdit(new Range(pos, Utils.createPosition(ldoc, offs + len)), "");
+                edits.add(last);
+            }
+        }
+
+        @Override
+        public void insertString(int offset, String str, AttributeSet a) throws BadLocationException {
+            LineDocument ldoc = LineDocumentUtils.as(doc, LineDocument.class);
+            Position pos = Utils.createPosition(ldoc, offset);
+            if (last != null && pos.equals(last.getRange().getStart())) {
+                if (str != null) {
+                    last.setNewText(last.getNewText() + str);
+                }
+            } else {
+                last = new TextEdit(new Range(pos, pos), str != null ? str : "");
+                edits.add(last);
+            }
+        }
+
+        @Override
+        public String getText(int offset, int length) throws BadLocationException {
+            return doc.getText(offset, length);
+        }
+
+        @Override
+        public void getText(int offset, int length, Segment txt) throws BadLocationException {
+            doc.getText(offset, length, txt);
+        }
+
+        @Override
+        public javax.swing.text.Position getStartPosition() {
+            return doc.getStartPosition();
+        }
+
+        @Override
+        public javax.swing.text.Position getEndPosition() {
+            return doc.getEndPosition();
+        }
+
+        @Override
+        public javax.swing.text.Position createPosition(int offs) throws BadLocationException {
+            return doc.createPosition(offs);
+        }
+
+        @Override
+        public javax.swing.text.Element[] getRootElements() {
+            return doc.getRootElements();
+        }
+
+        @Override
+        public javax.swing.text.Element getDefaultRootElement() {
+            return doc.getDefaultRootElement();
+        }
+
+        @Override
+        public void render(Runnable r) {
+            doc.render(r);
+        }
+
+        @Override
+        public javax.swing.text.Position createPosition(int offset, javax.swing.text.Position.Bias bias) throws BadLocationException {
+            LineDocument ldoc = LineDocumentUtils.as(doc, LineDocument.class);
+            return ldoc.createPosition(offset, bias);
+        }
+
+        @Override
+        public Document getDocument() {
+            return this;
+        }
+
+        @Override
+        public void atomicUndo() {
+            AtomicLockDocument bdoc = LineDocumentUtils.as(doc, AtomicLockDocument.class);
+            bdoc.atomicUndo();
+        }
+
+        @Override
+        public void runAtomic(Runnable r) {
+            AtomicLockDocument bdoc = LineDocumentUtils.as(doc, AtomicLockDocument.class);
+            bdoc.runAtomic(r);
+        }
+
+        @Override
+        public void runAtomicAsUser(Runnable r) {
+            AtomicLockDocument bdoc = LineDocumentUtils.as(doc, AtomicLockDocument.class);
+            bdoc.runAtomicAsUser(r);
+        }
+
+        @Override
+        public void addAtomicLockListener(AtomicLockListener l) {
+            AtomicLockDocument bdoc = LineDocumentUtils.as(doc, AtomicLockDocument.class);
+            bdoc.addAtomicLockListener(l);
+        }
+
+        @Override
+        public void removeAtomicLockListener(AtomicLockListener l) {
+            AtomicLockDocument bdoc = LineDocumentUtils.as(doc, AtomicLockDocument.class);
+            bdoc.removeAtomicLockListener(l);
+        }
+    }
 }
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/WorkspaceServiceImpl.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/WorkspaceServiceImpl.java
index 11f576e576..d7d07d2959 100644
--- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/WorkspaceServiceImpl.java
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/protocol/WorkspaceServiceImpl.java
@@ -24,6 +24,7 @@ import com.google.gson.JsonObject;
 import com.google.gson.JsonPrimitive;
 import com.sun.source.util.TreePath;
 import java.beans.PropertyChangeListener;
+import java.io.File;
 import java.io.IOException;
 import java.lang.reflect.Method;
 import java.net.MalformedURLException;
@@ -32,6 +33,8 @@ import java.net.URL;
 import java.net.URLDecoder;
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -42,6 +45,7 @@ import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Optional;
@@ -118,8 +122,10 @@ import org.openide.DialogDisplayer;
 import org.openide.NotifyDescriptor;
 import org.openide.filesystems.FileObject;
 import org.openide.filesystems.URLMapper;
+import org.openide.modules.Places;
 import org.openide.util.Exceptions;
 import org.openide.util.Lookup;
+import org.openide.util.NbPreferences;
 import org.openide.util.Pair;
 import org.openide.util.RequestProcessor;
 import org.openide.util.WeakListeners;
@@ -900,14 +906,47 @@ public final class WorkspaceServiceImpl implements WorkspaceService, LanguageCli
     public void didChangeConfiguration(DidChangeConfigurationParams params) {
         server.openedProjects().thenAccept(projects -> {
             if (projects != null && projects.length > 0) {
+                updateJavaFormatPreferences(projects[0].getProjectDirectory(), ((JsonObject) params.getSettings()).getAsJsonObject("netbeans").getAsJsonObject("format"));
                 updateJavaImportPreferences(projects[0].getProjectDirectory(), ((JsonObject) params.getSettings()).getAsJsonObject("netbeans").getAsJsonObject("java").getAsJsonObject("imports"));
             }
         });
     }
 
+    void updateJavaFormatPreferences(FileObject fo, JsonObject configuration) {
+        if (configuration != null) {
+            NbPreferences.Provider provider = Lookup.getDefault().lookup(NbPreferences.Provider.class);
+            Preferences prefs = provider != null ? provider.preferencesRoot().node("de/funfried/netbeans/plugins/externalcodeformatter") : null;
+            JsonPrimitive formatterPrimitive = configuration.getAsJsonPrimitive("codeFormatter");
+            String formatter = formatterPrimitive != null ? formatterPrimitive.getAsString() : null;
+            JsonPrimitive pathPrimitive = configuration.getAsJsonPrimitive("settingsPath");
+            String path = pathPrimitive != null ? pathPrimitive.getAsString() : null;
+            if (formatter == null || "NetBeans".equals(formatter)) {
+                if (prefs != null) {
+                    prefs.put("enabledFormatter.JAVA", "netbeans-formatter");
+                }
+                Path p = path != null ? Paths.get(path) : null;
+                File file = p != null ? p.toFile() : null;
+                try {
+                    if (file != null && file.exists() && file.canRead() && file.getName().endsWith(".zip")) {
+                        OptionsExportModel.get().doImport(file);
+                    } else {
+                        OptionsExportModel.get().clean();
+                    }
+                } catch (IOException ex) {
+                    Exceptions.printStackTrace(ex);
+                }
+            } else if (prefs != null) {
+                prefs.put("enabledFormatter.JAVA", formatter.toLowerCase(Locale.ENGLISH).concat("-java-formatter"));
+                if (path != null) {
+                    prefs.put(formatter.toLowerCase(Locale.ENGLISH).concat("FormatterLocation"), path);
+                }
+            }
+        }
+    }
+
     void updateJavaImportPreferences(FileObject fo, JsonObject configuration) {
         Preferences prefs = CodeStylePreferences.get(fo, "text/x-java").getPreferences();
-        if (prefs != null) {
+        if (prefs != null && configuration != null) {
             prefs.put("importGroupsOrder", String.join(";", gson.fromJson(configuration.get("groups"), String[].class)));
             prefs.putBoolean("allowConvertToStarImport", true);
             prefs.putInt("countForUsingStarImport", configuration.getAsJsonPrimitive("countForUsingStarImport").getAsInt());
diff --git a/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/protocol/ServerTest.java b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/protocol/ServerTest.java
index 0525b4a910..d021ac258b 100644
--- a/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/protocol/ServerTest.java
+++ b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/protocol/ServerTest.java
@@ -81,14 +81,17 @@ import org.eclipse.lsp4j.Diagnostic;
 import org.eclipse.lsp4j.DidChangeTextDocumentParams;
 import org.eclipse.lsp4j.DidCloseTextDocumentParams;
 import org.eclipse.lsp4j.DidOpenTextDocumentParams;
+import org.eclipse.lsp4j.DocumentFormattingParams;
 import org.eclipse.lsp4j.DocumentHighlight;
 import org.eclipse.lsp4j.DocumentHighlightKind;
 import org.eclipse.lsp4j.DocumentHighlightParams;
+import org.eclipse.lsp4j.DocumentRangeFormattingParams;
 import org.eclipse.lsp4j.DocumentSymbol;
 import org.eclipse.lsp4j.DocumentSymbolParams;
 import org.eclipse.lsp4j.ExecuteCommandParams;
 import org.eclipse.lsp4j.FoldingRange;
 import org.eclipse.lsp4j.FoldingRangeRequestParams;
+import org.eclipse.lsp4j.FormattingOptions;
 import org.eclipse.lsp4j.Hover;
 import org.eclipse.lsp4j.HoverParams;
 import org.eclipse.lsp4j.ImplementationParams;
@@ -4476,6 +4479,166 @@ public class ServerTest extends NbTestCase {
                      "} while (${1:true});", obj.getAsJsonPrimitive("snippet").getAsString());
     }
 
+    public void testFormatDocument() throws Exception {
+        File src = new File(getWorkDir(), "Test.java");
+        src.getParentFile().mkdirs();
+        String code = "public class Test\n" +
+                      "{\n" +
+                      "    public static void main(String[] args)\n" +
+                      "    {\n" +
+                      "        System.out.println(\"Hello World\");\n" +
+                      "    }\n" +
+                      "}\n";
+        try (Writer w = new FileWriter(src)) {
+            w.write(code);
+        }
+
+        List<Diagnostic>[] diags = new List[1];
+        Launcher<LanguageServer> serverLauncher = LSPLauncher.createClientLauncher(new LspClient() {
+            @Override
+            public void telemetryEvent(Object arg0) {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+
+            @Override
+            public void publishDiagnostics(PublishDiagnosticsParams params) {
+                synchronized (diags) {
+                    diags[0] = params.getDiagnostics();
+                    diags.notifyAll();
+                }
+            }
+
+            @Override
+            public void showMessage(MessageParams arg0) {
+            }
+
+            @Override
+            public CompletableFuture<MessageActionItem> showMessageRequest(ShowMessageRequestParams arg0) {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+
+            @Override
+            public void logMessage(MessageParams arg0) {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+
+            @Override
+            public CompletableFuture<ApplyWorkspaceEditResponse> applyEdit(ApplyWorkspaceEditParams params) {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+
+        }, client.getInputStream(), client.getOutputStream());
+        serverLauncher.startListening();
+        LanguageServer server = serverLauncher.getRemoteProxy();
+        server.initialize(new InitializeParams()).get();
+        String uri = src.toURI().toString();
+        server.getTextDocumentService().didOpen(new DidOpenTextDocumentParams(new TextDocumentItem(uri, "java", 0, code)));
+        synchronized (diags) {
+            while (diags[0] == null) {
+                try {
+                    diags.wait();
+                } catch (InterruptedException ex) {
+                }
+            }
+        }
+        VersionedTextDocumentIdentifier id = new VersionedTextDocumentIdentifier(src.toURI().toString(), 1);
+        List<? extends TextEdit> edits = server.getTextDocumentService().formatting(new DocumentFormattingParams(id, new FormattingOptions(4, true))).get();
+        assertNotNull(edits);
+        assertEquals(4, edits.size());
+        assertEquals(new Range(new Position(2, 42),
+                               new Position(3, 4)),
+                     edits.get(0).getRange());
+        assertEquals(edits.get(0).getNewText(), " ");
+        assertEquals(new Range(new Position(2, 0),
+                               new Position(2, 4)),
+                     edits.get(1).getRange());
+        assertEquals(edits.get(1).getNewText(), "\n    ");
+        assertEquals(new Range(new Position(0, 17),
+                               new Position(1, 0)),
+                     edits.get(2).getRange());
+        assertEquals(edits.get(2).getNewText(), " ");
+        assertEquals(new Range(new Position(0, 0),
+                               new Position(0, 0)),
+                     edits.get(3).getRange());
+        assertEquals(edits.get(3).getNewText(), "\n");
+    }
+
+    public void testFormatSelection() throws Exception {
+        File src = new File(getWorkDir(), "Test.java");
+        src.getParentFile().mkdirs();
+        String code = "public class Test\n" +
+                      "{\n" +
+                      "    public static void main(String[] args)\n" +
+                      "    {\n" +
+                      "        System.out.println(\"Hello World\");\n" +
+                      "    }\n" +
+                      "}\n";
+        try (Writer w = new FileWriter(src)) {
+            w.write(code);
+        }
+
+        List<Diagnostic>[] diags = new List[1];
+        Launcher<LanguageServer> serverLauncher = LSPLauncher.createClientLauncher(new LspClient() {
+            @Override
+            public void telemetryEvent(Object arg0) {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+
+            @Override
+            public void publishDiagnostics(PublishDiagnosticsParams params) {
+                synchronized (diags) {
+                    diags[0] = params.getDiagnostics();
+                    diags.notifyAll();
+                }
+            }
+
+            @Override
+            public void showMessage(MessageParams arg0) {
+            }
+
+            @Override
+            public CompletableFuture<MessageActionItem> showMessageRequest(ShowMessageRequestParams arg0) {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+
+            @Override
+            public void logMessage(MessageParams arg0) {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+
+            @Override
+            public CompletableFuture<ApplyWorkspaceEditResponse> applyEdit(ApplyWorkspaceEditParams params) {
+                throw new UnsupportedOperationException("Not supported yet.");
+            }
+
+        }, client.getInputStream(), client.getOutputStream());
+        serverLauncher.startListening();
+        LanguageServer server = serverLauncher.getRemoteProxy();
+        server.initialize(new InitializeParams()).get();
+        String uri = src.toURI().toString();
+        server.getTextDocumentService().didOpen(new DidOpenTextDocumentParams(new TextDocumentItem(uri, "java", 0, code)));
+        synchronized (diags) {
+            while (diags[0] == null) {
+                try {
+                    diags.wait();
+                } catch (InterruptedException ex) {
+                }
+            }
+        }
+        VersionedTextDocumentIdentifier id = new VersionedTextDocumentIdentifier(src.toURI().toString(), 1);
+        List<? extends TextEdit> edits = server.getTextDocumentService().rangeFormatting(new DocumentRangeFormattingParams(id, new FormattingOptions(4, true), new Range(new Position(2, 0), new Position(6, 0)))).get();
+        assertNotNull(edits);
+        assertEquals(2, edits.size());
+        assertEquals(new Range(new Position(2, 42),
+                               new Position(3, 4)),
+                     edits.get(0).getRange());
+        assertEquals(edits.get(0).getNewText(), " ");
+        assertEquals(new Range(new Position(2, 0),
+                               new Position(2, 4)),
+                     edits.get(1).getRange());
+        assertEquals(edits.get(1).getNewText(), "    ");
+    }
+
     public void testNoErrorAndHintsFor() throws Exception {
         File src = new File(getWorkDir(), "Test.java");
         src.getParentFile().mkdirs();
diff --git a/java/java.lsp.server/vscode/package.json b/java/java.lsp.server/vscode/package.json
index 788103fedf..41cec58ee4 100644
--- a/java/java.lsp.server/vscode/package.json
+++ b/java/java.lsp.server/vscode/package.json
@@ -140,6 +140,11 @@
 					"default": 100,
 					"description": "Timeout (in milliseconds) for loading Javadoc in code completion (-1 for unlimited)"
 				},
+				"netbeans.format.settingsPath": {
+					"type": "string",
+					"description": "Path to the file containing exported formatter settings",
+					"default": null
+				},
 				"netbeans.java.onSave.organizeImports": {
 					"type": "boolean",
 					"default": true,
diff --git a/java/java.lsp.server/vscode/src/extension.ts b/java/java.lsp.server/vscode/src/extension.ts
index ce9499be5a..64e4531acf 100644
--- a/java/java.lsp.server/vscode/src/extension.ts
+++ b/java/java.lsp.server/vscode/src/extension.ts
@@ -779,7 +779,10 @@ function doActivateWithJDK(specifiedJDK: string | null, context: ExtensionContex
             // Register the server for java documents
             documentSelector: documentSelectors,
             synchronize: {
-                configurationSection: 'netbeans.java.imports',
+                configurationSection: [
+                    'netbeans.format',
+                    'netbeans.java.imports'
+                ],
                 fileEvents: [
                     workspace.createFileSystemWatcher('**/*.java')
                 ]
diff --git a/java/java.source.base/src/org/netbeans/modules/java/source/save/Reformatter.java b/java/java.source.base/src/org/netbeans/modules/java/source/save/Reformatter.java
index 09dc151a49..434d2cda7d 100644
--- a/java/java.source.base/src/org/netbeans/modules/java/source/save/Reformatter.java
+++ b/java/java.source.base/src/org/netbeans/modules/java/source/save/Reformatter.java
@@ -5460,6 +5460,9 @@ public class Reformatter implements ReformatTask {
                 int offset = (int)sp.getStartPosition(path.getCompilationUnit(), path.getLeaf());
                 if (offset < 0)
                     return indent;
+                if (offset == 0) {
+                    return 0;
+                }
                 tokens.move(offset);
                 String text = null;
                 while (tokens.movePrevious()) {


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@netbeans.apache.org
For additional commands, e-mail: commits-help@netbeans.apache.org

For further information about the NetBeans mailing lists, visit:
https://cwiki.apache.org/confluence/display/NETBEANS/Mailing+lists