diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 85a8ae934b83..d12852e36065 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1563,8 +1563,8 @@ jobs: # - name: debugger.jpda.truffle # run: ant $OPTS -f java/debugger.jpda.truffle test -# - name: debugger.jpda.ui -# run: ant $OPTS -f java/debugger.jpda.ui test + - name: debugger.jpda.ui + run: ant $OPTS -f java/debugger.jpda.ui test-unit - name: Create Test Summary uses: test-summary/action@31493c76ec9e7aa675f1585d3ed6f1da69269a86 # v2.4 diff --git a/ide/api.lsp/apichanges.xml b/ide/api.lsp/apichanges.xml index 2fa4b8a31d9f..1d953d50f986 100644 --- a/ide/api.lsp/apichanges.xml +++ b/ide/api.lsp/apichanges.xml @@ -51,6 +51,19 @@ + + + Adding InlineValue and InlineValuesProvider + + + + + + The InlineValuesProvider is introduced to provide debugger + inline values in the the LSP protocol. + + + Adding InlayHintsProvider diff --git a/ide/api.lsp/manifest.mf b/ide/api.lsp/manifest.mf index 215b31c6f0f8..ba673ca61bc9 100644 --- a/ide/api.lsp/manifest.mf +++ b/ide/api.lsp/manifest.mf @@ -1,5 +1,5 @@ Manifest-Version: 1.0 OpenIDE-Module: org.netbeans.api.lsp/1 OpenIDE-Module-Localizing-Bundle: org/netbeans/api/lsp/Bundle.properties -OpenIDE-Module-Specification-Version: 1.34 +OpenIDE-Module-Specification-Version: 1.35 AutoUpdate-Show-In-Client: false diff --git a/ide/api.lsp/nbproject/project.properties b/ide/api.lsp/nbproject/project.properties index 0e0574c19817..9f5c53ed9300 100644 --- a/ide/api.lsp/nbproject/project.properties +++ b/ide/api.lsp/nbproject/project.properties @@ -15,7 +15,7 @@ # specific language governing permissions and limitations # under the License. is.autoload=true -javac.release=11 +javac.release=17 javac.compilerargs=-Xlint -Xlint:-serial javadoc.name=LSP APIs javadoc.title=LSP APIs diff --git a/ide/api.lsp/src/org/netbeans/api/lsp/InlineValue.java b/ide/api.lsp/src/org/netbeans/api/lsp/InlineValue.java new file mode 100644 index 000000000000..2cf55ac104f5 --- /dev/null +++ b/ide/api.lsp/src/org/netbeans/api/lsp/InlineValue.java @@ -0,0 +1,58 @@ +/* + * 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.api.lsp; + +/** + * An expression whose value may be shown inline while debugging. + * + * @since 1.35 + */ +public final class InlineValue { + private final Range range; + private final String expression; + + private InlineValue(Range range, String expression) { + this.range = range; + this.expression = expression; + } + + /** + * {@return Range to which the inline value applies} + */ + public Range getRange() { + return range; + } + + /** + * {@return The expression of that should be evaluated for the inline value.} + */ + public String getExpression() { + return expression; + } + + /** + * {@return a new instance of {@code InlineValue}, based on the provided information.} + * + * @param range range to which the inline value should apply + * @param expression expression that should be evaluted + */ + public static InlineValue createInlineVariable(Range range, String expression) { + return new InlineValue(range, expression); + } +} diff --git a/ide/api.lsp/src/org/netbeans/spi/lsp/InlineValuesProvider.java b/ide/api.lsp/src/org/netbeans/spi/lsp/InlineValuesProvider.java new file mode 100644 index 000000000000..a7da33441512 --- /dev/null +++ b/ide/api.lsp/src/org/netbeans/spi/lsp/InlineValuesProvider.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.netbeans.spi.lsp; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import org.netbeans.api.annotations.common.NonNull; +import org.netbeans.api.lsp.InlineValue; +import org.openide.filesystems.FileObject; + +/** + * Compute {@link InlineValue}s for the given file and offset. + * + * @since 1.35 + */ +public interface InlineValuesProvider { + + /** + * Compute {@linkplain InlineValue}s for the given file and location. + * + * @param file file for which the inline values should be computed + * @param currentExecutionPosition position for which the inline values should be computed + * @return the computed inline values + */ + public CompletableFuture> inlineValues(@NonNull FileObject file, int currentExecutionPosition); +} diff --git a/ide/editor.actions/src/org/netbeans/modules/editor/actions/ShowInlineHintsAction.java b/ide/editor.actions/src/org/netbeans/modules/editor/actions/ShowInlineHintsAction.java index a97a8efeaa1b..c2ad3468a4d5 100644 --- a/ide/editor.actions/src/org/netbeans/modules/editor/actions/ShowInlineHintsAction.java +++ b/ide/editor.actions/src/org/netbeans/modules/editor/actions/ShowInlineHintsAction.java @@ -24,7 +24,7 @@ @EditorActionRegistration(name="toggle-inline-hints", menuPath="View", - menuPosition=899, + menuPosition=897, preferencesKey=ShowInlineHintsAction.KEY_LINES, preferencesDefault=ShowInlineHintsAction.DEF_LINES) public class ShowInlineHintsAction extends AbstractAction { diff --git a/ide/editor.actions/src/org/netbeans/modules/editor/actions/ShowLinesAction.java b/ide/editor.actions/src/org/netbeans/modules/editor/actions/ShowLinesAction.java index 6cd466dd7b37..9b71b51e3829 100644 --- a/ide/editor.actions/src/org/netbeans/modules/editor/actions/ShowLinesAction.java +++ b/ide/editor.actions/src/org/netbeans/modules/editor/actions/ShowLinesAction.java @@ -24,7 +24,7 @@ @EditorActionRegistration(name="toggle-lines-view", menuPath="View", - menuPosition=898, + menuPosition=895, preferencesKey=ShowLinesAction.KEY_LINES, preferencesDefault=ShowLinesAction.DEF_LINES) public class ShowLinesAction extends AbstractAction { diff --git a/ide/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/HighlightsList.java b/ide/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/HighlightsList.java index a97a9598ed37..578e9bf0b88a 100644 --- a/ide/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/HighlightsList.java +++ b/ide/editor.lib2/src/org/netbeans/modules/editor/lib2/highlighting/HighlightsList.java @@ -121,7 +121,7 @@ public void add(HighlightItem item) { * @param usePrependText reflect the prepended text setting. * @return either simple or compound attribute set. */ - public AttributeSet cutSameFont(Font defaultFont, int maxEndOffset, int wsEndOffset, CharSequence docText, boolean usePrependText) { + public AttributeSet cutSameFont(Font defaultFont, int maxEndOffset, int wsEndOffset, CharSequence docText) { assert (maxEndOffset <= endOffset()) : "maxEndOffset=" + maxEndOffset + " > endOffset()=" + endOffset() + ", " + this; // NOI18N HighlightItem item = get(0); @@ -159,13 +159,13 @@ public AttributeSet cutSameFont(Font defaultFont, int maxEndOffset, int wsEndOff // Extends beyond first highlight Font firstFont = ViewUtils.getFont(firstAttrs, defaultFont); - Object firstPrependText = usePrependText && firstAttrs != null ? firstAttrs.getAttribute(ViewUtils.KEY_VIRTUAL_TEXT_PREPEND) : null; + Object firstPrependText = firstAttrs != null ? firstAttrs.getAttribute(ViewUtils.KEY_VIRTUAL_TEXT_PREPEND) : null; int index = 1; while (true) { item = get(index); AttributeSet attrs = item.getAttributes(); Font font = ViewUtils.getFont(attrs, defaultFont); - Object prependText = usePrependText && attrs != null ? attrs.getAttribute(ViewUtils.KEY_VIRTUAL_TEXT_PREPEND) : null; + Object prependText = attrs != null ? attrs.getAttribute(ViewUtils.KEY_VIRTUAL_TEXT_PREPEND) : null; if (!font.equals(firstFont) || !Objects.equals(firstPrependText, prependText)) { // Stop at itemEndOffset if (index == 1) { // Just single attribute set cutStartItems(1); diff --git a/ide/editor.lib2/src/org/netbeans/modules/editor/lib2/view/DocumentViewOp.java b/ide/editor.lib2/src/org/netbeans/modules/editor/lib2/view/DocumentViewOp.java index 08ad998762c0..cfe34feb58e6 100644 --- a/ide/editor.lib2/src/org/netbeans/modules/editor/lib2/view/DocumentViewOp.java +++ b/ide/editor.lib2/src/org/netbeans/modules/editor/lib2/view/DocumentViewOp.java @@ -317,7 +317,6 @@ public final class DocumentViewOp boolean asTextField; private boolean guideLinesEnable; - private boolean inlineHintsEnable; private int indentLevelSize; @@ -924,13 +923,10 @@ public void run() { // Line height correction float lineHeightCorrectionOrig = rowHeightCorrection; rowHeightCorrection = prefs.getFloat(SimpleValueNames.LINE_HEIGHT_CORRECTION, 1.0f); - boolean inlineHintsEnableOrig = inlineHintsEnable; - inlineHintsEnable = prefs.getBoolean("enable.inline.hints", false); // NOI18N boolean updateMetrics = (rowHeightCorrection != lineHeightCorrectionOrig); boolean releaseChildren = nonInitialUpdate && ((nonPrintableCharactersVisible != nonPrintableCharactersVisibleOrig) || - (rowHeightCorrection != lineHeightCorrectionOrig) || - (inlineHintsEnable != inlineHintsEnableOrig)); + (rowHeightCorrection != lineHeightCorrectionOrig)); indentLevelSize = getIndentSize(); tabSize = prefs.getInt(SimpleValueNames.TAB_SIZE, EditorPreferencesDefaults.defaultTabSize); if (updateMetrics) { @@ -1172,10 +1168,6 @@ public boolean isGuideLinesEnable() { return guideLinesEnable && !asTextField; } - public boolean isInlineHintsEnable() { - return inlineHintsEnable; - } - public int getIndentLevelSize() { return indentLevelSize; } diff --git a/ide/editor.lib2/src/org/netbeans/modules/editor/lib2/view/HighlightsViewFactory.java b/ide/editor.lib2/src/org/netbeans/modules/editor/lib2/view/HighlightsViewFactory.java index fce916dd5bba..8d0c0b8631f2 100644 --- a/ide/editor.lib2/src/org/netbeans/modules/editor/lib2/view/HighlightsViewFactory.java +++ b/ide/editor.lib2/src/org/netbeans/modules/editor/lib2/view/HighlightsViewFactory.java @@ -218,8 +218,7 @@ public EditorView createView(int startOffset, int limitOffset, boolean forcedLim } } - boolean inlineHints = documentView().op.isInlineHintsEnable(); - AttributeSet attrs = hList.cutSameFont(defaultFont, limitOffset, wsEndOffset, docText, inlineHints); + AttributeSet attrs = hList.cutSameFont(defaultFont, limitOffset, wsEndOffset, docText); int length = hList.startOffset() - startOffset; EditorView view = wrapWithPrependedText(new HighlightsView(length, attrs), attrs); EditorView origViewUnwrapped = origView instanceof PrependedTextView ? ((PrependedTextView) origView).getDelegate() : origView; @@ -255,9 +254,7 @@ public EditorView createView(int startOffset, int limitOffset, boolean forcedLim } private @NonNull EditorView wrapWithPrependedText(@NonNull EditorView origView, @NullAllowed AttributeSet attrs) { - boolean inlineHints = documentView().op.isInlineHintsEnable(); - - if (attrs != null && inlineHints && attrs.getAttribute(ViewUtils.KEY_VIRTUAL_TEXT_PREPEND) instanceof String) { + if (attrs != null && attrs.getAttribute(ViewUtils.KEY_VIRTUAL_TEXT_PREPEND) instanceof String) { return new PrependedTextView(documentView().op, attrs, origView); } diff --git a/ide/editor.lib2/src/org/netbeans/spi/editor/highlighting/support/HighlightsContainers.java b/ide/editor.lib2/src/org/netbeans/spi/editor/highlighting/support/HighlightsContainers.java new file mode 100644 index 000000000000..5447ab950347 --- /dev/null +++ b/ide/editor.lib2/src/org/netbeans/spi/editor/highlighting/support/HighlightsContainers.java @@ -0,0 +1,80 @@ +/* + * 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.spi.editor.highlighting.support; + +import java.util.prefs.PreferenceChangeEvent; +import java.util.prefs.PreferenceChangeListener; +import java.util.prefs.Preferences; +import javax.swing.text.Document; +import org.netbeans.api.editor.mimelookup.MimeLookup; +import org.netbeans.lib.editor.util.swing.DocumentUtilities; +import org.netbeans.spi.editor.highlighting.HighlightsContainer; +import org.netbeans.spi.editor.highlighting.HighlightsSequence; +import org.openide.util.WeakListeners; + +public class HighlightsContainers { + public static HighlightsContainer inlineHintsSettingAwareContainer(Document doc, HighlightsContainer delegate) { + class InlineHintsSettingsAwareContainer extends AbstractHighlightsContainer implements PreferenceChangeListener { + private static final String KEY_ENABLE_INLINE_HINTS = "enable.inline.hints"; + + private final Preferences prefs; + private boolean enabled; + + public InlineHintsSettingsAwareContainer() { + String mimeType = DocumentUtilities.getMimeType(doc); + prefs = MimeLookup.getLookup(mimeType).lookup(Preferences.class); + prefs.addPreferenceChangeListener(WeakListeners.create(PreferenceChangeListener.class, this, prefs)); + inlineSettingChanged(); + } + + @Override + public HighlightsSequence getHighlights(int startOffset, int endOffset) { + synchronized (this) { + if (!enabled) { + return HighlightsSequence.EMPTY; + } + } + return delegate.getHighlights(startOffset, endOffset); + } + + @Override + public void preferenceChange(PreferenceChangeEvent evt) { + if (KEY_ENABLE_INLINE_HINTS.equals(evt.getKey())) { + inlineSettingChanged(); + } + } + + private void inlineSettingChanged() { + synchronized (this) { + boolean newValue = prefs.getBoolean(KEY_ENABLE_INLINE_HINTS, false); + + if (enabled == newValue) { + return ; + } + + enabled = newValue; + } + + fireHighlightsChange(0, doc.getLength()); + } + } + + return new InlineHintsSettingsAwareContainer(); + } +} diff --git a/ide/editor.lib2/test/unit/src/org/netbeans/modules/editor/lib2/highlighting/HighlightsListTest.java b/ide/editor.lib2/test/unit/src/org/netbeans/modules/editor/lib2/highlighting/HighlightsListTest.java index 86e06938767b..f0ec2912a311 100644 --- a/ide/editor.lib2/test/unit/src/org/netbeans/modules/editor/lib2/highlighting/HighlightsListTest.java +++ b/ide/editor.lib2/test/unit/src/org/netbeans/modules/editor/lib2/highlighting/HighlightsListTest.java @@ -68,7 +68,7 @@ public void testSimple() throws Exception { HighlightsList hList = highlightsListSimple(doc); // Fetch first - AttributeSet attrs = hList.cutSameFont(defaultFont, 10, 10, null, false); + AttributeSet attrs = hList.cutSameFont(defaultFont, 10, 10, null); assert (attrs instanceof CompoundAttributes) : "Non-CompoundAttributes attrs=" + attrs; CompoundAttributes cAttrs = (CompoundAttributes) attrs; assert (cAttrs.startOffset() == 0) : "startOffset=" + cAttrs.startOffset(); @@ -79,21 +79,21 @@ public void testSimple() throws Exception { assertItem(items[2], 6, null); // Fetch next assert (hList.startOffset() == 6); - attrs = hList.cutSameFont(defaultFont, 10, 10, null, false); + attrs = hList.cutSameFont(defaultFont, 10, 10, null); assert !(attrs instanceof CompoundAttributes); assert attrs == attrSets[1]; assert (hList.startOffset() == 8); - attrs = hList.cutSameFont(defaultFont, 10, 10, null, false); + attrs = hList.cutSameFont(defaultFont, 10, 10, null); assert !(attrs instanceof CompoundAttributes); assert (attrs == null); assert (hList.startOffset() == 10); hList = highlightsListSimple(doc); - attrs = hList.cutSameFont(defaultFont, 2, 2, null, false); + attrs = hList.cutSameFont(defaultFont, 2, 2, null); assert !(attrs instanceof CompoundAttributes); assert (attrs == null); - attrs = hList.cutSameFont(defaultFont, 10, 10, null, false); + attrs = hList.cutSameFont(defaultFont, 10, 10, null); assert (hList.startOffset() == 6); assert (attrs instanceof CompoundAttributes) : "Non-CompoundAttributes attrs=" + attrs; cAttrs = (CompoundAttributes) attrs; @@ -104,7 +104,7 @@ public void testSimple() throws Exception { assertItem(items[1], 6, null); hList = highlightsListSimple(doc); - attrs = hList.cutSameFont(defaultFont, 3, 3, null, false); + attrs = hList.cutSameFont(defaultFont, 3, 3, null); cAttrs = (CompoundAttributes) attrs; assert (cAttrs.startOffset() == 0) : "startOffset=" + cAttrs.startOffset(); items = cAttrs.highlightItems(); @@ -112,7 +112,7 @@ public void testSimple() throws Exception { assertItem(items[0], 2, null); assertItem(items[1], 3, attrSets[0]); // Next - attrs = hList.cutSameFont(defaultFont, 5, 5, null, false); + attrs = hList.cutSameFont(defaultFont, 5, 5, null); cAttrs = (CompoundAttributes) attrs; assert (cAttrs.startOffset() == 3) : "startOffset=" + cAttrs.startOffset(); items = cAttrs.highlightItems(); @@ -120,22 +120,22 @@ public void testSimple() throws Exception { assertItem(items[0], 4, attrSets[0]); assertItem(items[1], 5, null); // Next - attrs = hList.cutSameFont(defaultFont, 7, 7, null, false); + attrs = hList.cutSameFont(defaultFont, 7, 7, null); assert !(attrs instanceof CompoundAttributes); assert (attrs == null); assert (hList.startOffset() == 6); // Next - attrs = hList.cutSameFont(defaultFont, 7, 7, null, false); + attrs = hList.cutSameFont(defaultFont, 7, 7, null); assert !(attrs instanceof CompoundAttributes); assert (attrs == attrSets[1]); assert (hList.startOffset() == 7); // Next - attrs = hList.cutSameFont(defaultFont, 10, 10, null, false); + attrs = hList.cutSameFont(defaultFont, 10, 10, null); assert !(attrs instanceof CompoundAttributes); assert (attrs == attrSets[1]); assert (hList.startOffset() == 8); // Next - attrs = hList.cutSameFont(defaultFont, 10, 10, null, false); + attrs = hList.cutSameFont(defaultFont, 10, 10, null); assert !(attrs instanceof CompoundAttributes); assert (attrs == null); assert (hList.startOffset() == 10); @@ -182,7 +182,7 @@ public void testSplitPrependText() throws Exception { HighlightsList hList = reader.highlightsList(); // Fetch first - AttributeSet attrs = hList.cutSameFont(defaultFont, end, end, null, false); + AttributeSet attrs = hList.cutSameFont(defaultFont, end, end, null); assert (attrs instanceof CompoundAttributes) : "Non-CompoundAttributes attrs=" + attrs; CompoundAttributes cAttrs = (CompoundAttributes) attrs; assert (cAttrs.startOffset() == 0) : "startOffset=" + cAttrs.startOffset(); @@ -194,11 +194,11 @@ public void testSplitPrependText() throws Exception { assertItem(items[3], 8, null); // Fetch next assert (hList.startOffset() == 8); - attrs = hList.cutSameFont(defaultFont, end, end, null, false); + attrs = hList.cutSameFont(defaultFont, end, end, null); assert !(attrs instanceof CompoundAttributes); assert attrs == attrs3; assert (hList.startOffset() == 10); - attrs = hList.cutSameFont(defaultFont, end, end, null, false); + attrs = hList.cutSameFont(defaultFont, end, end, null); assert !(attrs instanceof CompoundAttributes); assert (attrs == null); assert (hList.startOffset() == 14); @@ -210,15 +210,15 @@ public void testSplitPrependText() throws Exception { HighlightsList hList = reader.highlightsList(); // Fetch first - AttributeSet attrs = hList.cutSameFont(defaultFont, end, end, null, true); + AttributeSet attrs = hList.cutSameFont(defaultFont, end, end, null); assert !(attrs instanceof CompoundAttributes) : "CompoundAttributes attrs=" + attrs; assert attrs == attrs1; assert (hList.startOffset() == 2); - attrs = hList.cutSameFont(defaultFont, end, end, null, true); + attrs = hList.cutSameFont(defaultFont, end, end, null); assert !(attrs instanceof CompoundAttributes); assert (attrs == attrs2); assert (hList.startOffset() == 4); - attrs = hList.cutSameFont(defaultFont, end, end, null, true); + attrs = hList.cutSameFont(defaultFont, end, end, null); assert (attrs instanceof CompoundAttributes) : "Non-CompoundAttributes attrs=" + attrs; CompoundAttributes cAttrs = (CompoundAttributes) attrs; assert (cAttrs.startOffset() == 4) : "startOffset=" + cAttrs.startOffset(); @@ -228,11 +228,11 @@ public void testSplitPrependText() throws Exception { assertItem(items[1], 8, null); // Fetch next assert (hList.startOffset() == 8); - attrs = hList.cutSameFont(defaultFont, end, end, null, true); + attrs = hList.cutSameFont(defaultFont, end, end, null); assert !(attrs instanceof CompoundAttributes); assert attrs == attrs3; assert (hList.startOffset() == 10); - attrs = hList.cutSameFont(defaultFont, end, end, null, true); + attrs = hList.cutSameFont(defaultFont, end, end, null); assert !(attrs instanceof CompoundAttributes); assert (attrs == null); assert (hList.startOffset() == 14); diff --git a/ide/spi.debugger.ui/apichanges.xml b/ide/spi.debugger.ui/apichanges.xml index 9c01659b7761..618b8cda0d19 100644 --- a/ide/spi.debugger.ui/apichanges.xml +++ b/ide/spi.debugger.ui/apichanges.xml @@ -51,6 +51,23 @@ + + + Constants class enhanced with constants for inline values. + + + + + +

