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