diff --git a/groovy/groovy.editor/apichanges.xml b/groovy/groovy.editor/apichanges.xml index 31187a7186f0..b730b8fa9a7b 100644 --- a/groovy/groovy.editor/apichanges.xml +++ b/groovy/groovy.editor/apichanges.xml @@ -84,6 +84,20 @@ is the proper place. + + + Variants of the exisitng methods taking LineDocument as argument added. + + + + + +

+ Variants of the exisitng methods taking LineDocument as argument added. +

+
+ +
Alternative construction for path more suitable for expressions, API to resolve types diff --git a/groovy/groovy.editor/manifest.mf b/groovy/groovy.editor/manifest.mf index 813047c4d581..44d57ad4fa9b 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 602f80079a31..dc7aa9586216 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 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 static OffsetRange getLexerOffsets(GroovyParserResult info, OffsetRange a /** Find the Groovy token sequence (in case it's embedded in something else at the top level. */ @SuppressWarnings("unchecked") public static TokenSequence 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 static TokenSequence getPositionedSequence(BaseDocument do return getPositionedSequence(doc, offset, true); } + public static TokenSequence getPositionedSequence(LineDocument doc, int offset) { + return getPositionedSequence(doc, offset, true); + } + public static TokenSequence getPositionedSequence(BaseDocument doc, int offset, boolean lookBack) { TokenSequence ts = getGroovyTokenSequence(doc, offset); @@ -264,6 +273,34 @@ public static TokenSequence getPositionedSequence(BaseDocument do return null; } + public static TokenSequence getPositionedSequence(LineDocument doc, int offset, boolean lookBack) { + TokenSequence 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 getToken(BaseDocument doc, int offset) { TokenSequence ts = getGroovyTokenSequence(doc, offset); @@ -292,6 +329,34 @@ public static Token getToken(BaseDocument doc, int offset) { return null; } + public static Token getToken(LineDocument doc, int offset) { + TokenSequence 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 token = ts.token(); + + return token; + } + + return null; + } + public static char getTokenChar(BaseDocument doc, int offset) { Token token = getToken(doc, offset); @@ -446,6 +511,15 @@ public static boolean isBeginToken(TokenId id, BaseDocument 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", + * 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 static boolean isBeginToken(TokenId id, BaseDocument doc, TokenSequence 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 1c4915acf1ce..95a66c220a11 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.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 int hangingIndentSize() { } /** 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 ts = LexUtilities.getGroovyTokenSequence(doc, offset); if (ts == null) { return 0; @@ -104,7 +104,7 @@ private int getFormatStableStart(BaseDocument doc, int offset) { } private int getTokenBalanceDelta(TokenId id, Token token, - BaseDocument doc, TokenSequence ts, boolean includeKeywords) { + LineDocument doc, TokenSequence 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 @@ private int getTokenBalanceDelta(TokenId id, Token token, } // 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 ts = LexUtilities.getGroovyTokenSequence(doc, begin); @@ -159,8 +159,8 @@ private int getTokenBalance(BaseDocument doc, int begin, int end, boolean includ } // 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 token = LexUtilities.getToken(doc, pos); if (token != null) { @@ -176,7 +176,7 @@ private boolean isJavaDocComment(BaseDocument doc, int offset, int endOfLine) th 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 @@ private boolean isInLiteral(BaseDocument doc, int offset) throws BadLocationExce // 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 @@ private boolean isInLiteral(BaseDocument doc, int offset) throws BadLocationExce 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 token = LexUtilities.getToken(doc, lineBegin); @@ -248,8 +248,8 @@ private boolean isEndIndent(BaseDocument doc, int offset) throws BadLocationExce 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 @@ private boolean isLineContinued(BaseDocument doc, int offset, int bracketBalance // 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 @@ private void reindent(final Context context, ParserResult info, final boolean in 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 offsets = new ArrayList(); - final List indents = new ArrayList(); - - // 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 offsets = new ArrayList(); + final List indents = new ArrayList(); + + // 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 offsets, List indents, boolean indentEmptyLines, boolean includeEnd, boolean indentOnly @@ -429,7 +426,7 @@ private void computeIndents(BaseDocument doc, int initialIndent, int startOffset // 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 @@ private void computeIndents(BaseDocument doc, int initialIndent, int startOffset 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 @@ private void computeIndents(BaseDocument doc, int initialIndent, int startOffset 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 55ab99bd7c7e..cbc0350e5e48 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 @@ private void addAllTypes(Env env, EnumSet kinds) { 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 @@ private void addAllTypes(Env env, EnumSet kinds) { Set> 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 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 0b1afccd52a9..b05ab5d9b6af 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.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 static Position createPosition(FileObject file, int offset) { } } + 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 000000000000..82d62bf54352 --- /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 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 categories; + /** Cache of paths relative to source root. */ + List relativePaths; + /** Include patterns. */ + private Set includePatterns; + /** Exclude patterns. */ + private Set excludePatterns; + /** Properties currently being copied. */ + private EditableProperties currentProperties; + /** List of ignored folders in userdir. It speeds up folder scanning. */ + private static final List 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 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 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 parsePattern(String pattern) { + Set patterns = new HashSet(); + 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 getIncludePatterns() { + if (includePatterns == null) { + Set 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 getExcludePatterns() { + if (excludePatterns == null) { + Set 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 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 applicablePaths = getApplicablePaths( + Collections.singleton(include), + Collections.singleton(exclude)); + Set 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 getItems() { + if (items == null) { + items = Collections.synchronizedList(new ArrayList<>()); + FileObject[] itemsFOs = categoryFO.getChildren(); + // respect ordering defined in layers + List 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 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 getApplicablePaths(Set includePatterns, Set excludePatterns) { + List applicablePaths = new ArrayList<>(); + for (String relativePath : getRelativePaths()) { + if (matches(relativePath, includePatterns, excludePatterns)) { + applicablePaths.add(relativePath); + } + } + return applicablePaths; + } + + private List 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 getRelativePaths(File sourceRoot) { + return getRelativePaths(sourceRoot, sourceRoot); + } + + private static List getRelativePaths(File root, File file) { + String relativePath = getRelativePath(root, file); + List 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 includePatterns, Set 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 matchingKeys(String relativePath, String propertiesPattern) throws IOException { + Set matchingKeys = new HashSet(); + 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 includeKeys = new HashSet<>(); + Set 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 includeKeys = new HashSet<>(); + Set 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 includeKeys, Set 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 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 listZipFile(File file) throws IOException { + List relativePaths = new ArrayList<>(); + // Open the ZIP file + ZipFile zipFile = new ZipFile(file); + // Enumerate each entry + Enumeration 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 b1e26e645bb7..3b31b21b04bc 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 Project next() { 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 @@ private InitializeResult constructInitResponse(InitializeParams init, JavaSource 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 @@ private void initializeOptions() { 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 843da8014e12..4db5958cc312 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.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.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.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.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 CompletableFuture resolveCodeLens(CodeLens arg0) { } @Override - public CompletableFuture> formatting(DocumentFormattingParams arg0) { - throw new UnsupportedOperationException("Not supported yet."); + public CompletableFuture> formatting(DocumentFormattingParams params) { + String uri = params.getTextDocument().getUri(); + Document doc = server.getOpenedDocuments().getDocument(uri); + return format((LineDocument) doc, 0, doc.getLength()); } @Override - public CompletableFuture> rangeFormatting(DocumentRangeFormattingParams arg0) { - throw new UnsupportedOperationException("Not supported yet."); + public CompletableFuture> 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> format(Document doc, int startOffset, int endOffset) { + CompletableFuture> 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 @@ protected CallHierarchyOutgoingCall createResultItem(CallHierarchyItem item, Lis }; return t.processRequest(); } + + private static class FormatterDocument implements StyledDocument, LineDocument, AtomicLockDocument { + + private final StyledDocument doc; + private final List edits = new ArrayList<>(); + private TextEdit last = null; + + private FormatterDocument(StyledDocument lineDocument) { + this.doc = lineDocument; + } + + private List 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 11f576e5763d..d7d07d29598d 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.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.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.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.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 @@ private static String getSimpleName ( 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 0525b4a9104e..d021ac258b2a 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.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 CompletableFuture applyEdit(ApplyWorkspaceEdi "} 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[] diags = new List[1]; + Launcher 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 showMessageRequest(ShowMessageRequestParams arg0) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void logMessage(MessageParams arg0) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public CompletableFuture 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 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[] diags = new List[1]; + Launcher 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 showMessageRequest(ShowMessageRequestParams arg0) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public void logMessage(MessageParams arg0) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public CompletableFuture 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 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 788103fedf01..41cec58ee4e2 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 ce9499be5aa9..64e4531acfa3 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 09dc151a49f8..434d2cda7dba 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 @@ private int getIndentLevel(TokenSequence tokens, TreePath path) { 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()) {