+ Two new constants are added to Constants: + KEY_INLINE_VALUES: the Preferences key for the inline value setting. + DEF_INLINE_VALUES: the default value of the inline value setting. +

+
+ +
+ BreakpointAnnotation class added. @@ -209,7 +226,7 @@ A method for retrieving a set of language MIME types on a line in a document is added. - + @@ -248,7 +265,7 @@ Frames added into DebuggingView. - + @@ -280,7 +297,7 @@ Add a way not to persist breakpoints and watches. - + diff --git a/ide/spi.debugger.ui/manifest.mf b/ide/spi.debugger.ui/manifest.mf index 4f6053fbc3be..87834de70921 100644 --- a/ide/spi.debugger.ui/manifest.mf +++ b/ide/spi.debugger.ui/manifest.mf @@ -2,6 +2,6 @@ Manifest-Version: 1.0 OpenIDE-Module: org.netbeans.spi.debugger.ui/1 OpenIDE-Module-Localizing-Bundle: org/netbeans/modules/debugger/ui/Bundle.properties OpenIDE-Module-Layer: org/netbeans/modules/debugger/resources/mf-layer.xml -OpenIDE-Module-Specification-Version: 2.87 +OpenIDE-Module-Specification-Version: 2.88 OpenIDE-Module-Provides: org.netbeans.spi.debugger.ui OpenIDE-Module-Install: org/netbeans/modules/debugger/ui/DebuggerModule.class diff --git a/ide/spi.debugger.ui/nbproject/project.properties b/ide/spi.debugger.ui/nbproject/project.properties index 863151bde8cb..91c069f2dd47 100644 --- a/ide/spi.debugger.ui/nbproject/project.properties +++ b/ide/spi.debugger.ui/nbproject/project.properties @@ -19,6 +19,7 @@ is.autoload=true javac.compilerargs=-Xlint -Xlint:-serial javac.source=1.8 javadoc.arch=${basedir}/arch.xml +javadoc.apichanges=${basedir}/apichanges.xml test.config.stableBTD.includes=**/*Test.class test.config.stableBTD.excludes=\ diff --git a/ide/spi.debugger.ui/src/org/netbeans/modules/debugger/ui/actions/ShowInlineValuesAction.java b/ide/spi.debugger.ui/src/org/netbeans/modules/debugger/ui/actions/ShowInlineValuesAction.java new file mode 100644 index 000000000000..122591df1c5e --- /dev/null +++ b/ide/spi.debugger.ui/src/org/netbeans/modules/debugger/ui/actions/ShowInlineValuesAction.java @@ -0,0 +1,41 @@ +/* + * 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.debugger.ui.actions; + +import java.awt.event.ActionEvent; +import javax.swing.AbstractAction; +import org.netbeans.api.editor.EditorActionRegistration; +import org.netbeans.spi.debugger.ui.Constants; +import org.openide.util.NbBundle.Messages; + +@EditorActionRegistration(name="toggle-inline-values", + menuPath="View", + menuPosition=899, + preferencesKey=Constants.KEY_INLINE_VALUES, + preferencesDefault=Constants.DEF_INLINE_VALUES) +@Messages({ + "toggle-inline-values=Show Inline Values", + "toggle-inline-values_menu_text=Show Inline V&alues" +}) +public class ShowInlineValuesAction extends AbstractAction { + @Override + public void actionPerformed(ActionEvent e) { + } + +} diff --git a/ide/spi.debugger.ui/src/org/netbeans/spi/debugger/ui/Constants.java b/ide/spi.debugger.ui/src/org/netbeans/spi/debugger/ui/Constants.java index efe31270aed7..c89a8084dce3 100644 --- a/ide/spi.debugger.ui/src/org/netbeans/spi/debugger/ui/Constants.java +++ b/ide/spi.debugger.ui/src/org/netbeans/spi/debugger/ui/Constants.java @@ -149,4 +149,16 @@ public interface Constants { * @see org.netbeans.spi.viewmodel.ColumnModel#getNextColumnID */ public static final String SESSION_LANGUAGE_COLUMN_ID = "SessionLanguage"; + + /**Should the inline values be visible - preferences key. + * + * @since 2.88 + */ + public static final String KEY_INLINE_VALUES = "enable.inline.values"; + + /**Should the inline values be visible - default value. + * + * @since 2.88 + */ + public static final boolean DEF_INLINE_VALUES = true; } diff --git a/java/debugger.jpda.ui/nbproject/project.properties b/java/debugger.jpda.ui/nbproject/project.properties index 298dd64d1b56..d90f014facdf 100644 --- a/java/debugger.jpda.ui/nbproject/project.properties +++ b/java/debugger.jpda.ui/nbproject/project.properties @@ -17,7 +17,7 @@ cp.extra=${tools.jar} javac.compilerargs=-Xlint:unchecked -javac.source=1.8 +javac.release=17 javadoc.arch=${basedir}/arch.xml requires.nb.javac=true diff --git a/java/debugger.jpda.ui/nbproject/project.xml b/java/debugger.jpda.ui/nbproject/project.xml index ecd74d3025b8..a353f9e44341 100644 --- a/java/debugger.jpda.ui/nbproject/project.xml +++ b/java/debugger.jpda.ui/nbproject/project.xml @@ -60,6 +60,15 @@ 1.25 + + org.netbeans.api.lsp + + + + 1 + 1.31 + + org.netbeans.api.progress @@ -137,6 +146,24 @@ 1.25 + + org.netbeans.modules.editor.settings + + + + 1 + 1.84 + + + + org.netbeans.modules.editor.util + + + + 1 + 1.92 + + org.netbeans.modules.java.preprocessorbridge @@ -179,6 +206,15 @@ 1.0 + + org.netbeans.modules.parsing.api + + + + 1 + 9.34 + + org.netbeans.modules.projectapi @@ -343,6 +379,45 @@ org.netbeans.modules.projectapi.nb + + unit + + org.netbeans.libs.junit4 + + + + org.netbeans.modules.editor.mimelookup + + + + + org.netbeans.modules.java.j2seplatform + + + + org.netbeans.modules.java.source.base + + + + + org.netbeans.modules.nbjunit + + + + + org.netbeans.modules.parsing.indexing + + + + + org.netbeans.modules.parsing.nb + + + + org.netbeans.modules.projectapi.nb + + + org.netbeans.modules.debugger.jpda.truffle diff --git a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/models/InlineValueComputerImpl.java b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/models/InlineValueComputerImpl.java new file mode 100644 index 000000000000..4badb40d0297 --- /dev/null +++ b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/models/InlineValueComputerImpl.java @@ -0,0 +1,463 @@ +/* + * 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.debugger.jpda.ui.models; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.net.MalformedURLException; +import java.net.URI; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.prefs.PreferenceChangeEvent; +import java.util.prefs.PreferenceChangeListener; +import java.util.prefs.Preferences; +import java.util.stream.Collectors; +import javax.swing.text.AttributeSet; +import javax.swing.text.Document; +import org.netbeans.api.debugger.DebuggerManagerAdapter; +import org.netbeans.api.debugger.LazyDebuggerManagerListener; +import org.netbeans.api.debugger.Session; +import org.netbeans.api.debugger.jpda.CallStackFrame; +import org.netbeans.api.debugger.jpda.InvalidExpressionException; +import org.netbeans.api.debugger.jpda.JPDAClassType; +import org.netbeans.api.debugger.jpda.JPDADebugger; +import org.netbeans.api.debugger.jpda.ObjectVariable; +import org.netbeans.api.debugger.jpda.Variable; +import org.netbeans.api.editor.mimelookup.MimeLookup; +import org.netbeans.api.editor.mimelookup.MimeRegistration; +import org.netbeans.api.editor.settings.AttributesUtilities; +import org.netbeans.api.java.source.CancellableTask; +import org.netbeans.api.java.source.CompilationInfo; +import org.netbeans.api.java.source.JavaSource.Phase; +import org.netbeans.api.java.source.JavaSource.Priority; +import org.netbeans.api.java.source.JavaSourceTaskFactory; +import org.netbeans.modules.debugger.jpda.JPDADebuggerImpl; +import org.netbeans.modules.debugger.jpda.expr.formatters.Formatters; +import org.netbeans.modules.debugger.jpda.expr.formatters.FormattersLoopControl; +import org.netbeans.modules.debugger.jpda.expr.formatters.VariablesFormatter; +import org.netbeans.modules.debugger.jpda.jdi.InternalExceptionWrapper; +import org.netbeans.modules.debugger.jpda.jdi.InvalidStackFrameExceptionWrapper; +import org.netbeans.modules.debugger.jpda.jdi.ObjectCollectedExceptionWrapper; +import org.netbeans.modules.debugger.jpda.jdi.VMDisconnectedExceptionWrapper; +import org.netbeans.modules.debugger.jpda.ui.values.ComputeInlineValues; +import org.netbeans.modules.debugger.jpda.ui.values.ComputeInlineValues.InlineVariable; +import org.netbeans.modules.parsing.spi.TaskIndexingMode; +import org.netbeans.spi.debugger.DebuggerServiceRegistration; +import org.netbeans.spi.debugger.ui.Constants; +import org.netbeans.spi.editor.highlighting.HighlightsLayer; +import org.netbeans.spi.editor.highlighting.HighlightsLayerFactory; +import org.netbeans.spi.editor.highlighting.ZOrder; +import org.netbeans.spi.editor.highlighting.support.OffsetsBag; +import org.openide.cookies.EditorCookie; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.URLMapper; +import org.openide.util.Exceptions; +import org.openide.util.Lookup; +import org.openide.util.RequestProcessor; +import org.openide.util.WeakListeners; +import org.openide.util.lookup.ServiceProvider; +import org.openide.util.lookup.ServiceProviders; + + +public class InlineValueComputerImpl implements PreferenceChangeListener, PropertyChangeListener { + + private static final Logger LOG = Logger.getLogger(InlineValueComputerImpl.class.getName()); + private static final RequestProcessor EVALUATOR = new RequestProcessor(InlineValueComputerImpl.class.getName(), 1, false, false); + private static final String JAVA_STRATUM = "Java"; //XXX: this is probably already defined somewhere + private final JPDADebuggerImpl debugger; + private final Preferences prefs; + private TaskDescription currentTask; + + private InlineValueComputerImpl(Session session) { + debugger = (JPDADebuggerImpl) session.lookupFirst(null, JPDADebugger.class); + debugger.addPropertyChangeListener(this); + prefs = MimeLookup.getLookup("text/x-java").lookup(Preferences.class); + prefs.addPreferenceChangeListener(WeakListeners.create(PreferenceChangeListener.class, this, prefs)); + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + if (JPDADebugger.PROP_STATE.equals(evt.getPropertyName()) && + debugger.getState() == JPDADebugger.STATE_DISCONNECTED) { + setNewTask(null); + } + + if (JPDADebugger.PROP_CURRENT_CALL_STACK_FRAME.equals(evt.getPropertyName())) { + refreshVariables(); + } + } + + @Override + public void preferenceChange(PreferenceChangeEvent evt) { + refreshVariables(); + } + + private void refreshVariables() { + CallStackFrame frame = debugger.getCurrentCallStackFrame(); + + FileObject frameFile = null; + int frameLineNumber = -1; + Document frameDocument = null; + + if (prefs.getBoolean(Constants.KEY_INLINE_VALUES, Constants.DEF_INLINE_VALUES) && + frame != null && !frame.isObsolete() && + frame.getThread().isSuspended() && + JAVA_STRATUM.equals(frame.getDefaultStratum())) { + try { + String url = debugger.getEngineContext().getURL(frame, JAVA_STRATUM); + frameFile = url != null ? URLMapper.findFileObject(URI.create(url).toURL()) : null; + if (frameFile != null) { + frameLineNumber = frame.getLineNumber(JAVA_STRATUM); + EditorCookie ec = frameFile.getLookup().lookup(EditorCookie.class); + frameDocument = ec != null ? ec.getDocument() : null; + } + } catch (InternalExceptionWrapper | InvalidStackFrameExceptionWrapper | ObjectCollectedExceptionWrapper | VMDisconnectedExceptionWrapper | MalformedURLException ex) { + Exceptions.printStackTrace(ex); + } + } + + TaskDescription newTask; + + if (frameFile != null && frameDocument != null) { + newTask = new TaskDescription(frameFile, frameLineNumber, frameDocument); + } else { + newTask = null; + } + + if (setNewTask(newTask)) { + return; + } + + if (newTask != null) { + CountDownLatch computationDone = new CountDownLatch(1); + + newTask.addCancelCallback(computationDone::countDown); + + AtomicReference> values = new AtomicReference<>(); + + EVALUATOR.post(() -> { + OffsetsBag runningBag = new OffsetsBag(newTask.frameDocument); + + Lookup.getDefault().lookup(ComputeInlineVariablesFactory.class).set(newTask.frameFile, newTask.frameLineNumber, variables -> { + values.set(variables); + computationDone.countDown(); + }); + + try { + computationDone.await(); + } catch (InterruptedException ex) { + Exceptions.printStackTrace(ex); + } + + if (newTask.isCancelled()) { + return ; + } + + Collection variables = values.get(); + + if (variables == null) { + return ; + } + + Map expression2Value = new HashMap<>(); + Map> line2Values = new HashMap<>(); + + for (InlineVariable v : variables) { + if (newTask.isCancelled()) { + return ; + } + + Variable value = expression2Value.computeIfAbsent(v.expression(), expr -> { + try { + return debugger.evaluate(expr); + } catch (InvalidExpressionException ex) { + //the variable may not exist + LOG.log(Level.FINE, null, ex); + return null; + } + }); + if (value != null) { + String valueText; + if (value instanceof ObjectVariable ov) { + valueText = toValue(ov).replace("\n", "\\n"); + } else { + valueText = value.getValue(); + } + line2Values.computeIfAbsent(v.lineEnd(), __ -> new LinkedHashMap<>()) + .putIfAbsent(v.expression(), v.expression() + " = " + valueText); + String mergedValues = line2Values.get(v.lineEnd()).values().stream().collect(Collectors.joining(", ", " ", "")); + AttributeSet attrs = AttributesUtilities.createImmutable("virtual-text-prepend", mergedValues); + + runningBag.addHighlight(v.lineEnd(), v.lineEnd() + 1, attrs); + + setHighlights(newTask, runningBag); + } + } + }); + } + } + + private String toValue(ObjectVariable variable) { + //mostly copied from the VariablesFormatterFilter.getValueAt: + FormattersLoopControl formattersLoop = new FormattersLoopControl(); + String type = variable.getType (); + ObjectVariable ov = (ObjectVariable) variable; + JPDAClassType ct = ov.getClassType(); + if (ct == null) { + return ov.getValue(); + } + VariablesFormatter f = Formatters.getFormatterForType(ct, formattersLoop.getFormatters()); + String[] formattersInLoopRef = new String[] { null }; + if (f != null && formattersLoop.canUse(f, ct.getName(), formattersInLoopRef)) { + String code = f.getValueFormatCode(); + if (code != null && code.length() > 0) { + try { + java.lang.reflect.Method evaluateMethod = ov.getClass().getMethod("evaluate", String.class); + evaluateMethod.setAccessible(true); + Variable ret = (Variable) evaluateMethod.invoke(ov, code); + if (ret == null) { + return null; + } + return ret.getValue(); + } catch (java.lang.reflect.InvocationTargetException itex) { + Throwable t = itex.getTargetException(); + if (t instanceof InvalidExpressionException) { + return ov.getValue(); + } else { + Exceptions.printStackTrace(t); + } + } catch (ReflectiveOperationException ex) { + Exceptions.printStackTrace(ex); + } + } + } else if (formattersInLoopRef[0] != null) { + //ignore loops(?) + } + if (VariablesFormatterFilter.isToStringValueType(type)) { + try { + return "\""+ov.getToStringValue ()+"\""; + } catch (InvalidExpressionException ex) { + // Not a supported operation (e.g. J2ME, see #45543) + // Or missing context or any other reason + Logger.getLogger(VariablesFormatterFilter.class.getName()).fine("getToStringValue() "+ex.getLocalizedMessage()); + if (ex.getTargetException () instanceof UnsupportedOperationException) { + // PATCH for J2ME. see 45543 + return ov.getValue(); + } + return ex.getLocalizedMessage (); + } + } + return ov.getValue(); + } + + private synchronized boolean setNewTask(TaskDescription newTask) { + if (Objects.equals(currentTask, newTask)) { + return true; //nothing changed, nothing to do + } + + if (currentTask != null) { + currentTask.cancel(); + getHighlightsBag(currentTask.frameDocument).clear(); + } + + currentTask = newTask; + + return false; + } + + private synchronized void setHighlights(TaskDescription task, OffsetsBag highlights) { + if (!task.isCancelled()) { + getHighlightsBag(currentTask.frameDocument).setHighlights(highlights); + } + } + + @DebuggerServiceRegistration(types=LazyDebuggerManagerListener.class) + public static final class Init extends DebuggerManagerAdapter { + @Override + public void sessionAdded(Session session) { + new InlineValueComputerImpl(session); + } + } + + @ServiceProviders({ + @ServiceProvider(service=JavaSourceTaskFactory.class), + @ServiceProvider(service=ComputeInlineVariablesFactory.class) + }) + public static final class ComputeInlineVariablesFactory extends JavaSourceTaskFactory { + + private FileObject currentFile; + private int currentLineNumber; + private Consumer> currentTarget; + + public ComputeInlineVariablesFactory() { + super(Phase.UP_TO_DATE, Priority.NORMAL, TaskIndexingMode.ALLOWED_DURING_SCAN); + } + + @Override + protected CancellableTask createTask(FileObject file) { + return new CancellableTask() { + private final AtomicBoolean cancel = new AtomicBoolean(); + + @Override + public void cancel() { + cancel.set(true); + } + + @Override + public void run(CompilationInfo info) throws Exception { + cancel.set(false); + + int line; + Consumer> target; + + synchronized (ComputeInlineVariablesFactory.this) { + if (!info.getFileObject().equals(currentFile)) { + return ; + } + line = currentLineNumber; + target = currentTarget; + } + + Collection variables = ComputeInlineValues.computeVariables(info, line, 1, cancel); + + target.accept(variables); + } + }; + } + + @Override + protected synchronized Collection getFileObjects() { + return currentFile != null ? List.of(currentFile) : List.of(); + } + + public synchronized void set(FileObject currentFile, int lineNumber, Consumer> target) { + this.currentFile = currentFile; + this.currentLineNumber = lineNumber; + this.currentTarget = target; + fileObjectsChanged(); + if (currentFile != null) { + reschedule(currentFile); + } + } + } + + private static final class TaskDescription { + public final FileObject frameFile; + public final int frameLineNumber; + public final Document frameDocument; + private final AtomicBoolean cancelled = new AtomicBoolean(); + private final List cancelCallbacks = + Collections.synchronizedList(new ArrayList<>()); + + public TaskDescription(FileObject frameFile, int frameLineNumber, Document frameDocument) { + this.frameFile = frameFile; + this.frameLineNumber = frameLineNumber; + this.frameDocument = frameDocument; + } + + public void cancel() { + cancelled.set(true); + + List callbacks; + + synchronized (cancelCallbacks) { + callbacks = new ArrayList<>(cancelCallbacks); + } + + for (Runnable r : callbacks) { + r.run(); + } + } + + public void addCancelCallback(Runnable r) { + cancelCallbacks.add(r); + + if (cancelled.get()) { + r.run(); + } + } + + @Override + public int hashCode() { + int hash = 7; + hash = 53 * hash + Objects.hashCode(this.frameFile); + hash = 53 * hash + this.frameLineNumber; + hash = 53 * hash + System.identityHashCode(this.frameDocument); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final TaskDescription other = (TaskDescription) obj; + if (this.frameLineNumber != other.frameLineNumber) { + return false; + } + if (!Objects.equals(this.frameFile, other.frameFile)) { + return false; + } + return this.frameDocument == other.frameDocument; + } + + public boolean isCancelled() { + return cancelled.get(); + } + + } + @MimeRegistration(mimeType="text/x-java", service=HighlightsLayerFactory.class) + public static HighlightsLayerFactory createHighlightsLayerFactory() { + return new HighlightsLayerFactory() { + @Override + public HighlightsLayer[] createLayers(HighlightsLayerFactory.Context context) { + return new HighlightsLayer[] { + HighlightsLayer.create(InlineValueComputerImpl.class.getName(), ZOrder.SYNTAX_RACK.forPosition(1400), false, getHighlightsBag(context.getDocument())) + }; + } + }; + } + + private static OffsetsBag getHighlightsBag(Document doc) { + OffsetsBag bag = (OffsetsBag) doc.getProperty(InlineValueComputerImpl.class); + if (bag == null) { + doc.putProperty(InlineValueComputerImpl.class, bag = new OffsetsBag(doc, true)); + } + return bag; + } + +} diff --git a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/models/VariablesFormatterFilter.java b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/models/VariablesFormatterFilter.java index 724aadc8c929..c4d8f94fa573 100644 --- a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/models/VariablesFormatterFilter.java +++ b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/models/VariablesFormatterFilter.java @@ -584,7 +584,7 @@ private static boolean isLeafType (String type) { } private static HashSet toStringValueType; - private static boolean isToStringValueType (String type) { + static boolean isToStringValueType (String type) { if (toStringValueType == null) { toStringValueType = new HashSet (); toStringValueType.add ("java.lang.StringBuffer"); diff --git a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/values/ComputeInlineValues.java b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/values/ComputeInlineValues.java new file mode 100644 index 000000000000..fa073e095bca --- /dev/null +++ b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/values/ComputeInlineValues.java @@ -0,0 +1,128 @@ +/* + * 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.debugger.jpda.ui.values; + +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.LambdaExpressionTree; +import com.sun.source.tree.LineMap; +import com.sun.source.tree.Tree; +import com.sun.source.tree.VariableTree; +import com.sun.source.util.TreePath; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import org.netbeans.api.java.source.CompilationInfo; +import org.netbeans.api.java.source.TreeUtilities; +import org.netbeans.api.java.source.support.CancellableTreePathScanner; + +public class ComputeInlineValues { + + public static Collection computeVariables(CompilationInfo info, int stackLine, int stackCol, AtomicBoolean cancel) { + int donePos = (int) info.getCompilationUnit().getLineMap().getPosition(stackLine, stackCol); + int upcomingPos = (int) info.getCompilationUnit().getLineMap().getStartPosition(stackLine + 1); + TreePath relevantPoint = info.getTreeUtilities().pathFor(donePos + 1); + OUTER: while (relevantPoint != null) { + Tree leaf = relevantPoint.getLeaf(); + switch (leaf.getKind()) { + case ANNOTATION_TYPE, CLASS, ENUM, INTERFACE, RECORD: + case METHOD, LAMBDA_EXPRESSION: break OUTER; + case BLOCK: + if (relevantPoint.getParentPath() != null && TreeUtilities.CLASS_TREE_KINDS.contains(relevantPoint.getParentPath().getLeaf().getKind())) { + break OUTER; + } + } + relevantPoint = relevantPoint.getParentPath(); + } + if (relevantPoint == null) { + return List.of(); + } + Collection result = new ArrayList<>(); + LineMap lm = info.getCompilationUnit().getLineMap(); + new CancellableTreePathScanner(cancel) { + @Override + public Void visitVariable(VariableTree node, Tree relevantPointTree) { + int end = (int) info.getTrees().getSourcePositions().getEndPosition(info.getCompilationUnit(), node); + if (end < donePos) { + int[] span = info.getTreeUtilities().findNameSpan(node); + + if (span != null) { + int lineEnd = (int) (lm.getStartPosition(lm.getLineNumber(span[1]) + 1) - 1); + + result.add(new InlineVariable(span[0], span[1], lineEnd, node.getName().toString())); + } + } + return super.visitVariable(node, relevantPointTree); + } + + @Override + public Void visitIdentifier(IdentifierTree node, Tree relevantPointTree) { + Element el = info.getTrees().getElement(getCurrentPath()); + + if (el != null && el.getKind().isVariable() && + el.getKind() != ElementKind.ENUM_CONSTANT) { + int start = (int) info.getTrees().getSourcePositions().getStartPosition(info.getCompilationUnit(), node); + int end = (int) info.getTrees().getSourcePositions().getEndPosition(info.getCompilationUnit(), node); + + if (start != (-1) && end != (-1)) { + int lineEnd = (int) (lm.getStartPosition(lm.getLineNumber(end) + 1) - 1); + + result.add(new InlineVariable(start, end, lineEnd, node.getName().toString())); + } + } + + return super.visitIdentifier(node, relevantPointTree); + } + + @Override + public Void visitClass(ClassTree node, Tree relevantPointTree) { + if (node == relevantPointTree) { + return super.visitClass(node, relevantPointTree); + } + return null; + } + + @Override + public Void visitLambdaExpression(LambdaExpressionTree node, Tree relevantPointTree) { + return null; + } + + @Override + public Void scan(Tree tree, Tree relevantPointTree) { + if (tree != null) { + int start = (int) info.getTrees().getSourcePositions().getStartPosition(info.getCompilationUnit(), tree); + + if (start > upcomingPos) { + return null; + } + } + return super.scan(tree, relevantPointTree); + } + + }.scan(relevantPoint, relevantPoint.getLeaf()); + + return result; + } + + public record InlineVariable(int start, int end, int lineEnd, String expression) {} + +} diff --git a/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/values/InlineValueProviderImpl.java b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/values/InlineValueProviderImpl.java new file mode 100644 index 000000000000..502f444e7885 --- /dev/null +++ b/java/debugger.jpda.ui/src/org/netbeans/modules/debugger/jpda/ui/values/InlineValueProviderImpl.java @@ -0,0 +1,61 @@ +/* + * 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.debugger.jpda.ui.values; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; +import org.netbeans.api.editor.mimelookup.MimeRegistration; +import org.netbeans.api.java.source.JavaSource; +import org.netbeans.api.lsp.InlineValue; +import org.netbeans.api.lsp.Range; +import org.netbeans.spi.lsp.InlineValuesProvider; +import org.openide.filesystems.FileObject; + +@MimeRegistration(mimeType = "text/x-java", service = InlineValuesProvider.class) +public final class InlineValueProviderImpl implements InlineValuesProvider { + + @Override + public CompletableFuture> inlineValues(FileObject file, int currentExecutionPosition) { + //TODO: proper cancellability + JavaSource js = JavaSource.forFileObject(file); + CompletableFuture> result = new CompletableFuture<>(); + List resultValues = new ArrayList<>(); + if (js != null) { + try { + js.runUserActionTask(cc -> { + cc.toPhase(JavaSource.Phase.RESOLVED); + int stackLine = (int) cc.getCompilationUnit().getLineMap().getLineNumber(currentExecutionPosition); + int stackCol = (int) cc.getCompilationUnit().getLineMap().getColumnNumber(currentExecutionPosition); + for (ComputeInlineValues.InlineVariable var : ComputeInlineValues.computeVariables(cc, stackLine, stackCol, new AtomicBoolean())) { + resultValues.add(InlineValue.createInlineVariable(new Range(var.start(), var.end()), var.expression())); + } + }, true); + } catch (IOException ex) { + result.completeExceptionally(ex); + return result; + } + } + result.complete(resultValues); + return result; + } + +} diff --git a/java/debugger.jpda.ui/test/unit/src/org/netbeans/modules/debugger/jpda/ui/values/ComputeInlineValuesTest.java b/java/debugger.jpda.ui/test/unit/src/org/netbeans/modules/debugger/jpda/ui/values/ComputeInlineValuesTest.java new file mode 100644 index 000000000000..8677629456b2 --- /dev/null +++ b/java/debugger.jpda.ui/test/unit/src/org/netbeans/modules/debugger/jpda/ui/values/ComputeInlineValuesTest.java @@ -0,0 +1,171 @@ +/* + * 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.debugger.jpda.ui.values; + +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; +import org.netbeans.api.java.source.CompilationInfo; +import org.netbeans.api.java.source.JavaSource; +import org.netbeans.api.java.source.SourceUtilsTestUtil; +import org.netbeans.api.java.source.TestUtilities; +import org.netbeans.junit.NbTestCase; +import org.netbeans.modules.debugger.jpda.ui.values.ComputeInlineValues.InlineVariable; +import org.openide.filesystems.FileObject; +import org.openide.filesystems.FileUtil; + +public class ComputeInlineValuesTest extends NbTestCase { + + private FileObject srcDir; + + public ComputeInlineValuesTest(String name) { + super(name); + } + + protected void setUp() throws Exception { + SourceUtilsTestUtil.prepareTest(new String[0], new Object[0]); + + clearWorkDir(); + + FileObject wd = FileUtil.toFileObject(getWorkDir()); + + srcDir = FileUtil.createFolder(wd, "src"); + + FileObject buildDir = FileUtil.createFolder(wd, "build"); + FileObject cacheDir = FileUtil.createFolder(wd, "cache"); + + SourceUtilsTestUtil.prepareTest(srcDir, buildDir, cacheDir); + } + + public void testSimpleComputeInlineVariables() throws Exception { + runCompileInlineVariablesTest(""" + public class Test { + private void test(String /param/) { + int /i1/ = 0; + |int i2 = /i1/; + int i3 = 1; + } + } + """); + } + + public void testNestedMethodsComputeInlineVariables() throws Exception { + runCompileInlineVariablesTest(""" + public class Test { + private void test(String /param/) { + int /i1/ = 0; + Runnable /r1/ = new Runnable() { + public void run() { + int n = 0; + } + }; + Runnable /r2/ = () -> { + int n = 0; + }; + |int i2 = /i1/; + int i3 = 1; + } + } + """); + } + + public void testNoEnumConstants() throws Exception { + runCompileInlineVariablesTest(""" + package test; + import static test.Test.E.*; + public class Test { + private void test(String /param/) { + E /e/ = A; + |E e2 = /e/; + } + public enum E { + A, B, C; + } + } + """); + } + + public void testDeclarationAndUseOnTheSameLine() throws Exception { + runCompileInlineVariablesTest(""" + public class Test { + private void test(String /param/) { + int /i1/ = 0; + |for (int j = 0; /j/ < 10; /j/++) { + } + } + } + """); + } + + public void testFields() throws Exception { + runCompileInlineVariablesTest(""" + public class Test { + int /i1/ = 0; + |int j = /i1/; + } + """); + } + + public void testClassNoCrash() throws Exception { + runCompileInlineVariablesTest(""" + |public class Test { + } + """); + } + + private void runCompileInlineVariablesTest(String codeResultAndPos) throws Exception { + FileObject source = FileUtil.createData(srcDir, "Test.java"); + int pos = codeResultAndPos.replace("/", "").indexOf("|"); + int idx = 0; + List parts = new ArrayList<>(List.of(codeResultAndPos.replace("|", "").split("/"))); + Set expectedSpans = new HashSet<>(); + + while (parts.size() > 1) { + int start = idx += parts.remove(0).length(); + int end = idx += parts.remove(0).length(); + + expectedSpans.add("" + start + "-" + end); + } + + String code = codeResultAndPos.replace("/", "").replace("|", ""); + + TestUtilities.copyStringToFile(source, code); + CompilationInfo info = + SourceUtilsTestUtil.getCompilationInfo(JavaSource.forFileObject(source), + JavaSource.Phase.RESOLVED); + int stackLine = (int) info.getCompilationUnit().getLineMap().getLineNumber(pos); + Collection computedVariables = ComputeInlineValues.computeVariables(info, stackLine, 0, new AtomicBoolean()); + + for (InlineVariable var : computedVariables) { + String snippet = code.substring(var.start(), var.end()); + + assertEquals(snippet, var.expression()); + assertFalse(code.substring(var.end(), var.lineEnd()).contains("\n")); + assertEquals('\n', code.charAt(var.lineEnd())); + + if (!expectedSpans.remove("" + var.start() + "-" + var.end())) { + throw new AssertionError("Returned span: " + var.start() + "-" + var.end() + " (" + snippet + "), but it is not among the expected spans."); + } + } + + if (!expectedSpans.isEmpty()) { + throw new AssertionError("Spans not found: " + expectedSpans); + } + } + +} diff --git a/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/expr/formatters/VariablesFormatter.java b/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/expr/formatters/VariablesFormatter.java index 3691c387d458..6959fe2208f6 100644 --- a/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/expr/formatters/VariablesFormatter.java +++ b/java/debugger.jpda/src/org/netbeans/modules/debugger/jpda/expr/formatters/VariablesFormatter.java @@ -301,6 +301,8 @@ public static VariablesFormatter[] loadFormatters() { "MSG_EnumFormatter=Default Enum Formatter", "MSG_BigDecimalFormatter=Default BigDecimal Formatter", "MSG_BigIntegerFormatter=Default BigInteger Formatter", + "MSG_JavadocASTFormatter=Default com.sun.source.doctree.DocTree Formatter", + "MSG_JavacASTFormatter=Default com.sun.source.tree.Tree Formatter", }) private static VariablesFormatter[] createDefaultFormatters() { VariablesFormatter charSequence = new VariablesFormatter(Bundle.MSG_CharSequenceFormatter()); @@ -354,8 +356,20 @@ private static VariablesFormatter[] createDefaultFormatters() { bigIntegerFormatter.setValueFormatCode("toString()"); bigIntegerFormatter.isDefault = true; + VariablesFormatter javadocASTFormatter = new VariablesFormatter(Bundle.MSG_JavadocASTFormatter()); + javadocASTFormatter.setClassTypes("com.sun.source.doctree.DocTree"); + javadocASTFormatter.setIncludeSubTypes(true); + javadocASTFormatter.setValueFormatCode("toString()"); + javadocASTFormatter.isDefault = true; + + VariablesFormatter javacASTFormatter = new VariablesFormatter(Bundle.MSG_JavacASTFormatter()); + javacASTFormatter.setClassTypes("com.sun.source.tree.Tree"); + javacASTFormatter.setIncludeSubTypes(true); + javacASTFormatter.setValueFormatCode("toString()"); + javacASTFormatter.isDefault = true; + return new VariablesFormatter[] { charSequence, collection, map, mapEntry, enumFormatter, - bigDecimalFormatter, bigIntegerFormatter }; + bigDecimalFormatter, bigIntegerFormatter, javadocASTFormatter, javacASTFormatter }; } diff --git a/java/java.editor/src/org/netbeans/modules/java/editor/semantic/HighlightsLayerFactoryImpl.java b/java/java.editor/src/org/netbeans/modules/java/editor/semantic/HighlightsLayerFactoryImpl.java index 03a98b4267ec..d136c48c7d66 100644 --- a/java/java.editor/src/org/netbeans/modules/java/editor/semantic/HighlightsLayerFactoryImpl.java +++ b/java/java.editor/src/org/netbeans/modules/java/editor/semantic/HighlightsLayerFactoryImpl.java @@ -23,6 +23,7 @@ import org.netbeans.spi.editor.highlighting.HighlightsLayerFactory; import org.netbeans.spi.editor.highlighting.HighlightsLayerFactory.Context; import org.netbeans.spi.editor.highlighting.ZOrder; +import org.netbeans.spi.editor.highlighting.support.HighlightsContainers; /** * @@ -38,7 +39,7 @@ public HighlightsLayer[] createLayers(Context context) { return new HighlightsLayer[] { HighlightsLayer.create(SemanticHighlighter.class.getName() + "-1", ZOrder.SYNTAX_RACK.forPosition(1000), false,semantic), HighlightsLayer.create(SemanticHighlighter.class.getName() + "-2", ZOrder.SYNTAX_RACK.forPosition(1500), false, SemanticHighlighter.getImportHighlightsBag(context.getDocument())), - HighlightsLayer.create(SemanticHighlighter.class.getName() + "-3", ZOrder.SYNTAX_RACK.forPosition(1600), false, SemanticHighlighter.getPreTextBag(context.getDocument())), + HighlightsLayer.create(SemanticHighlighter.class.getName() + "-3", ZOrder.SYNTAX_RACK.forPosition(1600), false, HighlightsContainers.inlineHintsSettingAwareContainer(context.getDocument(), SemanticHighlighter.getPreTextBag(context.getDocument()))), //the mark occurrences layer should be "above" current row and "below" the search layers: HighlightsLayer.create(MarkOccurrencesHighlighter.class.getName(), ZOrder.SHOW_OFF_RACK.forPosition(20), true, MarkOccurrencesHighlighter.getHighlightsBag(context.getDocument())), //"above" mark occurrences, "below" search layers: 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 9430217f02c1..f6bdf266256c 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 @@ -892,6 +892,7 @@ private InitializeResult constructInitResponse(InitializeParams init, JavaSource capabilities.setDocumentFormattingProvider(true); capabilities.setDocumentRangeFormattingProvider(true); capabilities.setReferencesProvider(true); + capabilities.setInlineValueProvider(true); CallHierarchyRegistrationOptions chOpts = new CallHierarchyRegistrationOptions(); chOpts.setWorkDoneProgress(true); 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 c5dbef18014e..19bb48142ae6 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 @@ -24,6 +24,7 @@ import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import com.sun.source.tree.ClassTree; +import com.sun.source.tree.IdentifierTree; import com.sun.source.tree.LineMap; import com.sun.source.tree.MethodTree; import com.sun.source.tree.Tree; @@ -138,6 +139,10 @@ import org.eclipse.lsp4j.InlayHint; import org.eclipse.lsp4j.InlayHintLabelPart; import org.eclipse.lsp4j.InlayHintParams; +import org.eclipse.lsp4j.InlineValue; +import org.eclipse.lsp4j.InlineValueEvaluatableExpression; +import org.eclipse.lsp4j.InlineValueParams; +import org.eclipse.lsp4j.InlineValueVariableLookup; import org.eclipse.lsp4j.InsertTextFormat; import org.eclipse.lsp4j.Location; import org.eclipse.lsp4j.LocationLink; @@ -194,6 +199,8 @@ import org.netbeans.api.java.source.TreePathHandle; import org.netbeans.api.java.source.TreeUtilities; import org.netbeans.api.java.source.WorkingCopy; +import org.netbeans.api.java.source.support.CancellableTreePathScanner; +import org.netbeans.api.java.source.support.ErrorAwareTreePathScanner; import org.netbeans.api.java.source.ui.ElementOpen; import org.netbeans.api.lexer.Token; import org.netbeans.api.lexer.TokenSequence; @@ -256,6 +263,7 @@ import org.netbeans.spi.lsp.CodeLensProvider; import org.netbeans.spi.lsp.ErrorProvider; import org.netbeans.spi.lsp.InlayHintsProvider; +import org.netbeans.spi.lsp.InlineValuesProvider; import org.netbeans.spi.lsp.StructureProvider; import org.netbeans.spi.project.ActionProvider; import org.netbeans.spi.project.ProjectConfiguration; @@ -2865,4 +2873,32 @@ public CompletableFuture> inlayHint(InlayHintParams params) { }); } + @Override + public CompletableFuture> inlineValue(InlineValueParams params) { + String uri = params.getTextDocument().getUri(); + FileObject file = fromURI(uri); + if (file == null) { + return CompletableFuture.completedFuture(null); + } + CompletableFuture> result = new CompletableFuture<>(); + result.complete(List.of()); + Document rawDoc = server.getOpenedDocuments().getDocument(uri); + if (!(rawDoc instanceof StyledDocument)) { + return CompletableFuture.completedFuture(Collections.emptyList()); + } + StyledDocument doc = (StyledDocument)rawDoc; + int currentExecutionPosition = Utils.getOffset(doc, params.getContext().getStoppedLocation().getEnd()); + for (InlineValuesProvider provider : MimeLookup.getLookup(file.getMIMEType()).lookupAll(InlineValuesProvider.class)) { + result = result.thenCombine(provider.inlineValues(file, currentExecutionPosition), (l1, l2) -> { + List res = new ArrayList<>(l1.size() + l2.size()); + res.addAll(l1); + for (org.netbeans.api.lsp.InlineValue val : l2) { + res.add(new InlineValue(new InlineValueEvaluatableExpression(new Range(Utils.createPosition(file, val.getRange().getStartOffset()), Utils.createPosition(file, val.getRange().getEndOffset())), val.getExpression()))); + } + return res; + }); + } + return result; + } + }