diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml new file mode 100644 index 000000000..46a7e6323 --- /dev/null +++ b/.github/codeql/codeql-config.yml @@ -0,0 +1,15 @@ +# https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning#using-a-custom-configuration-file +name: "CodeQL config" +queries: +- uses: security-and-quality +- uses: ./.github/codeql/queries/java # load our custom CodeQL rules + +query-filters: +- exclude: + # Exclude the built-in shadowing rule. + # We intentionally use final locals that copy instance fields + # (e.g. `final var name = this.name`) to support Eclipse null analysis + # without introducing noisy renaming. This pattern is deliberate and safe, + # so the built-in rule is disabled in favor of our custom rule + # (local-shadows-instance-field.ql). + id: java/local-shadows-field diff --git a/.github/codeql/queries/java/local-shadows-instance-field.ql b/.github/codeql/queries/java/local-shadows-instance-field.ql new file mode 100644 index 000000000..7ff5a8f59 --- /dev/null +++ b/.github/codeql/queries/java/local-shadows-instance-field.ql @@ -0,0 +1,47 @@ +/** + * @name Local variable shadows instance field (except final this-copy) + * @description Flags local variables that shadow an instance field, unless they are final + * and initialized directly from that same field (for example `final var name = this.name;`). + * @id custom-java/local-shadows-instance-field-strict + * @kind problem + * @problem.severity warning + * @precision medium + * @tags correctness readability maintainability + */ +import java +import semmle.code.java.Member +import semmle.code.java.Variable + +/** + * True if this local variable is an allowed shadowing of the given instance field: + * + * final var name = this.name; + * final var name = name; // implicit this + */ +predicate allowedShadowing(LocalVariableDecl local, Field field) { + local.isFinal() and + exists(FieldAccess fa | + fa = local.getInitializer().getUnderlyingExpr().(FieldAccess) and + fa.getField() = field and + fa.isOwnFieldAccess() + ) +} + +from LocalVariableDecl local, Field field, Callable c +where + // same simple name + local.getName() = field.getName() and + + // only consider instance fields + not field.isStatic() and + + // local is inside a callable of a class related to the field's declaring type + c = local.getCallable() and + c.getDeclaringType().getASupertype*() = field.getDeclaringType() and + + // shadowing is NOT in the allowed final-this-copy form + not allowedShadowing(local, field) +select local, + "Local variable '" + local.getName() + "' shadows instance field '" + + field.getQualifiedName() + + "'. Shadowing is only allowed for 'final' locals directly initialized from this field." diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 11b962ec7..060425039 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -133,7 +133,7 @@ jobs: # https://github.com/github/codeql-action#build-modes build-mode: ${{ matrix.build-mode }} # https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning#using-queries-in-ql-packs - queries: +security-and-quality + config-file: ./.github/codeql/codeql-config.yml - name: "Build with Maven 🔨" diff --git a/org.eclipse.lsp4e.debug/META-INF/MANIFEST.MF b/org.eclipse.lsp4e.debug/META-INF/MANIFEST.MF index 0408e6831..8caf3fb2e 100644 --- a/org.eclipse.lsp4e.debug/META-INF/MANIFEST.MF +++ b/org.eclipse.lsp4e.debug/META-INF/MANIFEST.MF @@ -19,13 +19,16 @@ Require-Bundle: org.eclipse.ui, org.eclipse.jface.text, org.eclipse.ui.genericeditor, org.eclipse.core.variables, - org.eclipse.lsp4e;bundle-version="0.18.13" + org.eclipse.lsp4e;bundle-version="0.19.0", + org.eclipse.tm4e.core;resolution:=optional, + org.eclipse.tm4e.registry;resolution:=optional, + org.eclipse.tm4e.ui;resolution:=optional Bundle-RequiredExecutionEnvironment: JavaSE-21 Bundle-ActivationPolicy: lazy Import-Package: com.google.gson, com.google.gson.reflect;version="2.7.0" Export-Package: org.eclipse.lsp4e.debug;x-internal:=true, - org.eclipse.lsp4e.debug.breakpoints;x-internal:=true, + org.eclipse.lsp4e.debug.breakpoints;x-friends:="org.eclipse.lsp4e.test", org.eclipse.lsp4e.debug.console;x-internal:=true, org.eclipse.lsp4e.debug.debugmodel;x-friends:="org.eclipse.lsp4e.test", org.eclipse.lsp4e.debug.launcher, diff --git a/org.eclipse.lsp4e.debug/plugin.xml b/org.eclipse.lsp4e.debug/plugin.xml index d4da83c47..caf236091 100644 --- a/org.eclipse.lsp4e.debug/plugin.xml +++ b/org.eclipse.lsp4e.debug/plugin.xml @@ -113,4 +113,20 @@ delegateClass="org.eclipse.lsp4e.debug.presentation.DAPWatchExpression"> + + + + + + + + + + + + + + diff --git a/org.eclipse.lsp4e.debug/src/org/eclipse/lsp4e/debug/breakpoints/DSPLineBreakpoint.java b/org.eclipse.lsp4e.debug/src/org/eclipse/lsp4e/debug/breakpoints/DSPLineBreakpoint.java index 9d12d5db3..d319a30e7 100644 --- a/org.eclipse.lsp4e.debug/src/org/eclipse/lsp4e/debug/breakpoints/DSPLineBreakpoint.java +++ b/org.eclipse.lsp4e.debug/src/org/eclipse/lsp4e/debug/breakpoints/DSPLineBreakpoint.java @@ -13,12 +13,22 @@ import org.eclipse.core.runtime.CoreException; import org.eclipse.debug.core.model.IBreakpoint; import org.eclipse.debug.core.model.LineBreakpoint; +import org.eclipse.jdt.annotation.Nullable; import org.eclipse.lsp4e.debug.DSPPlugin; public class DSPLineBreakpoint extends LineBreakpoint { public static final String ID = "org.eclipse.lsp4e.debug.breakpoints.markerType.lineBreakpoint"; + /** Marker attribute key for a conditional expression. */ + public static final String ATTR_CONDITION = "org.eclipse.lsp4e.debug.breakpoints.condition"; + + /** Marker attribute key for inline breakpoint column (1-based). */ + public static final String ATTR_COLUMN = "org.eclipse.lsp4e.debug.breakpoints.column"; + + /** Marker attribute key for hit condition expression. */ + public static final String ATTR_HIT_CONDITION = "org.eclipse.lsp4e.debug.breakpoints.hitCondition"; + public DSPLineBreakpoint() { } @@ -48,4 +58,59 @@ public DSPLineBreakpoint(final IResource resource, String fileName, final int li public String getModelIdentifier() { return DSPPlugin.ID_DSP_DEBUG_MODEL; } + + /** + * @return the inline breakpoint column (1-based) or {@code -1} if unset. + */ + public int getColumn() { + final IMarker m = getMarker(); + return m == null ? -1 : m.getAttribute(ATTR_COLUMN, -1); + } + + /** + * Sets or clears the inline breakpoint column. Values <= 0 clear the column. + */ + public void setColumn(final int column) throws CoreException { + final IMarker m = getMarker(); + if (m != null) { + m.setAttribute(ATTR_COLUMN, column <= 0 ? null : column); + } + } + + /** + * @return the breakpoint condition or {@code null} if none. + */ + public @Nullable String getCondition() { + final IMarker m = getMarker(); + return m == null ? null : m.getAttribute(ATTR_CONDITION, (String) null); + } + + /** + * Sets or clears the breakpoint condition. A {@code null} or blank value clears + * the condition. + */ + public void setCondition(final @Nullable String condition) throws CoreException { + final IMarker m = getMarker(); + if (m != null) { + m.setAttribute(ATTR_CONDITION, condition == null || condition.isBlank() ? null : condition); + } + } + + /** + * @return the hit condition or {@code null} if none. + */ + public @Nullable String getHitCondition() { + final IMarker m = getMarker(); + return m == null ? null : m.getAttribute(ATTR_HIT_CONDITION, (String) null); + } + + /** + * Sets or clears the hit condition. A {@code null} or blank value clears it. + */ + public void setHitCondition(final @Nullable String hitCondition) throws CoreException { + final IMarker m = getMarker(); + if (m != null) { + m.setAttribute(ATTR_HIT_CONDITION, hitCondition == null || hitCondition.isBlank() ? null : hitCondition); + } + } } diff --git a/org.eclipse.lsp4e.debug/src/org/eclipse/lsp4e/debug/debugmodel/DSPBreakpointManager.java b/org.eclipse.lsp4e.debug/src/org/eclipse/lsp4e/debug/debugmodel/DSPBreakpointManager.java index 93200fb32..0eeada50b 100644 --- a/org.eclipse.lsp4e.debug/src/org/eclipse/lsp4e/debug/debugmodel/DSPBreakpointManager.java +++ b/org.eclipse.lsp4e.debug/src/org/eclipse/lsp4e/debug/debugmodel/DSPBreakpointManager.java @@ -30,6 +30,7 @@ import org.eclipse.debug.core.model.ILineBreakpoint; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.lsp4e.debug.DSPPlugin; +import org.eclipse.lsp4e.debug.breakpoints.DSPLineBreakpoint; import org.eclipse.lsp4j.debug.BreakpointEventArguments; import org.eclipse.lsp4j.debug.Capabilities; import org.eclipse.lsp4j.debug.SetBreakpointsArguments; @@ -179,6 +180,25 @@ private void addBreakpointToMap(IBreakpoint breakpoint) { s -> new ArrayList<>()); final var sourceBreakpoint = new SourceBreakpoint(); sourceBreakpoint.setLine(lineNumber); + + // inline (column) breakpoint support + final int column = marker.getAttribute(DSPLineBreakpoint.ATTR_COLUMN, -1); + if (column > 0) { + sourceBreakpoint.setColumn(column); + } + + // conditional breakpoint support + final String condition = marker.getAttribute(DSPLineBreakpoint.ATTR_CONDITION, (String) null); + if (condition != null && !condition.isBlank()) { + sourceBreakpoint.setCondition(condition); + } + + // hit condition support + final String hitCondition = marker.getAttribute(DSPLineBreakpoint.ATTR_HIT_CONDITION, (String) null); + if (hitCondition != null && !hitCondition.isBlank()) { + sourceBreakpoint.setHitCondition(hitCondition); + } + sourceBreakpoints.add(sourceBreakpoint); } } @@ -202,7 +222,16 @@ private void deleteBreakpointFromMap(IBreakpoint breakpoint) { List bps = entry.getValue(); for (Iterator iterator = bps.iterator(); iterator.hasNext();) { SourceBreakpoint sourceBreakpoint = iterator.next(); - if (Objects.equals(lineNumber, sourceBreakpoint.getLine())) { + + // Match by line and (if present) column + Integer bpColumn = sourceBreakpoint.getColumn(); + int markerColumn = lineBreakpoint.getMarker().getAttribute(DSPLineBreakpoint.ATTR_COLUMN, -1); + final boolean lineMatches = Objects.equals(lineNumber, sourceBreakpoint.getLine()); + final boolean columnMatches = (markerColumn <= 0 + && (bpColumn == null || bpColumn.intValue() <= 0)) + || (markerColumn > 0 && bpColumn != null && bpColumn.intValue() == markerColumn); + + if (lineMatches && columnMatches) { iterator.remove(); } } diff --git a/org.eclipse.lsp4e.debug/src/org/eclipse/lsp4e/debug/presentation/DSPBreakpointDetailPane.java b/org.eclipse.lsp4e.debug/src/org/eclipse/lsp4e/debug/presentation/DSPBreakpointDetailPane.java new file mode 100644 index 000000000..036d3c33b --- /dev/null +++ b/org.eclipse.lsp4e.debug/src/org/eclipse/lsp4e/debug/presentation/DSPBreakpointDetailPane.java @@ -0,0 +1,330 @@ +/******************************************************************************* + * Copyright (c) 2025 Vegard IT GmbH and others. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Sebastian Thomschke (Vegard IT GmbH) - initial implementation. + *******************************************************************************/ +package org.eclipse.lsp4e.debug.presentation; + +import static org.eclipse.lsp4e.internal.NullSafetyHelper.lateNonNull; +import static org.eclipse.swt.events.SelectionListener.widgetSelectedAdapter; + +import java.util.Objects; + +import org.eclipse.core.runtime.Adapters; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.debug.core.model.IBreakpoint; +import org.eclipse.debug.core.model.IDebugElement; +import org.eclipse.debug.core.model.IDebugTarget; +import org.eclipse.debug.ui.DebugUITools; +import org.eclipse.debug.ui.IDetailPane; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.layout.GridLayoutFactory; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.lsp4e.debug.DSPPlugin; +import org.eclipse.lsp4e.debug.breakpoints.DSPLineBreakpoint; +import org.eclipse.lsp4e.debug.debugmodel.DSPDebugTarget; +import org.eclipse.lsp4e.ui.UI; +import org.eclipse.lsp4j.debug.Capabilities; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Spinner; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.IWorkbenchPartSite; +// + +/** + * Detail pane for LSP4E line breakpoints. + */ +public class DSPBreakpointDetailPane implements IDetailPane { + + public static final String ID = DSPBreakpointDetailPaneFactory.PANE_ID; + public static final String NAME = DSPBreakpointDetailPaneFactory.PANE_NAME; + public static final String DESCRIPTION = DSPBreakpointDetailPaneFactory.PANE_DESCRIPTION; + + private Composite control = lateNonNull(); + private Button enableConditionButton = lateNonNull(); + private SourceCodeEditor conditionEditor = lateNonNull(); + private Button enableHitConditionButton = lateNonNull(); + private Text hitConditionText = lateNonNull(); + private Spinner columnSpinner = lateNonNull(); + private @Nullable DSPLineBreakpoint selectedBP = null; + private volatile boolean updating; + + /** + * @return the capabilities of the active debug adapter or null if none is + * active + */ + private @Nullable Capabilities getDebugAdapterCapabilities() { + final var ctx = DebugUITools.getDebugContext(); + if (ctx == null) + return null; + + final Object targetObj = ctx instanceof final IDebugElement de // + ? de.getDebugTarget() + : Adapters.adapt(ctx, IDebugTarget.class); + return targetObj instanceof final DSPDebugTarget dt // + ? dt.getCapabilities() + : null; + } + + private @Nullable DSPLineBreakpoint getSelectedBreakPoint(final IStructuredSelection selection) { + if (selection.size() != 1) + return null; + + Object first = selection.getFirstElement(); + if (first == null) + return null; + + if (first instanceof DSPLineBreakpoint d) + return d; + + if (!(first instanceof IBreakpoint)) { + Object adapted = org.eclipse.core.runtime.Platform.getAdapterManager().getAdapter(first, IBreakpoint.class); + if (adapted instanceof IBreakpoint b) { + first = b; + } + } + return first instanceof DSPLineBreakpoint d ? d : null; + } + + @Override + public Control createControl(final Composite parent) { + control = new Composite(parent, SWT.NONE); + control.setBackground(UI.getDisplay().getSystemColor(SWT.COLOR_LIST_BACKGROUND)); + GridLayoutFactory.swtDefaults().numColumns(1).equalWidth(false).applyTo(control); + GridDataFactory.fillDefaults().grab(true, true).applyTo(control); + + /* + * column row + */ + final var colRow = new Composite(control, SWT.NONE); + GridLayoutFactory.swtDefaults().numColumns(2).margins(0, 0).equalWidth(false).applyTo(colRow); + GridDataFactory.fillDefaults().grab(true, false).applyTo(colRow); + + final var colHint = "Inline breakpoint column (1-based).\nSet > 0 to break at a specific character position on this line; set 0 to use the adapter's default stoppable location."; + final var colLabel = new Label(colRow, SWT.NONE); + colLabel.setText("Column"); + colLabel.setToolTipText(colHint); + GridDataFactory.swtDefaults().align(SWT.BEGINNING, SWT.CENTER).applyTo(colLabel); + + columnSpinner = new Spinner(colRow, SWT.BORDER); + columnSpinner.setMinimum(0); + columnSpinner.setMaximum(Integer.MAX_VALUE); + columnSpinner.setToolTipText(colHint); + GridDataFactory.swtDefaults().hint(100, SWT.DEFAULT).applyTo(columnSpinner); + + /* + * hit condition + */ + final var hitHint = "Trigger this breakpoint only on specific hit counts.\nExamples: 5 (5th hit), >= 10 (10th and later), % 3 == 0 (every 3rd hit)."; + final var hitRow = new Composite(control, SWT.NONE); + GridLayoutFactory.swtDefaults().numColumns(2).margins(0, 0).equalWidth(false).applyTo(hitRow); + GridDataFactory.fillDefaults().grab(true, false).applyTo(hitRow); + + enableHitConditionButton = new Button(hitRow, SWT.CHECK); + enableHitConditionButton.setText("Hit condition"); + enableHitConditionButton.setToolTipText(hitHint); + GridDataFactory.swtDefaults().indent(0, 0).align(SWT.BEGINNING, SWT.CENTER).applyTo(enableHitConditionButton); + + hitConditionText = new Text(hitRow, SWT.BORDER); + hitConditionText.setToolTipText(hitHint); + GridDataFactory.fillDefaults().grab(true, false).applyTo(hitConditionText); + + /* + * condition + */ + final var condHint = "The breakpoint stops only if the expression evaluates to true in the program context."; + enableConditionButton = new Button(control, SWT.CHECK); + enableConditionButton.setText("Condition"); + enableConditionButton.setToolTipText(condHint); + + conditionEditor = SourceCodeEditor.create(control, SWT.NONE); + conditionEditor.setToolTipText(condHint); + conditionEditor.setEditorLayoutData(GridDataFactory.fillDefaults().grab(true, true).hint(SWT.DEFAULT, 120)); + + hookListeners(); + return control; + } + + @Override + public void display(final IStructuredSelection selection) { + final var selectedBP_ = selectedBP = getSelectedBreakPoint(selection); + updating = true; + try { + if (selectedBP_ != null) { + final var m = selectedBP_.getMarker(); + conditionEditor.configureForResource(m != null ? m.getResource() : null); + final String hit = selectedBP_.getHitCondition(); + final boolean hitEnabled = hit != null && !hit.isBlank(); + enableHitConditionButton.setSelection(hitEnabled); + if (hitEnabled) { + final String hitNew = hit != null ? hit : ""; + final String hitOld = hitConditionText.getText(); + if (!Objects.equals(hitOld, hitNew)) { + final Point sel2 = hitConditionText.getSelection(); + final int caret2 = sel2.y; + hitConditionText.setText(hitNew); + hitConditionText.setSelection(Math.min(caret2, hitConditionText.getCharCount())); + } + } + + final String condition = selectedBP_.getCondition(); + final boolean condEnabled = condition != null && !condition.isBlank(); + enableConditionButton.setSelection(condEnabled); + if (condEnabled) { + final String newText = condition != null ? condition : ""; + final String oldText = conditionEditor.getText(); + if (!Objects.equals(oldText, newText)) { + final Point sel = conditionEditor.getSelection(); + final int caret = sel.y; + conditionEditor.setText(newText); + conditionEditor.setSelection(Math.min(caret, conditionEditor.getTextWidget().getCharCount())); + } + } + + columnSpinner.setSelection(Math.max(0, selectedBP_.getColumn())); + setEnabled(true); + + // Apply capability gating + final var caps = getDebugAdapterCapabilities(); // is null if no debug session is active + + final boolean condSupported = caps == null || caps.getSupportsConditionalBreakpoints(); + enableConditionButton.setEnabled(condSupported); + conditionEditor.setEnabled(condSupported); + + final boolean hitSupported = caps == null || caps.getSupportsHitConditionalBreakpoints(); + enableHitConditionButton.setEnabled(hitSupported); + hitConditionText.setEnabled(hitSupported); + } else { + enableConditionButton.setSelection(false); + conditionEditor.setText(""); + enableHitConditionButton.setSelection(false); + hitConditionText.setText(""); + columnSpinner.setSelection(0); + setEnabled(false); + } + } finally { + updating = false; + } + } + + @Override + public void dispose() { + if (control != null && !control.isDisposed()) { + control.dispose(); + } + selectedBP = null; + } + + @Override + public @Nullable String getDescription() { + return DESCRIPTION; + } + + @Override + public String getID() { + return ID; + } + + @Override + public String getName() { + return NAME; + } + + // removed legacy reflection-based TM4E viewer creation + + private void hookListeners() { + enableConditionButton.addSelectionListener(widgetSelectedAdapter(e -> { + boolean enabled = enableConditionButton.getSelection(); + if (!enabled && selectedBP != null) { + try { + selectedBP.setCondition(null); + } catch (CoreException ex) { + DSPPlugin.logError(ex); + } + } + })); + + conditionEditor.addModifyListener(e -> { + final var selectedBP = this.selectedBP; + if (updating || selectedBP == null) + return; + + if (!enableConditionButton.getSelection()) { + enableConditionButton.setSelection(true); + } + try { + selectedBP.setCondition(conditionEditor.getText()); + } catch (CoreException ex) { + DSPPlugin.logError(ex); + } + }); + + enableHitConditionButton.addSelectionListener(widgetSelectedAdapter(e -> { + boolean enabled = enableHitConditionButton.getSelection(); + if (!enabled && selectedBP != null) { + try { + selectedBP.setHitCondition(null); + } catch (CoreException ex) { + DSPPlugin.logError(ex); + } + } + })); + + hitConditionText.addModifyListener(e -> { + final var selectedBP = this.selectedBP; + if (updating || selectedBP == null) + return; + + if (!enableHitConditionButton.getSelection()) { + enableHitConditionButton.setSelection(true); + } + try { + selectedBP.setHitCondition(hitConditionText.getText()); + } catch (CoreException ex) { + DSPPlugin.logError(ex); + } + }); + + columnSpinner.addSelectionListener(widgetSelectedAdapter(e -> { + final var selectedBP = this.selectedBP; + if (updating || selectedBP == null) + return; + + try { + selectedBP.setColumn(columnSpinner.getSelection()); + } catch (CoreException ex) { + DSPPlugin.logError(ex); + } + })); + } + + @Override + public void init(final @Nullable IWorkbenchPartSite partSite) { + // no-op + } + + private void setEnabled(boolean enabled) { + columnSpinner.setEnabled(enabled); + } + + @Override + public boolean setFocus() { + if (conditionEditor != null && !conditionEditor.isDisposed() && conditionEditor.isEnabled()) { + conditionEditor.getTextWidget().setFocus(); + return true; + } + return false; + } +} diff --git a/org.eclipse.lsp4e.debug/src/org/eclipse/lsp4e/debug/presentation/DSPBreakpointDetailPaneFactory.java b/org.eclipse.lsp4e.debug/src/org/eclipse/lsp4e/debug/presentation/DSPBreakpointDetailPaneFactory.java new file mode 100644 index 000000000..823acdbdc --- /dev/null +++ b/org.eclipse.lsp4e.debug/src/org/eclipse/lsp4e/debug/presentation/DSPBreakpointDetailPaneFactory.java @@ -0,0 +1,70 @@ +/******************************************************************************* + * Copyright (c) 2025 Vegard IT GmbH and others. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Sebastian Thomschke (Vegard IT GmbH) - initial implementation. + *******************************************************************************/ +package org.eclipse.lsp4e.debug.presentation; + +import java.util.Collections; +import java.util.Set; + +import org.eclipse.core.runtime.Adapters; +import org.eclipse.debug.core.model.IBreakpoint; +import org.eclipse.debug.ui.IDetailPane; +import org.eclipse.debug.ui.IDetailPaneFactory; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jface.viewers.IStructuredSelection; +import org.eclipse.lsp4e.debug.breakpoints.DSPLineBreakpoint; + +/** + * Contributes a detail pane for LSP4E breakpoints in the Breakpoints view. + */ +public class DSPBreakpointDetailPaneFactory implements IDetailPaneFactory { + + static final String PANE_ID = "org.eclipse.lsp4e.debug.detailPane.breakpoint"; + static final String PANE_NAME = "LSP4E Breakpoint"; + static final String PANE_DESCRIPTION = "Edit condition and column for LSP4E breakpoints"; + + @Override + public Set getDetailPaneTypes(final IStructuredSelection selection) { + return isDSPBreakpointSelection(selection) ? Set.of(PANE_ID) : Collections.emptySet(); + } + + @Override + public @Nullable String getDefaultDetailPane(final IStructuredSelection selection) { + return isDSPBreakpointSelection(selection) ? PANE_ID : null; + } + + @Override + public @Nullable IDetailPane createDetailPane(final String paneID) { + return PANE_ID.equals(paneID) ? new DSPBreakpointDetailPane() : null; + } + + @Override + public @Nullable String getDetailPaneName(final String id) { + return PANE_ID.equals(id) ? PANE_NAME : null; + } + + @Override + public @Nullable String getDetailPaneDescription(final String id) { + return PANE_ID.equals(id) ? PANE_DESCRIPTION : null; + } + + private boolean isDSPBreakpointSelection(final IStructuredSelection selection) { + if (selection.size() != 1) { + return false; + } + final Object element = selection.getFirstElement(); + if (element == null) + return false; + + return element instanceof DSPLineBreakpoint + || Adapters.adapt(element, IBreakpoint.class) instanceof DSPLineBreakpoint; + } +} diff --git a/org.eclipse.lsp4e.debug/src/org/eclipse/lsp4e/debug/presentation/SourceCodeEditor.java b/org.eclipse.lsp4e.debug/src/org/eclipse/lsp4e/debug/presentation/SourceCodeEditor.java new file mode 100644 index 000000000..ccff8f4f4 --- /dev/null +++ b/org.eclipse.lsp4e.debug/src/org/eclipse/lsp4e/debug/presentation/SourceCodeEditor.java @@ -0,0 +1,170 @@ +/******************************************************************************* + * Copyright (c) 2025 Vegard IT GmbH and others. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Sebastian Thomschke (Vegard IT GmbH) - initial implementation. + *******************************************************************************/ +package org.eclipse.lsp4e.debug.presentation; + +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.Platform; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.layout.GridLayoutFactory; +import org.eclipse.jface.resource.JFaceResources; +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.presentation.IPresentationReconciler; +import org.eclipse.jface.text.source.ISourceViewer; +import org.eclipse.jface.text.source.SourceViewer; +import org.eclipse.jface.text.source.SourceViewerConfiguration; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.tm4e.core.grammar.IGrammar; +import org.eclipse.tm4e.registry.TMEclipseRegistryPlugin; +import org.eclipse.tm4e.ui.TMUIPlugin; +import org.eclipse.tm4e.ui.text.TMPresentationReconciler; + +/** + * Editor wrapper that normalizes access to the underlying text widget and + * allows optional syntax highlighting if TM4E plugin is present. + */ +abstract class SourceCodeEditor extends Composite { + + private static final class PlainEditor extends SourceCodeEditor { + private final StyledText text; + + PlainEditor(Composite parent, int style) { + super(parent, style); + text = new StyledText(this, SWT.BORDER | SWT.MULTI | SWT.V_SCROLL | SWT.H_SCROLL); + } + + @Override + StyledText getTextWidget() { + return text; + } + } + + /** + * Source editor based on TM4E TextMate syntax highlighting + */ + private static final class TMEditor extends SourceCodeEditor { + + private final TMPresentationReconciler reconciler = new TMPresentationReconciler(); + private final SourceViewer viewer; + private final Document doc; + + TMEditor(final Composite parent, final int style) { + super(parent, style); + viewer = new SourceViewer(this, null, null, false, SWT.BORDER | SWT.MULTI | SWT.V_SCROLL | SWT.H_SCROLL); + viewer.configure(new SourceViewerConfiguration() { + @Override + public IPresentationReconciler getPresentationReconciler(final @Nullable ISourceViewer sourceViewer) { + return reconciler; + } + }); + doc = new Document(); + viewer.setDocument(doc); + } + + @Override + StyledText getTextWidget() { + return viewer.getTextWidget(); + } + + @Override + void setText(String text) { + doc.set(text); + } + + @Override + String getText() { + return viewer.getTextWidget().getText(); + } + + @Override + void configureForResource(@Nullable IResource resource) { + if (resource == null) { + return; + } + final var contentTypes = Platform.getContentTypeManager().findContentTypesFor(resource.getName()); + final IGrammar grammar = TMEclipseRegistryPlugin.getGrammarRegistryManager().getGrammarFor(contentTypes); + reconciler.setGrammar(grammar); + if (grammar != null) { + final var theme = TMUIPlugin.getThemeManager().getThemeForScope(grammar.getScopeName()); + final StyledText styledText = viewer.getTextWidget(); + styledText.setFont(JFaceResources.getTextFont()); + styledText.setForeground(null); + styledText.setBackground(null); + theme.initializeViewerColors(styledText); + reconciler.setTheme(theme); + } + } + } + + static SourceCodeEditor create(Composite parent, int style) { + if (Platform.getBundle("org.eclipse.tm4e.ui") != null) { + return new TMEditor(parent, style); + } + return new PlainEditor(parent, style); + } + + private SourceCodeEditor(Composite parent, int style) { + super(parent, style); + GridLayoutFactory.swtDefaults().margins(0, 0).applyTo(this); + } + + /** + * Underlying text widget to use for caret/selection operations. + */ + abstract StyledText getTextWidget(); + + /** + * Optional hook for syntax highlighting based on the resource's content type. + */ + void configureForResource(@Nullable IResource resource) { + // default no-op + } + + void setEditorLayoutData(GridDataFactory data) { + data.applyTo(this); + GridDataFactory.fillDefaults().grab(true, true).applyTo(getTextWidget()); + } + + String getText() { + return getTextWidget().getText(); + } + + void setText(String text) { + getTextWidget().setText(text); + } + + void addModifyListener(ModifyListener l) { + getTextWidget().addModifyListener(l); + } + + Point getSelection() { + return getTextWidget().getSelection(); + } + + void setSelection(int caret) { + getTextWidget().setSelection(caret); + } + + @Override + public void setToolTipText(@Nullable String string) { + getTextWidget().setToolTipText(string); + } + + @Override + public void setEnabled(boolean enabled) { + getTextWidget().setEnabled(enabled); + } +} diff --git a/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/debug/BreakpointMappingTest.java b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/debug/BreakpointMappingTest.java new file mode 100644 index 000000000..5d2a9ce6b --- /dev/null +++ b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/debug/BreakpointMappingTest.java @@ -0,0 +1,116 @@ +/******************************************************************************* + * Copyright (c) 2025 Vegard IT GmbH and others. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Sebastian Thomschke (Vegard IT GmbH) - initial implementation. + *******************************************************************************/ +package org.eclipse.lsp4e.test.debug; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.debug.core.DebugPlugin; +import org.eclipse.debug.core.model.IBreakpoint; +import org.eclipse.lsp4e.debug.breakpoints.DSPLineBreakpoint; +import org.eclipse.lsp4e.debug.debugmodel.DSPBreakpointManager; +import org.eclipse.lsp4e.test.utils.AbstractTestWithProject; +import org.eclipse.lsp4e.test.utils.TestUtils; +import org.eclipse.lsp4j.debug.Breakpoint; +import org.eclipse.lsp4j.debug.SetBreakpointsArguments; +import org.eclipse.lsp4j.debug.SetBreakpointsResponse; +import org.eclipse.lsp4j.debug.SourceBreakpoint; +import org.eclipse.lsp4j.debug.services.IDebugProtocolServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Verifies that DSPBreakpointManager maps marker attributes to DAP + * SourceBreakpoint (condition, column, hitCondition). + */ +class BreakpointMappingTest extends AbstractTestWithProject { + + private static class CapturingServer implements IDebugProtocolServer { + List calls = new ArrayList<>(); + + @Override + public CompletableFuture setBreakpoints(SetBreakpointsArguments arguments) { + synchronized (calls) { + calls.add(arguments); + } + var resp = new SetBreakpointsResponse(); + resp.setBreakpoints(new Breakpoint[0]); + return CompletableFuture.completedFuture(resp); + } + } + + private List created = new ArrayList<>(); + + @BeforeEach + void clearBreakpoints() throws CoreException { + // Ensure a clean slate for the test case + for (IBreakpoint bp : DebugPlugin.getDefault().getBreakpointManager().getBreakpoints()) { + bp.delete(); + } + } + + @AfterEach + void cleanupCreated() throws CoreException { + for (IBreakpoint bp : created) { + bp.delete(); + } + } + + @Test + void breakpoint_conditions_are_sent_to_server() throws Exception { + IFile file = TestUtils.createUniqueTestFile(project, "txt", "first line\nsecond line\n"); + + var bp = new DSPLineBreakpoint(file, 2); + bp.setCondition("x > 0"); + bp.setColumn(7); + bp.setHitCondition(">= 3"); + created.add(bp); + DebugPlugin.getDefault().getBreakpointManager().addBreakpoint(bp); + + var server = new CapturingServer(); + var manager = new DSPBreakpointManager(DebugPlugin.getDefault().getBreakpointManager(), server, null); + + try { + manager.initialize().join(); + + SetBreakpointsArguments matching = null; + synchronized (server.calls) { + assertTrue(!server.calls.isEmpty(), "No setBreakpoints() calls captured"); + String path = file.getLocation().toOSString(); + for (SetBreakpointsArguments a : server.calls) { + if (a.getSource() != null && path.equals(a.getSource().getPath())) { + matching = a; + break; + } + } + } + + assertNotNull(matching, "No setBreakpoints() call for our file was captured"); + SourceBreakpoint[] sent = matching.getBreakpoints(); + assertNotNull(sent); + assertEquals(1, sent.length, "Expected exactly one SourceBreakpoint"); + + SourceBreakpoint sb = sent[0]; + assertEquals("x > 0", sb.getCondition()); + assertEquals(7, sb.getColumn()); + assertEquals(">= 3", sb.getHitCondition()); + } finally { + manager.shutdown(); + } + } +} diff --git a/target-platforms/target-platform-latest/target-platform-latest.target b/target-platforms/target-platform-latest/target-platform-latest.target index c0d82d56b..185a42306 100644 --- a/target-platforms/target-platform-latest/target-platform-latest.target +++ b/target-platforms/target-platform-latest/target-platform-latest.target @@ -32,7 +32,7 @@ com.vegardit.no-npe no-npe-eea-all - 1.3.7 + 1.3.8 jar