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