diff --git a/ide/lsp.client/nbproject/project.properties b/ide/lsp.client/nbproject/project.properties
index cdc99aad839b..5c12ed9c52dd 100644
--- a/ide/lsp.client/nbproject/project.properties
+++ b/ide/lsp.client/nbproject/project.properties
@@ -26,4 +26,4 @@ release.external/org.eclipse.lsp4j.jsonrpc.debug-0.13.0.jar=modules/ext/org.ecli
release.external/org.eclipse.xtend.lib-2.24.0.jar=modules/ext/org.eclipse.xtend.lib-2.24.0.jar
release.external/org.eclipse.xtend.lib.macro-2.24.0.jar=modules/ext/org.eclipse.xtend.lib.macro-2.24.0.jar
release.external/org.eclipse.xtext.xbase.lib-2.24.0.jar=modules/ext/org.eclipse.xtext.xbase.lib-2.24.0.jar
-spec.version.base=1.28.0
+spec.version.base=1.29.0
diff --git a/ide/lsp.client/nbproject/project.xml b/ide/lsp.client/nbproject/project.xml
index fb3fc407596f..b5bb7c5e5a46 100644
--- a/ide/lsp.client/nbproject/project.xml
+++ b/ide/lsp.client/nbproject/project.xml
@@ -50,6 +50,15 @@
1.30
+
+ org.netbeans.api.debugger
+
+
+
+ 1
+ 1.82
+
+
org.netbeans.api.io
@@ -261,6 +270,15 @@
+
+ org.netbeans.spi.debugger.ui
+
+
+
+ 1
+ 2.85
+
+
org.netbeans.spi.editor.hints
@@ -279,6 +297,15 @@
1.40
+
+ org.netbeans.spi.viewmodel
+
+
+
+ 2
+ 1.78
+
+
org.openide.awt
@@ -429,6 +456,8 @@
+ org.netbeans.modules.lsp.client.debugger.api
+ org.netbeans.modules.lsp.client.debugger.spi
org.netbeans.modules.lsp.client.spi
diff --git a/ide/lsp.client/src/META-INF/debugger/DAPDebuggerSession/org.netbeans.modules.lsp.client.debugger.DAPDebugger b/ide/lsp.client/src/META-INF/debugger/DAPDebuggerSession/org.netbeans.modules.lsp.client.debugger.DAPDebugger
new file mode 100644
index 000000000000..461297e06adf
--- /dev/null
+++ b/ide/lsp.client/src/META-INF/debugger/DAPDebuggerSession/org.netbeans.modules.lsp.client.debugger.DAPDebugger
@@ -0,0 +1 @@
+org.netbeans.modules.lsp.client.debugger.DAPDebugger
diff --git a/ide/lsp.client/src/META-INF/debugger/DAPDebuggerSession/org.netbeans.spi.debugger.DebuggerEngineProvider b/ide/lsp.client/src/META-INF/debugger/DAPDebuggerSession/org.netbeans.spi.debugger.DebuggerEngineProvider
new file mode 100644
index 000000000000..c591ca306403
--- /dev/null
+++ b/ide/lsp.client/src/META-INF/debugger/DAPDebuggerSession/org.netbeans.spi.debugger.DebuggerEngineProvider
@@ -0,0 +1 @@
+org.netbeans.modules.lsp.client.debugger.DAPDebuggerEngineProvider
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPActionsProvider.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPActionsProvider.java
new file mode 100644
index 000000000000..51560713c61c
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPActionsProvider.java
@@ -0,0 +1,129 @@
+/*
+ * 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.lsp.client.debugger;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import org.netbeans.api.debugger.ActionsManager;
+import org.netbeans.spi.debugger.ActionsProvider;
+import org.netbeans.spi.debugger.ActionsProviderSupport;
+import org.netbeans.spi.debugger.ContextProvider;
+import org.openide.util.RequestProcessor;
+
+@ActionsProvider.Registration(path=DAPDebugger.SESSION_TYPE_ID, actions={"start", "stepInto", "stepOver", "stepOut",
+ "pause", "continue", "kill"})
+public final class DAPActionsProvider extends ActionsProviderSupport implements ChangeListener {
+
+ private static final Logger LOGGER = Logger.getLogger(DAPActionsProvider.class.getName());
+
+ private static final Set ACTIONS = new HashSet<>();
+ private static final Set ACTIONS_TO_DISABLE = new HashSet<>();
+
+ static {
+ ACTIONS.add (ActionsManager.ACTION_KILL);
+ ACTIONS.add (ActionsManager.ACTION_CONTINUE);
+ ACTIONS.add (ActionsManager.ACTION_PAUSE);
+ ACTIONS.add (ActionsManager.ACTION_START);
+ ACTIONS.add (ActionsManager.ACTION_STEP_INTO);
+ ACTIONS.add (ActionsManager.ACTION_STEP_OVER);
+ ACTIONS.add (ActionsManager.ACTION_STEP_OUT);
+ ACTIONS_TO_DISABLE.addAll(ACTIONS);
+ // Ignore the KILL action
+ ACTIONS_TO_DISABLE.remove(ActionsManager.ACTION_KILL);
+ }
+
+ /** The ReqeustProcessor used by action performers. */
+ private static final RequestProcessor ACTIONS_WORKER = new RequestProcessor("DAP debugger actions RP", 1);
+ private static RequestProcessor killRequestProcessor;
+
+ private final DAPDebugger debugger;
+
+ public DAPActionsProvider(ContextProvider contextProvider) {
+ debugger = contextProvider.lookupFirst(null, DAPDebugger.class);
+ // init actions
+ for (Object action : ACTIONS) {
+ setEnabled (action, true);
+ }
+ debugger.addChangeListener(this);
+ }
+
+ @Override
+ public Set getActions () {
+ return ACTIONS;
+ }
+
+ @Override
+ public void doAction (Object action) {
+ LOGGER.log(Level.FINE, "DAPDebugger.doAction({0}), is kill = {1}", new Object[]{action, action == ActionsManager.ACTION_KILL});
+ if (action == ActionsManager.ACTION_KILL) {
+ debugger.finish();
+ } else if (action == ActionsManager.ACTION_CONTINUE) {
+ debugger.resume();
+ } else if (action == ActionsManager.ACTION_STEP_OVER) {
+ debugger.stepOver();
+ } else if (action == ActionsManager.ACTION_STEP_INTO) {
+ debugger.stepInto();
+ } else if (action == ActionsManager.ACTION_STEP_OUT) {
+ debugger.stepOut();
+ } else if (action == ActionsManager.ACTION_PAUSE) {
+ debugger.pause();
+ }
+ }
+
+ @Override
+ public void postAction(final Object action, final Runnable actionPerformedNotifier) {
+ setDebugActionsEnabled(false);
+ ACTIONS_WORKER.post(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ doAction(action);
+ } finally {
+ actionPerformedNotifier.run();
+ setDebugActionsEnabled(true);
+ }
+ }
+ });
+ }
+
+ private void setDebugActionsEnabled(boolean enabled) {
+ if (!enabled) {
+ for (Object action : ACTIONS_TO_DISABLE) {
+ setEnabled(action, enabled);
+ }
+ } else {
+ setEnabled(ActionsManager.ACTION_CONTINUE, debugger.isSuspended());
+ setEnabled(ActionsManager.ACTION_PAUSE, !debugger.isSuspended());
+ setEnabled(ActionsManager.ACTION_STEP_INTO, debugger.isSuspended());
+ setEnabled(ActionsManager.ACTION_STEP_OUT, debugger.isSuspended());
+ setEnabled(ActionsManager.ACTION_STEP_OVER, debugger.isSuspended());
+ }
+ }
+
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ setDebugActionsEnabled(true); //TODO...
+ }
+
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPConfigurationAccessor.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPConfigurationAccessor.java
new file mode 100644
index 000000000000..0a8f5f1c0fd7
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPConfigurationAccessor.java
@@ -0,0 +1,49 @@
+/*
+ * 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.lsp.client.debugger;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Map;
+import org.netbeans.modules.lsp.client.debugger.api.DAPConfiguration;
+import org.openide.util.Exceptions;
+
+public abstract class DAPConfigurationAccessor {
+ private static DAPConfigurationAccessor instance;
+
+ public static DAPConfigurationAccessor getInstance() {
+ try {
+ Class.forName(DAPConfiguration.class.getName(), true, DAPConfiguration.class.getClassLoader());
+ } catch (ClassNotFoundException ex) {
+ Exceptions.printStackTrace(ex);
+ }
+ return instance;
+ }
+
+ public static void setInstance(DAPConfigurationAccessor instance) {
+ DAPConfigurationAccessor.instance = instance;
+ }
+
+ public abstract OutputStream getOut(DAPConfiguration config);
+ public abstract InputStream getIn(DAPConfiguration config);
+ public abstract boolean getDelayLaunch(DAPConfiguration config);
+ public abstract Map getConfiguration(DAPConfiguration config);
+ public abstract String getSessionName(DAPConfiguration config);
+
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPDebugger.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPDebugger.java
new file mode 100644
index 000000000000..8049bec650f4
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPDebugger.java
@@ -0,0 +1,579 @@
+/*
+ * 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.lsp.client.debugger;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.Socket;
+import java.net.URI;
+import org.netbeans.modules.lsp.client.debugger.api.DAPConfiguration;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Predicate;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import javax.swing.event.ChangeListener;
+import org.eclipse.lsp4j.debug.Capabilities;
+import org.eclipse.lsp4j.debug.ConfigurationDoneArguments;
+import org.eclipse.lsp4j.debug.ContinueArguments;
+import org.eclipse.lsp4j.debug.ContinuedEventArguments;
+import org.eclipse.lsp4j.debug.DisconnectArguments;
+import org.eclipse.lsp4j.debug.EvaluateArguments;
+import org.eclipse.lsp4j.debug.InitializeRequestArguments;
+import org.eclipse.lsp4j.debug.NextArguments;
+import org.eclipse.lsp4j.debug.OutputEventArguments;
+import org.eclipse.lsp4j.debug.OutputEventArgumentsCategory;
+import org.eclipse.lsp4j.debug.PauseArguments;
+import org.eclipse.lsp4j.debug.ScopesArguments;
+import org.eclipse.lsp4j.debug.SetBreakpointsArguments;
+import org.eclipse.lsp4j.debug.Source;
+import org.eclipse.lsp4j.debug.SourceBreakpoint;
+import org.eclipse.lsp4j.debug.StackTraceArguments;
+import org.eclipse.lsp4j.debug.StepInArguments;
+import org.eclipse.lsp4j.debug.StepOutArguments;
+import org.eclipse.lsp4j.debug.SteppingGranularity;
+import org.eclipse.lsp4j.debug.StoppedEventArguments;
+import org.eclipse.lsp4j.debug.TerminateArguments;
+import org.eclipse.lsp4j.debug.TerminatedEventArguments;
+import org.eclipse.lsp4j.debug.Thread;
+import org.eclipse.lsp4j.debug.ThreadEventArguments;
+import org.eclipse.lsp4j.debug.ThreadEventArgumentsReason;
+import org.eclipse.lsp4j.debug.VariablesArguments;
+import org.eclipse.lsp4j.debug.launch.DSPLauncher;
+import org.eclipse.lsp4j.debug.services.IDebugProtocolClient;
+import org.eclipse.lsp4j.debug.services.IDebugProtocolServer;
+import org.eclipse.lsp4j.jsonrpc.Launcher;
+import org.eclipse.lsp4j.jsonrpc.services.JsonRequest;
+import org.netbeans.api.debugger.Breakpoint;
+import org.netbeans.api.debugger.DebuggerEngine;
+import org.netbeans.api.debugger.DebuggerInfo;
+import org.netbeans.api.debugger.DebuggerManager;
+import org.netbeans.api.debugger.DebuggerManagerAdapter;
+import org.netbeans.api.debugger.DebuggerManagerListener;
+import org.netbeans.api.debugger.Session;
+import org.netbeans.api.io.InputOutput;
+import org.netbeans.modules.lsp.client.debugger.spi.BreakpointConvertor;
+import org.netbeans.spi.debugger.ContextProvider;
+import org.netbeans.spi.debugger.DebuggerEngineProvider;
+import org.netbeans.spi.debugger.SessionProvider;
+import org.openide.text.Line;
+import org.openide.util.ChangeSupport;
+import org.openide.util.Exceptions;
+import org.openide.util.Lookup;
+import org.openide.util.RequestProcessor;
+import org.netbeans.modules.lsp.client.debugger.spi.BreakpointConvertor.ConvertedBreakpointConsumer;
+import org.openide.util.Utilities;
+
+public final class DAPDebugger implements IDebugProtocolClient {
+ public static final String ENGINE_TYPE_ID = "DAPDebuggerEngine";
+ public static final String SESSION_TYPE_ID = "DAPDebuggerSession";
+ public static final String DEBUGGER_INFO_TYPE_ID = "DAPDebuggerInfo";
+
+ private static final Logger LOG = Logger.getLogger(DAPDebugger.class.getName());
+ private static final RequestProcessor WORKER = new RequestProcessor(DAPDebugger.class.getName(), 1, false, false);
+
+ private final DAPDebuggerEngineProvider engineProvider;
+ private final Session session;
+ private final ContextProvider contextProvider;
+ private final DebuggerManagerListener updateBreakpointsListener;
+
+ private final ChangeSupport cs = new ChangeSupport(this);
+ private final CompletableFuture initialized = new CompletableFuture<>();
+ private final CompletableFuture terminated = new CompletableFuture<>();
+ private final AtomicBoolean suspended = new AtomicBoolean();
+ private final Map id2Thread = new HashMap<>(); //TODO: concurrent/synchronization!!!
+ private final AtomicReference runAfterConfigureDone = new AtomicReference<>();
+ private URIPathConvertor fileConvertor;
+ private InputStream in;
+ private Future launched;
+ private IDebugProtocolServer server;
+ private int currentThreadId = -1;
+
+ public DAPDebugger(ContextProvider contextProvider) {
+ this.contextProvider = contextProvider;
+ // init engineProvider
+ this.engineProvider = (DAPDebuggerEngineProvider) contextProvider.lookupFirst(null, DebuggerEngineProvider.class);
+ this.session = contextProvider.lookupFirst(null, Session.class);
+ this.updateBreakpointsListener = new DebuggerManagerAdapter() {
+ @Override
+ public void breakpointAdded(Breakpoint breakpoint) {
+ updateAfterBreakpointChange(breakpoint);
+ }
+ @Override
+ public void breakpointRemoved(Breakpoint breakpoint) {
+ updateAfterBreakpointChange(breakpoint);
+ }
+ private void updateAfterBreakpointChange(Breakpoint breakpoint) {
+ Set modifiedURLs =
+ convertBreakpoints(breakpoint).stream()
+ .map(b -> b.uri())
+ .collect(Collectors.toSet());
+
+ try {
+ setBreakpoints(d -> modifiedURLs.contains(d.uri()));
+ } catch (InterruptedException | ExecutionException ex) {
+ Exceptions.printStackTrace(ex);
+ }
+ }
+ };
+ DebuggerManager.getDebuggerManager().addDebuggerListener(DebuggerManager.PROP_BREAKPOINTS, updateBreakpointsListener);
+ }
+
+ public CompletableFuture connect(DAPConfiguration config, Type type) throws Exception {
+ fileConvertor = DEFAULT_CONVERTOR;
+ in = DAPConfigurationAccessor.getInstance().getIn(config);
+ Launcher serverLauncher = DSPLauncher.createClientLauncher(this, in, DAPConfigurationAccessor.getInstance().getOut(config));//, false, new PrintWriter(System.err));
+ launched = serverLauncher.startListening();
+ server = serverLauncher.getRemoteProxy();
+ InitializeRequestArguments initialize = new InitializeRequestArguments();
+ initialize.setAdapterID("dap");
+ initialize.setLinesStartAt1(true);
+ initialize.setColumnsStartAt1(true);
+ Capabilities cap = server.initialize(initialize).get();
+ CompletableFuture connection;
+ if (!DAPConfigurationAccessor.getInstance().getDelayLaunch(config)) {
+ connection = switch (type) {
+ case ATTACH -> server.attach(DAPConfigurationAccessor.getInstance().getConfiguration(config));
+ case LAUNCH -> server.launch(DAPConfigurationAccessor.getInstance().getConfiguration(config));
+ default -> throw new UnsupportedOperationException("Unknown type: " + type);
+ };
+ } else {
+ connection = new CompletableFuture<>();
+ runAfterConfigureDone.set(() -> {
+ (switch (type) {
+ case ATTACH -> server.attach(DAPConfigurationAccessor.getInstance().getConfiguration(config));
+ case LAUNCH -> server.launch(DAPConfigurationAccessor.getInstance().getConfiguration(config));
+ default -> throw new UnsupportedOperationException("Unknown type: " + type);
+ }).handle((r, ex) -> {
+ if (ex != null) {
+ connection.completeExceptionally(ex);
+ } else {
+ connection.complete(r);
+ }
+ return null;
+ });
+ });
+ }
+ return CompletableFuture.allOf(connection, initialized);
+ }
+
+ @Override
+ public void initialized() {
+ WORKER.post(() -> {
+ try {
+ setBreakpoints(d -> true);
+ server.configurationDone(new ConfigurationDoneArguments()).get();
+ initialized.complete(null);
+ Runnable r = runAfterConfigureDone.get();
+ if (r != null) {
+ r.run();
+ runAfterConfigureDone.set(null);
+ }
+ } catch (ExecutionException | InterruptedException ex) {
+ LOG.log(Level.FINE, null, ex);
+ initialized.completeExceptionally(ex);
+ }
+ });
+ }
+
+ private void setBreakpoints(Predicate filter) throws InterruptedException, ExecutionException {
+ Map> url2Breakpoints = new HashMap<>();
+
+ for (LineBreakpointData data : convertBreakpoints(DebuggerManager.getDebuggerManager().getBreakpoints())) {
+ if (data != null && filter.test(data)) {
+ SourceBreakpoint lb = new SourceBreakpoint();
+
+ lb.setLine(data.lineNumber());
+ lb.setCondition(data.condition());
+
+ String path = fileConvertor.toPath(data.uri());
+
+ if (path != null) {
+ url2Breakpoints.computeIfAbsent(path, x -> new ArrayList<>())
+ .add(lb);
+ }
+ }
+ }
+
+ for (Entry> e : url2Breakpoints.entrySet()) {
+ Source src = new Source();
+
+ src.setPath(e.getKey());
+
+ SetBreakpointsArguments breakpoints = new SetBreakpointsArguments();
+
+ breakpoints.setSource(src);
+ breakpoints.setBreakpoints(e.getValue().toArray(SourceBreakpoint[]::new));
+ server.setBreakpoints(breakpoints).get(); //wait using .get()?
+ }
+ }
+
+ private List convertBreakpoints(Breakpoint... breakpoints) {
+ List lineBreakpoints = new ArrayList<>();
+ ConvertedBreakpointConsumer consumer = SPIAccessor.getInstance().createConvertedBreakpointConsumer(lineBreakpoints);
+
+ //TODO: could cache the convertors:
+ for (BreakpointConvertor convertor : Lookup.getDefault().lookupAll(BreakpointConvertor.class)) {
+ for (Breakpoint b : breakpoints) {
+ convertor.convert(b, consumer);
+ }
+ }
+
+ return lineBreakpoints;
+ }
+
+ @Override
+ public void continued(ContinuedEventArguments args) {
+ continued();
+ }
+
+ private void continued() {
+ suspended.set(false);
+ DAPThread prevThread = getCurrentThread();
+ if (prevThread != null) {
+ prevThread.setStack(new DAPFrame[0]);
+ prevThread.setStatus(DAPThread.Status.RUNNING);
+ }
+ currentThreadId = -1;
+ cs.fireChange(); //TODO: in a different thread?
+ DAPStackTraceAnnotationHolder.unmarkCurrent();
+ }
+
+ @Override
+ public void stopped(StoppedEventArguments args) {
+ //TODO: thread id is optional here(?!)
+ currentThreadId = args.getThreadId();
+ suspended.set(true);
+ DAPThread currentThread = getCurrentThread();
+ currentThread.setStatus(DAPThread.Status.SUSPENDED);
+ //TODO: the focus hint! maybe we don't want to change the current thread?
+ DebuggerManager.getDebuggerManager().setCurrentSession(session);
+ cs.fireChange(); //TODO: in a different thread?
+ StackTraceArguments sta = new StackTraceArguments();
+ sta.setThreadId(args.getThreadId());
+ server.stackTrace(sta).thenAccept(resp -> {
+ DAPFrame[] frames =
+ Arrays.stream(resp.getStackFrames())
+ .map(frame -> new DAPFrame(fileConvertor, currentThread, frame))
+ .toArray(DAPFrame[]::new);
+ currentThread.setStack(frames);
+ });
+ }
+
+ @Override
+ public void terminated(TerminatedEventArguments args) {
+ initialized.complete(null);
+ terminated.complete(null);
+ WORKER.post(() -> { //TODO: what if something else is running in WORKER? And OK to coalescence all the below?
+ cs.fireChange(); //TODO: in a different thread?
+ engineProvider.getDestructor().killEngine();
+ DAPStackTraceAnnotationHolder.unmarkCurrent(); //TODO: can this be done cleaner?
+ DebuggerManager.getDebuggerManager().removeDebuggerListener(DebuggerManager.PROP_BREAKPOINTS, updateBreakpointsListener);
+ launched.cancel(true);
+ try {
+ //XXX: cleaner
+ in.close();
+ } catch (IOException ex) {
+ Exceptions.printStackTrace(ex);
+ }
+ });
+ }
+
+ public CompletableFuture getTerminated() {
+ return terminated;
+ }
+
+ public boolean isSuspended() {
+ return suspended.get();
+ }
+
+ public void resume() {
+ ContinueArguments args = new ContinueArguments();
+ //some servers (e.g. the GraalVM DAP server) require the threadId to be always set, even if singleThread is set to false
+ args.setThreadId(currentThreadId);
+ args.setSingleThread(Boolean.FALSE);
+
+ continued();
+ server.continue_(args);
+ }
+
+ public void stepInto() {
+ StepInArguments args = new StepInArguments();
+
+ args.setSingleThread(true);
+ args.setThreadId(currentThreadId);
+ args.setGranularity(SteppingGranularity.LINE);
+
+ continued();
+ server.stepIn(args);
+ }
+
+ public void stepOver() {
+ NextArguments args = new NextArguments();
+
+ args.setSingleThread(true);
+ args.setThreadId(currentThreadId);
+ args.setGranularity(SteppingGranularity.LINE);
+
+ continued();
+ server.next(args);
+ }
+
+ public void stepOut() {
+ StepOutArguments args = new StepOutArguments();
+
+ args.setSingleThread(true);
+ args.setThreadId(currentThreadId);
+
+ continued();
+ server.stepOut(args);
+ }
+
+ public void pause() {
+ server.threads().thenAccept(response -> {
+ for (Thread t : response.getThreads()) {
+ PauseArguments args = new PauseArguments();
+
+ args.setThreadId(t.getId());
+ server.pause(args);
+ }
+ });
+ }
+
+ public CompletableFuture evaluate(DAPFrame frame, String expression) {
+ EvaluateArguments args = new EvaluateArguments();
+
+ //TODO: context?
+ args.setExpression(expression);
+
+ if (frame != null) {
+ args.setFrameId(frame.getId());
+ }
+
+ return server.evaluate(args).thenApply(evaluated -> {
+ return new DAPVariable(this, frame, null, evaluated.getVariablesReference(), expression, evaluated.getType(), evaluated.getResult(), Integer.MAX_VALUE); //TODO: totalChildren
+ });
+ }
+
+ public void finish() {
+ DisconnectArguments args = new DisconnectArguments();
+ server.disconnect(args).handle((result, exc) -> {
+ if (!terminated.isDone()) {
+ terminated(null);
+ }
+ return null;
+ });
+ }
+
+ public DAPFrame getCurrentFrame() {
+ DAPThread currentThread = getCurrentThread();
+
+ if (currentThread == null) {
+ return null;
+ }
+
+ return currentThread.getCurrentFrame();
+ }
+
+ public Line getCurrentLine() {
+ DAPThread currentThread = getCurrentThread();
+
+ if (currentThread == null) {
+ return null;
+ }
+
+ return currentThread.getCurrentLine();
+ }
+
+ public CompletableFuture> getFrameVariables(DAPFrame frame) {
+ ScopesArguments args = new ScopesArguments();
+
+ args.setFrameId(frame.getId());
+
+ //TODO: the various attributes:
+ return server.scopes(args).thenApply(scopesResponse -> {
+ return Arrays.stream(scopesResponse.getScopes())
+ .map(scope -> new DAPVariable(this, frame, null, scope.getVariablesReference(), scope.getName(), "", "", Integer.MAX_VALUE)) //TODO: totalChildren
+ .toList();
+ });
+ }
+
+ public CompletableFuture> getVariableChildren(DAPFrame frame, DAPVariable parentVariable) {
+ VariablesArguments args = new VariablesArguments();
+
+ args.setVariablesReference(parentVariable.getVariableReference());
+
+ return server.variables(args).thenApply(variablesResponse -> {
+ return Arrays.stream(variablesResponse.getVariables())
+ .map(var -> new DAPVariable(this, frame, parentVariable, var.getVariablesReference(), var.getName(), var.getType(), var.getValue(), Integer.MAX_VALUE))
+ .toList();
+ });
+ }
+
+ public DAPThread getCurrentThread() {
+ if (currentThreadId == (-1)) {
+ return null;
+ }
+
+ return getThread(currentThreadId);
+ }
+
+ public DAPThread[] getThreads() {
+ return id2Thread.values().toArray(DAPThread[]::new);
+ }
+
+ public void thread(ThreadEventArguments args) {
+ switch (args.getReason()) {
+ case ThreadEventArgumentsReason.STARTED -> getThread(args.getThreadId()).setStatus(DAPThread.Status.CREATED);
+ case ThreadEventArgumentsReason.EXITED -> getThread(args.getThreadId()).setStatus(DAPThread.Status.EXITED);
+ }
+ server.threads().thenAccept(allThreads -> {
+ for (Thread t : allThreads.getThreads()) {
+ getThread(t.getId()).setName(t.getName());
+ }
+ });
+ }
+
+ @Override
+ public void output(OutputEventArguments args) {
+ switch (args.getCategory()) {
+ case OutputEventArgumentsCategory.CONSOLE,
+ OutputEventArgumentsCategory.IMPORTANT -> getConsoleIO().getOut().print(args.getOutput());
+ case OutputEventArgumentsCategory.STDOUT -> getDebugeeIO().getOut().print(args.getOutput());
+ case OutputEventArgumentsCategory.STDERR -> getDebugeeIO().getErr().print(args.getOutput());
+ case OutputEventArgumentsCategory.TELEMETRY -> {}
+ }
+ }
+
+ private InputOutput console;
+ private InputOutput getConsoleIO() {
+ if (console == null) { //TODO: synchronization!!
+ console = InputOutput.get(session.getName() + ": Debugger console", false);
+ }
+ return console;
+ }
+
+ private InputOutput debugeeIO;
+ private InputOutput getDebugeeIO() {
+ //TODO: might be injected from the outside, presumably (for attach, although can we get here from attach?)
+ if (debugeeIO == null) { //TODO: synchronization!!
+ debugeeIO = InputOutput.get(session.getName(), false);
+ }
+ return debugeeIO;
+ }
+
+ private DAPThread getThread(int id) {
+ return id2Thread.computeIfAbsent(id, _id -> new DAPThread(this, _id));
+ }
+
+ public void addChangeListener(ChangeListener l) {
+ cs.addChangeListener(l);
+ }
+
+ public void removeChangeListener(ChangeListener l) {
+ cs.removeChangeListener(l);
+ }
+
+ public static void startDebugger(DAPConfiguration config, Type type) throws Exception {
+ SessionProvider sessionProvider = new SessionProvider () {
+ @Override
+ public String getSessionName () {
+ return DAPConfigurationAccessor.getInstance().getSessionName(config);
+ }
+
+ @Override
+ public String getLocationName () {
+ return "localhost";
+ }
+
+ @Override
+ public String getTypeID () {
+ return SESSION_TYPE_ID;
+ }
+
+ @Override
+ public Object[] getServices () {
+ return new Object[] {};
+ }
+ };
+ List allServices = new ArrayList<>();
+ allServices.add(config);
+ allServices.add(sessionProvider);
+ DebuggerInfo di = DebuggerInfo.create(
+ DEBUGGER_INFO_TYPE_ID,
+ allServices.toArray(Object[]::new)
+ );
+ DebuggerEngine[] es = DebuggerManager.getDebuggerManager ().
+ startDebugging (di);
+ DAPDebugger debugger = es[0].lookupFirst(null, DAPDebugger.class);
+ debugger.connect(config, type);
+ }
+
+ //non-standard extension of vscode-js-debug:
+ @JsonRequest
+ public CompletableFuture attachedChildSession(Map args) {
+ Map config = (Map) args.get("config");
+ CompletableFuture result = new CompletableFuture<>();
+ try {
+ int port = Integer.parseInt((String) config.get("__jsDebugChildServer"));
+ Socket newSocket = new Socket("localhost", port);
+ DAPConfiguration.create(newSocket.getInputStream(), newSocket.getOutputStream()).setSessionName((String) config.get("name")).attach();
+ result.complete(new HashMap<>());
+ } catch (Exception ex) {
+ LOG.log(Level.FINE, null, ex);
+ result.completeExceptionally(ex);
+ }
+ return result;
+ }
+
+ public enum Type {LAUNCH, ATTACH}
+
+ private static final URIPathConvertor DEFAULT_CONVERTOR = new URIPathConvertor() {
+ @Override
+ public String toPath(URI uri) {
+ if ("file".equals(uri.getScheme())) {
+ return uri.getPath();
+ }
+
+ return null;
+ }
+
+ @Override
+ public URI toURI(String path) {
+ return Utilities.toURI(new File(path));
+ }
+ };
+
+ interface URIPathConvertor {
+ public String toPath(URI uri);
+ public URI toURI(String path);
+ }
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPDebuggerEngineProvider.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPDebuggerEngineProvider.java
new file mode 100644
index 000000000000..12225d3442b8
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPDebuggerEngineProvider.java
@@ -0,0 +1,56 @@
+/*
+ * 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.lsp.client.debugger;
+
+import org.netbeans.api.debugger.DebuggerEngine;
+import org.netbeans.spi.debugger.DebuggerEngineProvider;
+
+
+public class DAPDebuggerEngineProvider extends DebuggerEngineProvider {
+
+ private DebuggerEngine.Destructor desctuctor;
+
+
+ @Override
+ public String[] getLanguages () {
+ return new String[] {""};
+ }
+
+ @Override
+ public String getEngineTypeID () {
+ return DAPDebugger.ENGINE_TYPE_ID;
+ }
+
+ @Override
+ public Object[] getServices () {
+ return new Object[] {};
+ }
+
+ @Override
+ public void setDestructor (DebuggerEngine.Destructor desctuctor) {
+ this.desctuctor = desctuctor;
+ }
+
+ public DebuggerEngine.Destructor getDestructor () {
+ return desctuctor;
+ }
+}
+
+
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPFrame.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPFrame.java
new file mode 100644
index 000000000000..d78b8fea4021
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPFrame.java
@@ -0,0 +1,126 @@
+/*
+ * 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.lsp.client.debugger;
+
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.logging.Logger;
+import org.eclipse.lsp4j.debug.StackFrame;
+
+import org.netbeans.api.annotations.common.CheckForNull;
+import org.netbeans.modules.lsp.client.debugger.DAPDebugger.URIPathConvertor;
+import org.netbeans.spi.debugger.ui.DebuggingView.DVFrame;
+import org.netbeans.spi.debugger.ui.DebuggingView.DVThread;
+
+import org.openide.cookies.LineCookie;
+import org.openide.filesystems.FileObject;
+import org.openide.filesystems.URLMapper;
+import org.openide.text.Line;
+
+public final class DAPFrame implements DVFrame {
+
+ private static final Logger LOGGER = Logger.getLogger(DAPFrame.class.getName());
+
+ private final URIPathConvertor fileConvertor;
+ private final DAPThread thread;
+ private final StackFrame frame;
+
+ public DAPFrame(URIPathConvertor fileConvertor, DAPThread thread, StackFrame frame) {
+ this.fileConvertor = fileConvertor;
+ this.thread = thread;
+ this.frame = frame;
+ }
+
+ @Override
+ public String getName() {
+ String name = frame.getName();
+
+ if (name.length() > 100) {
+ name = name.substring(0, 100) + "...";
+ }
+
+ return name;
+ }
+
+ @Override
+ public DVThread getThread() {
+ return thread;
+ }
+
+ @Override
+ public void makeCurrent() {
+ thread.setCurrentFrame(this);
+ }
+
+ @Override
+ public URI getSourceURI() {
+ //XXX: frame.getSource().getPath() may not work(!)
+ if (frame.getSource() == null || frame.getSource().getPath() == null) {
+ return null;
+ }
+ return fileConvertor.toURI(frame.getSource().getPath());
+ }
+
+ @Override
+ public int getLine() {
+ return frame.getLine();
+ }
+
+ @Override
+ public int getColumn() {
+ return -1;//TODO
+ }
+
+ @CheckForNull
+ public Line location() {
+ if (frame.getLine() == 0) {
+ return null;
+ }
+
+ URI sourceURI = getSourceURI();
+ if (sourceURI == null) {
+ return null;
+ }
+ FileObject file;
+ try {
+ if (!sourceURI.isAbsolute()) {
+ return null;
+ }
+
+ file = URLMapper.findFileObject(sourceURI.toURL());
+ } catch (MalformedURLException ex) {
+ return null;
+ }
+ if (file == null) {
+ return null;
+ }
+ LineCookie lc = file.getLookup().lookup(LineCookie.class);
+ return lc.getLineSet().getOriginal(frame.getLine() - 1);
+ }
+
+ public int getId() {
+ return frame.getId();
+ }
+
+ public String getDescription() {
+ return getName(); //TODO!!
+ }
+
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPStackTraceAnnotationHolder.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPStackTraceAnnotationHolder.java
new file mode 100644
index 000000000000..12361910f109
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPStackTraceAnnotationHolder.java
@@ -0,0 +1,113 @@
+/*
+ * 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.lsp.client.debugger;
+
+import javax.swing.SwingUtilities;
+
+import org.openide.text.Annotatable;
+import org.openide.text.Line;
+import org.openide.text.Line.ShowOpenType;
+import org.openide.text.Line.ShowVisibilityType;
+
+public final class DAPStackTraceAnnotationHolder {
+
+ private static DebuggerAnnotation[] currentAnnotations;
+
+ private DAPStackTraceAnnotationHolder() {
+ }
+
+ static synchronized void markCurrent (Annotatable[] annotatables) {
+ unmarkCurrent ();
+
+ int i = 0, k = annotatables.length;
+
+ // first line with icon in gutter
+ DebuggerAnnotation[] annotations = new DebuggerAnnotation [k];
+ if (annotatables [i] instanceof Line.Part) {
+ annotations [i] = new DebuggerAnnotation (
+ DebuggerAnnotation.CURRENT_LINE_PART_ANNOTATION_TYPE,
+ annotatables [i]
+ );
+ } else {
+ annotations [i] = new DebuggerAnnotation (
+ DebuggerAnnotation.CURRENT_LINE_ANNOTATION_TYPE,
+ annotatables [i]
+ );
+ }
+
+ // other lines
+ for (i = 1; i < k; i++) {
+ if (annotatables [i] instanceof Line.Part) {
+ annotations [i] = new DebuggerAnnotation (
+ DebuggerAnnotation.CALL_STACK_FRAME_ANNOTATION_TYPE,
+ annotatables [i]
+ );
+ } else {
+ annotations [i] = new DebuggerAnnotation (
+ DebuggerAnnotation.CALL_STACK_FRAME_ANNOTATION_TYPE,
+ annotatables [i]
+ );
+ }
+ }
+
+ currentAnnotations = annotations;
+
+ showLine(annotatables);
+ }
+
+ static synchronized void unmarkCurrent () {
+ if (currentAnnotations != null) {
+ int k = currentAnnotations.length;
+ for (int i = 0; i < k; i++) {
+ currentAnnotations[i].detach();
+ }
+ currentAnnotations = null;
+ }
+ }
+
+ public static void showLine (Annotatable[] a) {
+ SwingUtilities.invokeLater (new Runnable () {
+ @Override
+ public void run () {
+ if (a[0] instanceof Line) {
+ ((Line) a[0]).show (ShowOpenType.OPEN, ShowVisibilityType.FOCUS);
+ } else if (a[0] instanceof Line.Part) {
+ ((Line.Part) a[0]).getLine ().show (ShowOpenType.OPEN, ShowVisibilityType.FOCUS);
+ } else {
+ throw new InternalError(a[0].toString());
+ }
+ }
+ });
+ }
+
+ public static boolean contains (Object currentLine, Line line) {
+ if (currentLine == null) return false;
+ final Annotatable[] a = (Annotatable[]) currentLine;
+ int i, k = a.length;
+ for (i = 0; i < k; i++) {
+ if (a [i].equals (line)) return true;
+ if ( a [i] instanceof Line.Part &&
+ ((Line.Part) a [i]).getLine ().equals (line)
+ ) return true;
+ }
+ return false;
+ }
+
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPThread.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPThread.java
new file mode 100644
index 000000000000..f537a2a3480f
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPThread.java
@@ -0,0 +1,202 @@
+/*
+ * 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.lsp.client.debugger;
+
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.netbeans.api.debugger.Breakpoint;
+import org.netbeans.spi.debugger.ui.DebuggingView.DVSupport;
+import org.netbeans.spi.debugger.ui.DebuggingView.DVThread;
+import org.openide.text.Line;
+
+
+public final class DAPThread implements DVThread {
+
+ public static final String PROP_STACK = "stack";
+ public static final String PROP_CURRENT_FRAME = "currentFrame";
+
+ public enum Status {
+ CREATED,
+ RUNNING,
+ SUSPENDED,
+ EXITED
+ }
+
+ private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
+ private final DAPDebugger debugger;
+ private final int id;
+ private final AtomicReference currentFrame = new AtomicReference<>();
+ private final AtomicReference currentStack = new AtomicReference<>();
+ private Status status;
+ private String name;
+
+ public DAPThread(DAPDebugger debugger, int id) {
+ this.debugger = debugger;
+ this.id = id;
+ this.name = "Thread #" + id;
+ }
+
+ @Override
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public boolean isSuspended() {
+ return status == Status.SUSPENDED;
+ }
+
+ @Override
+ public void resume() {
+ //TODO
+ }
+
+ @Override
+ public void suspend() {
+ //TODO
+ }
+
+ @Override
+ public void makeCurrent() {
+ //TODO
+ }
+
+ @Override
+ public DVSupport getDVSupport() {
+ throw new UnsupportedOperationException("Not supported yet."); // Generated from nbfs://nbhost/SystemFileSystem/Templates/Classes/Code/GeneratedMethodBody
+ }
+
+ @Override
+ public List getLockerThreads() {
+ return null; //TODO
+ }
+
+ @Override
+ public void resumeBlockingThreads() {
+ //TODO
+ }
+
+ @Override
+ public Breakpoint getCurrentBreakpoint() {
+ return null; //TODO
+ }
+
+ @Override
+ public boolean isInStep() {
+ return false; //TODO
+ }
+
+ @Override
+ public void addPropertyChangeListener(PropertyChangeListener pcl) {
+ pcs.addPropertyChangeListener(pcl);
+ }
+
+ @Override
+ public void removePropertyChangeListener(PropertyChangeListener pcl) {
+ pcs.removePropertyChangeListener(pcl);
+ }
+
+ public void setStack(DAPFrame[] frames) {
+ currentStack.set(frames);
+ if (frames.length > 0) {
+ currentFrame.set(frames[0]);
+ } else {
+ currentFrame.set(null);
+ }
+
+ refreshAnnotations();
+
+ pcs.firePropertyChange(PROP_STACK, null, frames);
+ pcs.firePropertyChange(PROP_CURRENT_FRAME, null, getCurrentFrame());
+ }
+
+ public DAPFrame[] getStack() {
+ return currentStack.get();
+ }
+
+ public void setStatus(Status status) {
+ boolean wasSuspended = isSuspended();
+
+ this.status = status;
+
+ boolean isSuspended = isSuspended();
+
+ if (wasSuspended != isSuspended) {
+ pcs.firePropertyChange(PROP_SUSPENDED, wasSuspended, isSuspended);
+ }
+ }
+
+ public Status getStatus() {
+ return status;
+ }
+
+ public String getDetails() {
+ return null;
+ }
+
+ public DAPFrame getCurrentFrame() {
+ return currentFrame.get();
+ }
+
+ public void setCurrentFrame(DAPFrame frame) {
+ currentFrame.set(frame);
+ refreshAnnotations();
+ pcs.firePropertyChange(PROP_CURRENT_FRAME, null, getCurrentFrame());
+ }
+
+ public Line getCurrentLine() {
+ DAPFrame frame = currentFrame.get();
+
+ return frame != null ? frame.location() : null;
+ }
+
+ private void refreshAnnotations() {
+ DAPFrame[] frames = getStack();
+
+ if (frames.length == 0) {
+ DAPStackTraceAnnotationHolder.unmarkCurrent();
+ return ;
+ }
+
+ Line currentLine = getCurrentLine();
+ List stack = new ArrayList<>();
+
+ if (currentLine != null) {
+ stack.add(currentLine);
+ }
+
+ Arrays.stream(frames)
+ .map(f -> f.location())
+ .filter(l -> l != null) //TODO
+ .filter(l -> l != currentLine)
+ .forEach(stack::add);
+
+ DAPStackTraceAnnotationHolder.markCurrent(stack.toArray(Line[]::new));
+ }
+
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPVariable.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPVariable.java
new file mode 100644
index 000000000000..b7ab47cf3442
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPVariable.java
@@ -0,0 +1,103 @@
+/*
+ * 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.lsp.client.debugger;
+
+import java.util.Arrays;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicReference;
+import org.openide.util.Exceptions;
+
+/**
+ * Representation of a variable.
+ */
+public final class DAPVariable {
+
+ private final DAPDebugger debugger;
+ private final DAPFrame frame;
+ private final DAPVariable parentVariable;
+ private final int variableReference;
+ private final String name;
+ private final String type;
+ private final String value;
+ private final int totalChildren;
+ private final AtomicReference children = new AtomicReference<>();
+
+ DAPVariable(DAPDebugger debugger, DAPFrame frame, DAPVariable parentVariable, int variableReference, String name, String type, String value, int totalChildren) {
+ this.debugger = debugger;
+ this.frame = frame;
+ this.parentVariable = parentVariable;
+ this.variableReference = variableReference;
+ this.name = name;
+ this.type = type;
+ this.value = value;
+ this.totalChildren = totalChildren;
+ }
+
+ public DAPFrame getFrame() {
+ return frame;
+ }
+
+ public DAPVariable getParent() {
+ return parentVariable;
+ }
+
+ public int getVariableReference() {
+ return variableReference;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public int getTotalChildren() { //XXX:
+ return totalChildren;
+ }
+
+ public DAPVariable[] getChildren(int from, int to) {
+ DAPVariable[] vars = children.get();
+
+ if (vars == null) {
+ try {
+ children.set(vars = debugger.getVariableChildren(frame, this).get().toArray(DAPVariable[]::new));
+ } catch (InterruptedException | ExecutionException ex) {
+ return new DAPVariable[0];
+ }
+ }
+
+ if (from >= 0) {
+ to = Math.min(to, vars.length);
+ if (from < to) {
+ vars = Arrays.copyOfRange(vars, from, to);
+ } else {
+ vars = new DAPVariable[0];
+ }
+ }
+ return vars;
+ }
+
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DebuggerAnnotation.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DebuggerAnnotation.java
new file mode 100644
index 000000000000..751d2b9e4dc2
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DebuggerAnnotation.java
@@ -0,0 +1,72 @@
+/*
+ * 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.lsp.client.debugger;
+
+import org.openide.text.Annotatable;
+import org.openide.text.Annotation;
+import org.openide.util.NbBundle;
+
+
+/**
+ * Debugger Annotation class.
+ */
+public final class DebuggerAnnotation extends Annotation {
+
+ /** Annotation type constant. */
+ public static final String CURRENT_LINE_ANNOTATION_TYPE = "CurrentPC";
+ /** Annotation type constant. */
+ public static final String CURRENT_LINE_ANNOTATION_TYPE2 = "CurrentPC2";
+ /** Annotation type constant. */
+ public static final String CURRENT_LINE_PART_ANNOTATION_TYPE = "CurrentPCLinePart";
+ /** Annotation type constant. */
+ public static final String CURRENT_LINE_PART_ANNOTATION_TYPE2 = "CurrentPC2LinePart";
+ /** Annotation type constant. */
+ public static final String CALL_STACK_FRAME_ANNOTATION_TYPE = "CallSite";
+
+ private final String type;
+
+ public DebuggerAnnotation (String type, Annotatable annotatable) {
+ this.type = type;
+ attach (annotatable);
+ }
+
+ @Override
+ public String getAnnotationType () {
+ return type;
+ }
+
+ @Override
+ @NbBundle.Messages({"TTP_CurrentPC=Current Program Counter",
+ "TTP_CurrentPC2=Current Target",
+ "TTP_Callsite=Call Stack Line"})
+ public String getShortDescription () {
+ switch (type) {
+ case CURRENT_LINE_ANNOTATION_TYPE:
+ case CURRENT_LINE_PART_ANNOTATION_TYPE:
+ return Bundle.TTP_CurrentPC();
+ case CURRENT_LINE_ANNOTATION_TYPE2:
+ return Bundle.TTP_CurrentPC2();
+ case CALL_STACK_FRAME_ANNOTATION_TYPE:
+ return Bundle.TTP_Callsite();
+ default:
+ throw new IllegalStateException(type);
+ }
+ }
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/LineBreakpointData.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/LineBreakpointData.java
new file mode 100644
index 000000000000..04a37604e5ca
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/LineBreakpointData.java
@@ -0,0 +1,24 @@
+/*
+ * 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.lsp.client.debugger;
+
+import java.net.URI;
+
+public record LineBreakpointData(URI uri, int lineNumber, String condition) {
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/RegisterDAPDebuggerProcessor.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/RegisterDAPDebuggerProcessor.java
new file mode 100644
index 000000000000..3c72c7e7e0d1
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/RegisterDAPDebuggerProcessor.java
@@ -0,0 +1,56 @@
+/*
+ * 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.lsp.client.debugger;
+
+import java.util.Set;
+import javax.annotation.processing.Processor;
+import javax.annotation.processing.RoundEnvironment;
+import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.TypeElement;
+import org.netbeans.modules.lsp.client.debugger.api.RegisterDAPDebugger;
+import org.openide.filesystems.annotations.LayerBuilder;
+import org.openide.filesystems.annotations.LayerGeneratingProcessor;
+import org.openide.filesystems.annotations.LayerGenerationException;
+import org.openide.util.lookup.ServiceProvider;
+
+@ServiceProvider(service=Processor.class)
+@SupportedAnnotationTypes("org.netbeans.modules.lsp.client.debugger.api.RegisterDAPDebugger")
+public class RegisterDAPDebuggerProcessor extends LayerGeneratingProcessor {
+
+ @Override
+ protected boolean handleProcess(Set extends TypeElement> annotations, RoundEnvironment roundEnv) throws LayerGenerationException {
+ for (Element el : roundEnv.getElementsAnnotatedWith(RegisterDAPDebugger.class)) {
+ LayerBuilder builder = layer(el);
+
+ for (String mimeType : el.getAnnotation(RegisterDAPDebugger.class).mimeType()) {
+ builder.file("Editors/" + mimeType + "/breakpoints.instance")
+ .stringvalue("instanceOf", "org.netbeans.modules.lsp.client.debugger.api.RegisterDAPBreakpoints")
+ .methodvalue("instanceCreate", "org.netbeans.modules.lsp.client.debugger.api.RegisterDAPBreakpoints", "newInstance")
+ .write()
+ .file("Editors/" + mimeType + "/GlyphGutterActions/org-netbeans-modules-debugger-ui-actions-ToggleBreakpointAction.shadow")
+ .stringvalue("originalFile", "Actions/Debug/org-netbeans-modules-debugger-ui-actions-ToggleBreakpointAction.instance")
+ .intvalue("position", 500)
+ .write();
+ }
+ }
+ return true;
+ }
+
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/SPIAccessor.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/SPIAccessor.java
new file mode 100644
index 000000000000..c010d1b6c1c0
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/SPIAccessor.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.modules.lsp.client.debugger;
+
+import java.util.List;
+import org.netbeans.modules.lsp.client.debugger.spi.BreakpointConvertor.ConvertedBreakpointConsumer;
+import org.openide.util.Exceptions;
+
+public abstract class SPIAccessor {
+ private static volatile SPIAccessor INSTANCE;
+
+ public static SPIAccessor getInstance() {
+ try {
+ Class.forName(ConvertedBreakpointConsumer.class.getName(), true, ConvertedBreakpointConsumer.class.getClassLoader());
+ } catch (ClassNotFoundException ex) {
+ Exceptions.printStackTrace(ex);
+ }
+ return INSTANCE;
+ }
+
+ public static void setInstance(SPIAccessor accessor) {
+ INSTANCE = accessor;
+ }
+
+ public abstract ConvertedBreakpointConsumer createConvertedBreakpointConsumer(List lineBreakpoints);
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/api/DAPConfiguration.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/api/DAPConfiguration.java
new file mode 100644
index 000000000000..30c8b3c543d8
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/api/DAPConfiguration.java
@@ -0,0 +1,144 @@
+/*
+ * 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.lsp.client.debugger.api;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.HashMap;
+import java.util.Map;
+import org.netbeans.modules.lsp.client.debugger.DAPConfigurationAccessor;
+import org.netbeans.modules.lsp.client.debugger.DAPDebugger;
+import org.netbeans.modules.lsp.client.debugger.DAPDebugger.Type;
+import org.openide.util.Exceptions;
+
+/**Configure and start the Debugger Adapter Protocol (DAP) client. Start with
+ * {@link #create(java.io.InputStream, java.io.OutputStream) }.
+ *
+ * @since 1.29
+ */
+public class DAPConfiguration {
+ private final InputStream in;
+ private final OutputStream out;
+ private Map configuration = new HashMap<>();
+ private String sessionName = "";
+ private boolean delayLaunch;
+
+ /**
+ * Start the configuration of the DAP client. The provided input and output
+ * should will be used to communicate with the server.
+ *
+ * @param in stream from which the server's output will be read
+ * @param out stream to which data for the server will be written
+ * @return the DAP client configuration
+ */
+ public static DAPConfiguration create(InputStream in, OutputStream out) {
+ return new DAPConfiguration(in, out);
+ }
+
+ private DAPConfiguration(InputStream in, OutputStream out) {
+ this.in = in;
+ this.out = out;
+ }
+
+ /**
+ * Add arbitrary configuration which will be sent to the server unmodified.
+ *
+ * @param configuration the configuration to send to the server
+ * @return the DAP client configuration
+ */
+ public DAPConfiguration addConfiguration(Map configuration) {
+ this.configuration.putAll(configuration);
+ return this;
+ }
+
+ /**
+ * Set the name of the UI session that will be created.
+ *
+ * @param sessionName the name of the UI session.
+ * @return the DAP client configuration
+ */
+ public DAPConfiguration setSessionName(String sessionName) {
+ this.sessionName = sessionName;
+ return this;
+ }
+
+ /**
+ * If set, the debuggee will only be launched, or attached to, after full
+ * configuration. Should only be used if the given DAP server requires this
+ * handling.
+ *
+ * @return the DAP client configuration
+ */
+ public DAPConfiguration delayLaunch() {
+ this.delayLaunch = true;
+ return this;
+ }
+
+ /**
+ * Attach to an already running DAP server/debuggee, based on the configuration up to
+ * this point.
+ */
+ public void attach() {
+ try {
+ DAPDebugger.startDebugger(this, Type.ATTACH);
+ } catch (Exception ex) {
+ Exceptions.printStackTrace(ex);
+ }
+ }
+
+ /**
+ * Launch a new debuggee, based on the configuration so far.
+ */
+ public void launch() {
+ try {
+ DAPDebugger.startDebugger(this, Type.LAUNCH);
+ } catch (Exception ex) {
+ Exceptions.printStackTrace(ex);
+ }
+ }
+
+ static {
+ DAPConfigurationAccessor.setInstance(new DAPConfigurationAccessor() {
+ @Override
+ public OutputStream getOut(DAPConfiguration config) {
+ return config.out;
+ }
+
+ @Override
+ public InputStream getIn(DAPConfiguration config) {
+ return config.in;
+ }
+
+ @Override
+ public boolean getDelayLaunch(DAPConfiguration config) {
+ return config.delayLaunch;
+ }
+
+ @Override
+ public Map getConfiguration(DAPConfiguration config) {
+ return config.configuration;
+ }
+
+ @Override
+ public String getSessionName(DAPConfiguration config) {
+ return config.sessionName;
+ }
+ });
+ }
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/api/RegisterDAPBreakpoints.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/api/RegisterDAPBreakpoints.java
new file mode 100644
index 000000000000..4e7188f5c600
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/api/RegisterDAPBreakpoints.java
@@ -0,0 +1,38 @@
+/*
+ * 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.lsp.client.debugger.api;
+
+/**If registered in the MIME Lookup for a given MIME-type, this module will
+ * provide basic support for breakpoints for this language.
+ *
+ * Please use {@link RegisterDAPDebugger} to get full UI integration.
+ *
+ * @since 1.29
+ */
+public final class RegisterDAPBreakpoints {
+ private RegisterDAPBreakpoints() {}
+ /**
+ * Create a new instance of {@link RegisterDAPBreakpoints}, to be registred
+ * in the MIME Lookup.
+ * @return a new instance of RegisterDAPBreakpoints.
+ */
+ public static RegisterDAPBreakpoints newInstance() {
+ return new RegisterDAPBreakpoints();
+ }
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/api/RegisterDAPDebugger.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/api/RegisterDAPDebugger.java
new file mode 100644
index 000000000000..adb957a03157
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/api/RegisterDAPDebugger.java
@@ -0,0 +1,40 @@
+/*
+ * 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.lsp.client.debugger.api;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/** Annotate a package, and provide mime types, for which full breakpoints support
+ * should be generated.
+ *
+ * @since 1.29
+ */
+@Target(ElementType.PACKAGE)
+@Retention(RetentionPolicy.SOURCE)
+public @interface RegisterDAPDebugger {
+ /**
+ * MIME-types for which full breakpoints support should be generated.
+ *
+ * @return the MIME-types.
+ */
+ public String[] mimeType();
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/attach/Bundle.properties b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/attach/Bundle.properties
new file mode 100644
index 000000000000..ba39d6a5ba1c
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/attach/Bundle.properties
@@ -0,0 +1,26 @@
+# 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.
+
+DAPAttachPanel.jLabel1.text=&Hostname:
+DAPAttachPanel.jLabel2.text=&Port:
+DAPAttachPanel.jLabel3.text=Connection &type:
+DAPAttachPanel.jLabel4.text=jLabel4
+DAPAttachPanel.jLabel5.text=Additional &Configuration (JSON):
+DAPAttachPanel.hostname.text=
+DAPAttachPanel.port.text=
+DAPAttachPanel.jLabel6.text=Above should be any configuration needed for the given debugger and connection type
+DAPAttachPanel.delay.text=&Attach after configure (expert option, GDB)
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/attach/DAPAttachPanel.form b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/attach/DAPAttachPanel.form
new file mode 100644
index 000000000000..608a44803642
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/attach/DAPAttachPanel.form
@@ -0,0 +1,207 @@
+
+
+
+
+
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/attach/DAPAttachPanel.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/attach/DAPAttachPanel.java
new file mode 100644
index 000000000000..e9f27e0cc89d
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/attach/DAPAttachPanel.java
@@ -0,0 +1,246 @@
+/*
+ * 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.lsp.client.debugger.attach;
+
+import java.awt.Component;
+import java.util.Map;
+import javax.swing.ComboBoxModel;
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.DefaultListCellRenderer;
+import javax.swing.JList;
+import javax.swing.ListCellRenderer;
+import org.netbeans.api.debugger.Properties;
+import org.netbeans.modules.lsp.client.debugger.DAPDebugger.Type;
+import org.openide.util.Exceptions;
+import org.openide.util.NbBundle.Messages;
+
+public class DAPAttachPanel extends javax.swing.JPanel {
+
+ @Messages({
+ "DN_Attach=Attach",
+ "DN_Launch=Launch"
+ })
+ private static final Map connectionType2DisplayName = Map.of (
+ Type.ATTACH, Bundle.DN_Attach(),
+ Type.LAUNCH, Bundle.DN_Launch()
+ );
+
+ private static final String NODE = "attach";
+ private static final String KEY_HOSTNAME = "hostname";
+ private static final String KEY_PORT = "port";
+ private static final String KEY_CONNECTION_TYPE = "type";
+ private static final String KEY_CONFIGURATION = "configuration";
+ private static final String KEY_DELAY = "delay";
+ private static final String DEFAULT_HOSTNAME = "localhost";
+ private static final String DEFAULT_PORT = "";
+ private static final String DEFAULT_CONNECTION_TYPE = "ATTACH";
+ private static final String DEFAULT_CONFIGURATION = "";
+ private static final boolean DEFAULT_DELAY = false;
+
+ /**
+ * Creates new form DAPAttachPanel
+ */
+ public DAPAttachPanel() {
+ initComponents();
+ }
+
+ public void load(Properties prefs) {
+ hostname.setText(prefs.getString(KEY_HOSTNAME, DEFAULT_HOSTNAME));
+ port.setText(prefs.getString(KEY_PORT, DEFAULT_PORT));
+ try {
+ connectionType.setSelectedItem(Type.valueOf(prefs.getString(KEY_CONNECTION_TYPE, DEFAULT_CONNECTION_TYPE)));
+ } catch (IllegalArgumentException ex) {
+ connectionType.setSelectedItem(Type.ATTACH);
+ }
+ jsonConfiguration.setText(prefs.getString(KEY_CONFIGURATION, DEFAULT_CONFIGURATION));
+ delay.setSelected(prefs.getBoolean(KEY_DELAY, DEFAULT_DELAY));
+ }
+
+ public void save(Properties prefs) {
+ prefs.setString(KEY_HOSTNAME, getHostName());
+ prefs.setString(KEY_PORT, port.getText());
+ prefs.setString(KEY_CONNECTION_TYPE, getConnectionType().name());
+ prefs.setString(KEY_CONFIGURATION, getJSONConfiguration());
+ prefs.setBoolean(KEY_DELAY, getDelay());
+ }
+
+ /**
+ * This method is called from within the constructor to initialize the form.
+ * WARNING: Do NOT modify this code. The content of this method is always
+ * regenerated by the Form Editor.
+ */
+ @SuppressWarnings("unchecked")
+ // //GEN-BEGIN:initComponents
+ private void initComponents() {
+
+ jLabel4 = new javax.swing.JLabel();
+ jLabel1 = new javax.swing.JLabel();
+ jLabel2 = new javax.swing.JLabel();
+ jLabel3 = new javax.swing.JLabel();
+ jLabel5 = new javax.swing.JLabel();
+ jScrollPane1 = new javax.swing.JScrollPane();
+ jsonConfiguration = new javax.swing.JEditorPane();
+ delay = new javax.swing.JCheckBox();
+ hostname = new javax.swing.JTextField();
+ port = new javax.swing.JTextField();
+ connectionType = new javax.swing.JComboBox<>();
+ jLabel6 = new javax.swing.JLabel();
+
+ org.openide.awt.Mnemonics.setLocalizedText(jLabel4, org.openide.util.NbBundle.getMessage(DAPAttachPanel.class, "DAPAttachPanel.jLabel4.text")); // NOI18N
+
+ jLabel1.setLabelFor(hostname);
+ org.openide.awt.Mnemonics.setLocalizedText(jLabel1, org.openide.util.NbBundle.getMessage(DAPAttachPanel.class, "DAPAttachPanel.jLabel1.text")); // NOI18N
+
+ jLabel2.setLabelFor(port);
+ org.openide.awt.Mnemonics.setLocalizedText(jLabel2, org.openide.util.NbBundle.getMessage(DAPAttachPanel.class, "DAPAttachPanel.jLabel2.text")); // NOI18N
+
+ org.openide.awt.Mnemonics.setLocalizedText(jLabel3, org.openide.util.NbBundle.getMessage(DAPAttachPanel.class, "DAPAttachPanel.jLabel3.text")); // NOI18N
+
+ jLabel5.setLabelFor(jsonConfiguration);
+ org.openide.awt.Mnemonics.setLocalizedText(jLabel5, org.openide.util.NbBundle.getMessage(DAPAttachPanel.class, "DAPAttachPanel.jLabel5.text")); // NOI18N
+
+ jsonConfiguration.setContentType("application/json"); // NOI18N
+ jScrollPane1.setViewportView(jsonConfiguration);
+
+ org.openide.awt.Mnemonics.setLocalizedText(delay, org.openide.util.NbBundle.getMessage(DAPAttachPanel.class, "DAPAttachPanel.delay.text")); // NOI18N
+
+ hostname.setText(org.openide.util.NbBundle.getMessage(DAPAttachPanel.class, "DAPAttachPanel.hostname.text")); // NOI18N
+
+ port.setText(org.openide.util.NbBundle.getMessage(DAPAttachPanel.class, "DAPAttachPanel.port.text")); // NOI18N
+
+ connectionType.setModel(getConnectionTypeModel());
+ connectionType.setRenderer(getConnectionTypeRenderer());
+
+ jLabel6.setFont(new java.awt.Font("sansserif", 2, 13)); // NOI18N
+ org.openide.awt.Mnemonics.setLocalizedText(jLabel6, org.openide.util.NbBundle.getMessage(DAPAttachPanel.class, "DAPAttachPanel.jLabel6.text")); // NOI18N
+
+ javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
+ this.setLayout(layout);
+ layout.setHorizontalGroup(
+ layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+ .addGroup(layout.createSequentialGroup()
+ .addContainerGap()
+ .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+ .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE)
+ .addGroup(layout.createSequentialGroup()
+ .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+ .addComponent(jLabel1)
+ .addComponent(jLabel2)
+ .addComponent(jLabel3))
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
+ .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+ .addComponent(hostname)
+ .addComponent(port)
+ .addComponent(connectionType, 0, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)))
+ .addGroup(layout.createSequentialGroup()
+ .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+ .addComponent(jLabel5)
+ .addComponent(delay)
+ .addComponent(jLabel6))
+ .addGap(0, 0, Short.MAX_VALUE)))
+ .addContainerGap())
+ );
+ layout.setVerticalGroup(
+ layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
+ .addGroup(layout.createSequentialGroup()
+ .addContainerGap()
+ .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
+ .addComponent(jLabel1)
+ .addComponent(hostname, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
+ .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
+ .addComponent(jLabel2)
+ .addComponent(port, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
+ .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
+ .addComponent(jLabel3)
+ .addComponent(connectionType, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
+ .addComponent(jLabel5)
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
+ .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 95, Short.MAX_VALUE)
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
+ .addComponent(jLabel6)
+ .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
+ .addComponent(delay)
+ .addGap(10, 10, 10))
+ );
+ }// //GEN-END:initComponents
+
+
+ // Variables declaration - do not modify//GEN-BEGIN:variables
+ private javax.swing.JComboBox connectionType;
+ private javax.swing.JCheckBox delay;
+ private javax.swing.JTextField hostname;
+ private javax.swing.JLabel jLabel1;
+ private javax.swing.JLabel jLabel2;
+ private javax.swing.JLabel jLabel3;
+ private javax.swing.JLabel jLabel4;
+ private javax.swing.JLabel jLabel5;
+ private javax.swing.JLabel jLabel6;
+ private javax.swing.JScrollPane jScrollPane1;
+ private javax.swing.JEditorPane jsonConfiguration;
+ private javax.swing.JTextField port;
+ // End of variables declaration//GEN-END:variables
+
+ private ComboBoxModel getConnectionTypeModel() {
+ DefaultComboBoxModel result = new DefaultComboBoxModel<>();
+
+ result.addElement(Type.ATTACH);
+ result.addElement(Type.LAUNCH);
+ return result;
+ }
+
+ private ListCellRenderer super Type> getConnectionTypeRenderer() {
+ return new DefaultListCellRenderer() {
+ @Override
+ public Component getListCellRendererComponent(JList> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
+ if (value instanceof Type type) {
+ value = connectionType2DisplayName.get(type);
+ }
+ return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
+ }
+ };
+ }
+
+ public String getHostName() {
+ return hostname.getText();
+ }
+
+ public int getPort() {
+ try {
+ return Integer.parseInt(port.getText());
+ } catch (NumberFormatException ex) {
+ Exceptions.printStackTrace(ex);
+ return -1;
+ }
+ }
+
+ public Type getConnectionType() {
+ return (Type) connectionType.getSelectedItem();
+ }
+
+ public String getJSONConfiguration() {
+ return jsonConfiguration.getText();
+ }
+
+ public boolean getDelay() {
+ return delay.isSelected();
+ }
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/attach/DAPAttachType.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/attach/DAPAttachType.java
new file mode 100644
index 000000000000..ccdb9068859c
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/attach/DAPAttachType.java
@@ -0,0 +1,141 @@
+/*
+ * 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.lsp.client.debugger.attach;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import java.io.IOException;
+import java.net.Socket;
+import java.util.HashMap;
+import java.util.Map;
+import javax.swing.JComponent;
+import org.netbeans.api.debugger.Properties;
+import org.netbeans.modules.lsp.client.debugger.DAPDebugger.Type;
+import org.netbeans.modules.lsp.client.debugger.api.DAPConfiguration;
+import org.netbeans.spi.debugger.ui.AttachType;
+import org.netbeans.spi.debugger.ui.AttachType.Registration;
+import org.netbeans.spi.debugger.ui.Controller;
+import org.netbeans.spi.debugger.ui.PersistentController;
+import org.openide.util.Exceptions;
+import org.openide.util.NbBundle.Messages;
+import org.openide.util.RequestProcessor;
+
+@Registration(displayName="#DN_DAPAttach")
+@Messages({
+ "DN_DAPAttach=Debuger Adapter Protocol (DAP) Debugger",
+ "DN_Default=Default configuration"
+})
+public final class DAPAttachType extends AttachType {
+
+ private static final RequestProcessor WORKER = new RequestProcessor(DAPAttachType.class.getName(), 1, false, false);
+
+ private DAPAttachPanel panel;
+
+ @Override
+ public JComponent getCustomizer() {
+ if (panel == null) {
+ panel = new DAPAttachPanel();
+ panel.load(getPrivateSettings());
+ }
+
+ return panel;
+ }
+
+ @Override
+ public Controller getController() {
+ return new PersistentController() {
+ private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
+
+ @Override
+ public boolean ok() {
+ String hostname = panel.getHostName();
+ int port = panel.getPort();
+ Type connectionType = panel.getConnectionType();
+ String configuration = panel.getJSONConfiguration();
+ boolean delay = panel.getDelay();
+ WORKER.post(() -> {
+ try {
+ Socket socket = new Socket(hostname, port);
+ DAPConfiguration dapConfig = DAPConfiguration.create(socket.getInputStream(), socket.getOutputStream());
+ if (!configuration.isBlank()) {
+ Map args = new Gson().fromJson(configuration, HashMap.class);
+ dapConfig.addConfiguration(args);
+ }
+ if (delay) {
+ dapConfig.delayLaunch();
+ }
+ switch (connectionType) {
+ case ATTACH -> dapConfig.attach();
+ case LAUNCH -> dapConfig.launch();
+ default -> throw new IllegalStateException("Unknown connection type: " + connectionType);
+ }
+ } catch (IOException | JsonSyntaxException ex) {
+ Exceptions.printStackTrace(ex);
+ }
+ });
+ return true;
+ }
+
+ @Override
+ public boolean cancel() {
+ return true;
+ }
+
+ @Override
+ public boolean isValid() {
+ return true; //TODO
+ }
+
+ @Override
+ public void addPropertyChangeListener(PropertyChangeListener l) {
+ pcs.addPropertyChangeListener(l);
+ }
+
+ @Override
+ public void removePropertyChangeListener(PropertyChangeListener l) {
+ pcs.removePropertyChangeListener(l);
+ }
+
+ @Override
+ public String getDisplayName() {
+ return Bundle.DN_Default();
+ }
+
+ @Override
+ public boolean load(Properties props) {
+ panel.load(props);
+ return true;
+ }
+
+ @Override
+ public void save(Properties props) {
+ panel.save(props);
+ panel.save(getPrivateSettings());
+ }
+ };
+ }
+
+ private static Properties getPrivateSettings() {
+ //the debugger does not seem to call "load" on the saved settings, so
+ //storing the settings in a private location as well:
+ return Properties.getDefault().getProperties("debugger").getProperties("dap_attach_configuration");
+ }
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/BreakpointAnnotationProvider.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/BreakpointAnnotationProvider.java
new file mode 100644
index 000000000000..308d8223f7d1
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/BreakpointAnnotationProvider.java
@@ -0,0 +1,260 @@
+/*
+ * 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.lsp.client.debugger.breakpoints;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.netbeans.api.debugger.Breakpoint;
+import org.netbeans.api.debugger.Breakpoint.VALIDITY;
+import org.netbeans.api.debugger.DebuggerManager;
+import org.netbeans.api.debugger.DebuggerManagerAdapter;
+
+import org.openide.filesystems.FileObject;
+import org.openide.loaders.DataObject;
+import org.openide.text.Annotation;
+import org.openide.text.AnnotationProvider;
+import org.openide.text.Line;
+import org.openide.util.Lookup;
+import org.openide.util.RequestProcessor;
+import org.openide.util.WeakSet;
+
+
+/**
+ * This class is called when some file in editor is opened. It changes if
+ * some breakpoints are added or removed.
+ *
+ */
+@org.openide.util.lookup.ServiceProvider(service=org.openide.text.AnnotationProvider.class)
+public final class BreakpointAnnotationProvider extends DebuggerManagerAdapter implements AnnotationProvider {
+
+ private final Map> breakpointToAnnotations = new IdentityHashMap<>();
+ private final Set annotatedFiles = new WeakSet<>();
+ private volatile boolean breakpointsActive = true;
+ private final RequestProcessor annotationProcessor = new RequestProcessor("CPP BP Annotation Refresh", 1);
+
+ public BreakpointAnnotationProvider() {
+ DebuggerManager.getDebuggerManager().addDebuggerListener(DebuggerManager.PROP_BREAKPOINTS, this);
+ }
+
+ @Override
+ public void annotate (Line.Set set, Lookup lookup) {
+ final FileObject fo = lookup.lookup(FileObject.class);
+ if (fo != null) {
+ DataObject dobj = lookup.lookup(DataObject.class);
+ if (dobj != null) {
+ PropertyChangeListener pchl = new PropertyChangeListener() {
+ /** annotate renamed files. */
+ @Override
+ public void propertyChange(PropertyChangeEvent evt) {
+ if (DataObject.PROP_PRIMARY_FILE.equals(evt.getPropertyName())) {
+ DataObject dobj = (DataObject) evt.getSource();
+ final FileObject newFO = dobj.getPrimaryFile();
+ annotationProcessor.post(new Runnable() {
+ @Override
+ public void run() {
+ annotate(newFO);
+ }
+ });
+ }
+ }
+ };
+ dobj.addPropertyChangeListener(pchl);
+ }
+ annotate(fo);
+ }
+ }
+
+ private void annotate (final FileObject fo) {
+ synchronized (breakpointToAnnotations) {
+ for (Breakpoint breakpoint : DebuggerManager.getDebuggerManager().getBreakpoints()) {
+ if (breakpoint instanceof DAPLineBreakpoint) {
+ DAPLineBreakpoint b = (DAPLineBreakpoint) breakpoint;
+ if (!b.isHidden() && isAt(b, fo)) {
+ if (!breakpointToAnnotations.containsKey(b)) {
+ b.addPropertyChangeListener(this);
+ }
+ removeAnnotations(b); // Remove any staled breakpoint annotations
+ addAnnotationTo(b);
+ }
+ }
+ }
+ annotatedFiles.add(fo);
+ }
+ }
+
+ private static boolean isAt(DAPLineBreakpoint b, FileObject fo) {
+ FileObject bfo = b.getFileObject();
+ return fo.equals(bfo);
+ }
+
+ @Override
+ public void breakpointAdded(Breakpoint breakpoint) {
+ if (breakpoint instanceof DAPLineBreakpoint && !((DAPLineBreakpoint) breakpoint).isHidden()) {
+ postAnnotationRefresh((DAPLineBreakpoint) breakpoint, false, true);
+ breakpoint.addPropertyChangeListener (this);
+ }
+ }
+
+ @Override
+ public void breakpointRemoved(Breakpoint breakpoint) {
+ if (breakpoint instanceof DAPLineBreakpoint && !((DAPLineBreakpoint) breakpoint).isHidden()) {
+ breakpoint.removePropertyChangeListener (this);
+ postAnnotationRefresh((DAPLineBreakpoint) breakpoint, true, false);
+ }
+ }
+
+ @Override
+ public void propertyChange(PropertyChangeEvent evt) {
+ Object source = evt.getSource();
+ if (source instanceof DAPLineBreakpoint) {
+ String propertyName = evt.getPropertyName ();
+ switch (propertyName) {
+ case Breakpoint.PROP_ENABLED:
+ case Breakpoint.PROP_VALIDITY:
+ case DAPLineBreakpoint.PROP_CONDITION:
+ postAnnotationRefresh((DAPLineBreakpoint) source, true, true);
+ }
+ }
+ }
+
+ void setBreakpointsActive(boolean active) {
+ if (breakpointsActive == active) {
+ return ;
+ }
+ breakpointsActive = active;
+ annotationProcessor.post(new AnnotationRefresh(null, true, true));
+ }
+
+ private void postAnnotationRefresh(DAPLineBreakpoint b, boolean remove, boolean add) {
+ annotationProcessor.post(new AnnotationRefresh(b, remove, add));
+ }
+
+ private final class AnnotationRefresh implements Runnable {
+
+ private final DAPLineBreakpoint b;
+ private final boolean remove;
+ private final boolean add;
+
+ public AnnotationRefresh(DAPLineBreakpoint b, boolean remove, boolean add) {
+ this.b = b;
+ this.remove = remove;
+ this.add = add;
+ }
+
+ @Override
+ public void run() {
+ synchronized (breakpointToAnnotations) {
+ if (b != null) {
+ refreshAnnotation(b);
+ } else {
+ List bpts = new ArrayList<>(breakpointToAnnotations.keySet());
+ for (DAPLineBreakpoint bp : bpts) {
+ refreshAnnotation(bp);
+ }
+ }
+ }
+ }
+
+ private void refreshAnnotation(DAPLineBreakpoint b) {
+ assert Thread.holdsLock(breakpointToAnnotations);
+ removeAnnotations(b);
+ if (remove) {
+ if (!add) {
+ breakpointToAnnotations.remove(b);
+ }
+ }
+ if (add) {
+ breakpointToAnnotations.put(b, new WeakSet<>());
+ for (FileObject fo : annotatedFiles) {
+ if (isAt(b, fo)) {
+ addAnnotationTo(b);
+ }
+ }
+ }
+ }
+
+ }
+
+ private static String getAnnotationType(DAPLineBreakpoint b, boolean isConditional,
+ boolean active) {
+ boolean isInvalid = b.getValidity() == VALIDITY.INVALID;
+ String annotationType = b.isEnabled() ?
+ (isConditional ? DebuggerBreakpointAnnotation.CONDITIONAL_BREAKPOINT_ANNOTATION_TYPE :
+ DebuggerBreakpointAnnotation.BREAKPOINT_ANNOTATION_TYPE) :
+ (isConditional ? DebuggerBreakpointAnnotation.DISABLED_CONDITIONAL_BREAKPOINT_ANNOTATION_TYPE :
+ DebuggerBreakpointAnnotation.DISABLED_BREAKPOINT_ANNOTATION_TYPE);
+ if (!active) {
+ annotationType += "_stroke"; // NOI18N
+ } else if (isInvalid && b.isEnabled ()) {
+ annotationType += "_broken"; // NOI18N
+ }
+ return annotationType;
+ }
+
+ private void addAnnotationTo(DAPLineBreakpoint b) {
+ assert Thread.holdsLock(breakpointToAnnotations);
+ String condition = getCondition(b);
+ boolean isConditional = condition.trim().length() > 0 || b.getHitCountFilteringStyle() != null;
+ String annotationType = getAnnotationType(b, isConditional, breakpointsActive);
+ DebuggerBreakpointAnnotation annotation = DebuggerBreakpointAnnotation.create(annotationType, b);
+ if (annotation == null) {
+ return ;
+ }
+ Set bpAnnotations = breakpointToAnnotations.get(b);
+ if (bpAnnotations == null) {
+ Set set = new WeakSet<>();
+ set.add(annotation);
+ breakpointToAnnotations.put(b, set);
+ } else {
+ bpAnnotations.add(annotation);
+ breakpointToAnnotations.put(b, bpAnnotations);
+ }
+ }
+
+ private void removeAnnotations(DAPLineBreakpoint b) {
+ assert Thread.holdsLock(breakpointToAnnotations);
+ Set annotations = breakpointToAnnotations.remove(b);
+ if (annotations == null) {
+ return ;
+ }
+ for (Annotation a : annotations) {
+ a.detach();
+ }
+ }
+
+ /**
+ * Gets the condition of a breakpoint.
+ * @param b The breakpoint
+ * @return The condition or empty {@link String} if no condition is supported.
+ */
+ static String getCondition(Breakpoint b) {
+ if (b instanceof DAPLineBreakpoint) {
+ return ""; // TODO
+ } else {
+ throw new IllegalStateException(b.toString());
+ }
+ }
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/BreakpointModel.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/BreakpointModel.java
new file mode 100644
index 000000000000..f82c95cf5edf
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/BreakpointModel.java
@@ -0,0 +1,154 @@
+/*
+ * 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.lsp.client.debugger.breakpoints;
+
+import java.io.File;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import org.netbeans.api.debugger.DebuggerEngine;
+import org.netbeans.api.debugger.DebuggerManager;
+import org.netbeans.modules.lsp.client.debugger.DAPDebugger;
+import org.netbeans.modules.lsp.client.debugger.DAPStackTraceAnnotationHolder;
+
+import org.netbeans.spi.debugger.DebuggerServiceRegistration;
+import org.netbeans.spi.viewmodel.ModelEvent;
+import org.netbeans.spi.viewmodel.NodeModel;
+import org.netbeans.spi.viewmodel.ModelListener;
+import org.netbeans.spi.viewmodel.UnknownTypeException;
+import org.openide.filesystems.FileObject;
+
+@DebuggerServiceRegistration(path="BreakpointsView", types={NodeModel.class})
+public final class BreakpointModel implements NodeModel {
+
+ public static final String LINE_BREAKPOINT =
+ "org/netbeans/modules/debugger/resources/breakpointsView/Breakpoint";
+ public static final String LINE_BREAKPOINT_PC =
+ "org/netbeans/modules/debugger/resources/breakpointsView/BreakpointHit";
+ public static final String DISABLED_LINE_BREAKPOINT =
+ "org/netbeans/modules/debugger/resources/breakpointsView/DisabledBreakpoint";
+
+ private List listeners = new CopyOnWriteArrayList<>();
+
+
+ // NodeModel implementation ................................................
+
+ /**
+ * Returns display name for given node.
+ *
+ * @throws ComputingException if the display name resolving process
+ * is time consuming, and the value will be updated later
+ * @throws UnknownTypeException if this NodeModel implementation is not
+ * able to resolve display name for given node type
+ * @return display name for given node
+ */
+ @Override
+ public String getDisplayName (Object node) throws UnknownTypeException {
+ if (node instanceof DAPLineBreakpoint) {
+ DAPLineBreakpoint breakpoint = (DAPLineBreakpoint) node;
+ String nameExt;
+ FileObject fileObject = breakpoint.getFileObject();
+ nameExt = fileObject.getNameExt();
+ return nameExt + ":" + breakpoint.getLineNumber();
+ }
+ throw new UnknownTypeException (node);
+ }
+
+ /**
+ * Returns icon for given node.
+ *
+ * @throws ComputingException if the icon resolving process
+ * is time consuming, and the value will be updated later
+ * @throws UnknownTypeException if this NodeModel implementation is not
+ * able to resolve icon for given node type
+ * @return icon for given node
+ */
+ @Override
+ public String getIconBase (Object node) throws UnknownTypeException {
+ if (node instanceof DAPLineBreakpoint) {
+ DAPLineBreakpoint breakpoint = (DAPLineBreakpoint) node;
+ if (!((DAPLineBreakpoint) node).isEnabled ())
+ return DISABLED_LINE_BREAKPOINT;
+ DAPDebugger debugger = getDebugger ();
+ if ( debugger != null &&
+ DAPStackTraceAnnotationHolder.contains (
+ debugger.getCurrentLine (),
+ breakpoint.getLine ()
+ )
+ )
+ return LINE_BREAKPOINT_PC;
+ return LINE_BREAKPOINT;
+ }
+ throw new UnknownTypeException (node);
+ }
+
+ /**
+ * Returns tooltip for given node.
+ *
+ * @throws ComputingException if the tooltip resolving process
+ * is time consuming, and the value will be updated later
+ * @throws UnknownTypeException if this NodeModel implementation is not
+ * able to resolve tooltip for given node type
+ * @return tooltip for given node
+ */
+ @Override
+ public String getShortDescription (Object node)
+ throws UnknownTypeException {
+ if (node instanceof DAPLineBreakpoint) {
+ DAPLineBreakpoint breakpoint = (DAPLineBreakpoint) node;
+ return breakpoint.getFileObject().getPath() + ":" + breakpoint.getLineNumber();
+ }
+ throw new UnknownTypeException (node);
+ }
+
+ /**
+ * Registers given listener.
+ *
+ * @param l the listener to add
+ */
+ @Override
+ public void addModelListener (ModelListener l) {
+ listeners.add (l);
+ }
+
+ /**
+ * Unregisters given listener.
+ *
+ * @param l the listener to remove
+ */
+ @Override
+ public void removeModelListener (ModelListener l) {
+ listeners.remove (l);
+ }
+
+
+ public void fireChanges () {
+ ModelEvent event = new ModelEvent.TreeChanged(this);
+ for (ModelListener l : listeners) {
+ l.modelChanged(event);
+ }
+ }
+
+ private static DAPDebugger getDebugger () {
+ DebuggerEngine engine = DebuggerManager.getDebuggerManager ().
+ getCurrentEngine ();
+ if (engine == null) return null;
+ return engine.lookupFirst(null, DAPDebugger.class);
+ }
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/BreakpointsReader.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/BreakpointsReader.java
new file mode 100644
index 000000000000..5d8de65f0275
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/BreakpointsReader.java
@@ -0,0 +1,106 @@
+/*
+ * 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.lsp.client.debugger.breakpoints;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import org.netbeans.api.debugger.Breakpoint;
+import org.netbeans.api.debugger.Properties;
+import org.netbeans.spi.debugger.DebuggerServiceRegistration;
+import org.openide.filesystems.FileObject;
+import org.openide.filesystems.URLMapper;
+
+@DebuggerServiceRegistration(types={Properties.Reader.class})
+public final class BreakpointsReader implements Properties.Reader {
+
+ @Override
+ public String [] getSupportedClassNames () {
+ return new String[] {
+ DAPLineBreakpoint.class.getName (),
+ };
+ }
+
+ @Override
+ public Object read (String typeID, Properties properties) {
+ if (!(typeID.equals (DAPLineBreakpoint.class.getName ())))
+ return null;
+
+ DAPLineBreakpoint b;
+ int lineNumber = properties.getInt("lineNumber", 0) + 1;
+ String url = properties.getString ("url", null);
+ FileObject fo;
+ try {
+ fo = URLMapper.findFileObject(new URL(url));
+ } catch (MalformedURLException ex) {
+ fo = null;
+ }
+ if (fo == null) {
+ // The user file is gone
+ return null;
+ }
+ b = DAPLineBreakpoint.create(fo, lineNumber);
+ b.setGroupName(
+ properties.getString (Breakpoint.PROP_GROUP_NAME, "")
+ );
+ int hitCountFilter = properties.getInt(Breakpoint.PROP_HIT_COUNT_FILTER, 0);
+ Breakpoint.HIT_COUNT_FILTERING_STYLE hitCountFilteringStyle;
+ if (hitCountFilter > 0) {
+ hitCountFilteringStyle = Breakpoint.HIT_COUNT_FILTERING_STYLE.values()
+ [properties.getInt(Breakpoint.PROP_HIT_COUNT_FILTER+"_style", 0)]; // NOI18N
+ } else {
+ hitCountFilteringStyle = null;
+ }
+ b.setHitCountFilter(hitCountFilter, hitCountFilteringStyle);
+ String condition = properties.getString(DAPLineBreakpoint.PROP_CONDITION, null);
+ if (condition != null && !condition.isEmpty()) {
+ b.setCondition(condition);
+ }
+ if (properties.getBoolean (Breakpoint.PROP_ENABLED, true))
+ b.enable ();
+ else
+ b.disable ();
+ return b;
+ }
+
+ @Override
+ public void write (Object object, Properties properties) {
+ DAPLineBreakpoint b = (DAPLineBreakpoint) object;
+ FileObject fo = b.getFileObject();
+ properties.setString("url", fo.toURL().toString());
+ properties.setInt (
+ "lineNumber",
+ b.getLineNumber() - 1
+ );
+ properties.setString (
+ Breakpoint.PROP_GROUP_NAME,
+ b.getGroupName ()
+ );
+ properties.setBoolean (Breakpoint.PROP_ENABLED, b.isEnabled ());
+ properties.setInt(Breakpoint.PROP_HIT_COUNT_FILTER, b.getHitCountFilter());
+ Breakpoint.HIT_COUNT_FILTERING_STYLE style = b.getHitCountFilteringStyle();
+ properties.setInt(Breakpoint.PROP_HIT_COUNT_FILTER+"_style", style != null ? style.ordinal() : 0); // NOI18N
+ String condition = b.getCondition();
+ if (condition == null) {
+ condition = "";
+ }
+ properties.setString(DAPLineBreakpoint.PROP_CONDITION, condition);
+ }
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/Bundle.properties b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/Bundle.properties
new file mode 100644
index 000000000000..438f33daaefa
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/Bundle.properties
@@ -0,0 +1,18 @@
+# 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.
+
+LineBrkp_Type=Line
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/DAPBreakpointActionProvider.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/DAPBreakpointActionProvider.java
new file mode 100644
index 000000000000..2f0af51b3b5a
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/DAPBreakpointActionProvider.java
@@ -0,0 +1,144 @@
+/*
+ * 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.lsp.client.debugger.breakpoints;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.netbeans.api.debugger.ActionsManager;
+import org.netbeans.api.debugger.Breakpoint;
+import org.netbeans.api.debugger.DebuggerManager;
+import org.netbeans.api.editor.mimelookup.MimeLookup;
+import org.netbeans.modules.lsp.client.debugger.api.RegisterDAPBreakpoints;
+import org.netbeans.spi.debugger.ActionsProvider.Registration;
+import org.netbeans.spi.debugger.ActionsProviderSupport;
+import org.netbeans.spi.debugger.ui.EditorContextDispatcher;
+import org.openide.filesystems.FileObject;
+import org.openide.filesystems.FileUtil;
+import org.openide.text.Line;
+import org.openide.util.Lookup.Result;
+import org.openide.util.WeakListeners;
+
+@Registration(actions={"toggleBreakpoint"})
+public final class DAPBreakpointActionProvider
+extends ActionsProviderSupport implements PropertyChangeListener {
+
+ private static final Set ACTIONS = Collections.singleton (
+ ActionsManager.ACTION_TOGGLE_BREAKPOINT
+ );
+
+ private record BreakpointInfo(boolean dapBreakpointsAllowed,
+ Result registerLookup) {}
+ private static final Map mimeType2BreakpointInfo = new HashMap<>();
+
+ private static boolean hasMimeTypeDAPBreakpoints(String mimeType) {
+ synchronized (mimeType2BreakpointInfo) {
+ return mimeType2BreakpointInfo.computeIfAbsent(mimeType, mt -> {
+ Result result = MimeLookup.getLookup(mimeType).lookupResult(RegisterDAPBreakpoints.class);
+ result.addLookupListener(evt -> {
+ synchronized (mimeType2BreakpointInfo) {
+ mimeType2BreakpointInfo.put(mimeType, new BreakpointInfo(!result.allInstances().isEmpty(), result));
+ }
+ });
+ return new BreakpointInfo(!result.allInstances().isEmpty(), result);
+ }).dapBreakpointsAllowed();
+ }
+ }
+
+ private EditorContextDispatcher context = EditorContextDispatcher.getDefault();
+
+ public DAPBreakpointActionProvider () {
+ context.addPropertyChangeListener(
+ WeakListeners.propertyChange(this, context));
+ setEnabled (ActionsManager.ACTION_TOGGLE_BREAKPOINT, false);
+ }
+
+ /**
+ * Called when the action is called (action button is pressed).
+ *
+ * @param action an action which has been called
+ */
+ @Override
+ public void doAction (Object action) {
+ Line line = getCurrentLine ();
+ if (line == null) {
+ return ;
+ }
+ Breakpoint[] breakpoints = DebuggerManager.getDebuggerManager().getBreakpoints ();
+ FileObject fo = line.getLookup().lookup(FileObject.class);
+ if (fo == null) {
+ return ;
+ }
+ int lineNumber = line.getLineNumber() + 1;
+ int i, k = breakpoints.length;
+ for (i = 0; i < k; i++) {
+ if (breakpoints[i] instanceof DAPLineBreakpoint lb) {
+ if (fo.equals(lb.getFileObject()) && lb.getLineNumber() == lineNumber) {
+ DebuggerManager.getDebuggerManager().removeBreakpoint(lb);
+ break;
+ }
+ }
+ }
+ if (i == k) {
+ DebuggerManager.getDebuggerManager ().addBreakpoint (
+ DAPLineBreakpoint.create(line)
+ );
+ }
+ }
+
+ /**
+ * Returns set of actions supported by this ActionsProvider.
+ *
+ * @return set of actions supported by this ActionsProvider
+ */
+ @Override
+ public Set getActions () {
+ return ACTIONS;
+ }
+
+ private static Line getCurrentLine () {
+ FileObject fo = EditorContextDispatcher.getDefault().getCurrentFile();
+ //System.out.println("n = "+n+", FO = "+fo+" => is ANT = "+isAntFile(fo));
+ if (!isRelevantFile(fo)) {
+ return null;
+ }
+ return EditorContextDispatcher.getDefault().getCurrentLine();
+ }
+
+ private static boolean isRelevantFile(FileObject fo) {
+ if (fo == null) {
+ return false;
+ } else {
+ return hasMimeTypeDAPBreakpoints(FileUtil.getMIMEType(fo));
+ }
+ }
+
+ @Override
+ public void propertyChange(PropertyChangeEvent evt) {
+ // We need to push the state there :-(( instead of wait for someone to be interested in...
+ boolean enabled = getCurrentLine() != null;
+ setEnabled (ActionsManager.ACTION_TOGGLE_BREAKPOINT, enabled);
+ }
+
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/DAPBreakpointConvertor.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/DAPBreakpointConvertor.java
new file mode 100644
index 000000000000..f553252b47ac
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/DAPBreakpointConvertor.java
@@ -0,0 +1,35 @@
+/*
+ * 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.lsp.client.debugger.breakpoints;
+
+import org.netbeans.api.debugger.Breakpoint;
+import org.netbeans.modules.lsp.client.debugger.spi.BreakpointConvertor;
+import org.openide.util.lookup.ServiceProvider;
+
+@ServiceProvider(service=BreakpointConvertor.class)
+public class DAPBreakpointConvertor implements BreakpointConvertor {
+
+ @Override
+ public void convert(Breakpoint b, ConvertedBreakpointConsumer breakpointConsumer) {
+ if (b instanceof DAPLineBreakpoint lb) {
+ breakpointConsumer.lineBreakpoint(lb.getFileObject().toURI(), lb.getLineNumber(), lb.getCondition());
+ }
+ }
+
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/DAPLineBreakpoint.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/DAPLineBreakpoint.java
new file mode 100644
index 000000000000..9339658cfbf4
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/DAPLineBreakpoint.java
@@ -0,0 +1,304 @@
+/*
+ * 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.lsp.client.debugger.breakpoints;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.netbeans.api.annotations.common.CheckForNull;
+import org.netbeans.api.annotations.common.NonNull;
+import org.netbeans.api.debugger.Breakpoint;
+import org.netbeans.api.debugger.DebuggerEngine;
+import org.netbeans.api.debugger.DebuggerManager;
+import org.netbeans.api.debugger.DebuggerManagerAdapter;
+import org.netbeans.api.debugger.DebuggerManagerListener;
+import org.netbeans.api.project.Project;
+import org.netbeans.api.project.ProjectManager;
+import org.netbeans.modules.lsp.client.debugger.DAPDebugger;
+import org.openide.cookies.LineCookie;
+import org.openide.filesystems.FileObject;
+import org.openide.filesystems.FileUtil;
+import org.openide.loaders.DataObject;
+import org.openide.loaders.DataObjectNotFoundException;
+import org.openide.text.Line;
+import org.openide.util.NbBundle;
+import org.openide.util.WeakListeners;
+
+
+
+public final class DAPLineBreakpoint extends Breakpoint {
+
+ public static final String PROP_CONDITION = "condition"; // NOI18N
+ public static final String PROP_HIDDEN = "hidden"; // NOI18N
+
+ private final AtomicBoolean enabled = new AtomicBoolean(true);
+ private final AtomicBoolean hidden = new AtomicBoolean(false);
+ private final FileObject fileObject; // The user file that contains the breakpoint
+ private final int lineNumber; // The breakpoint line number
+ private volatile String condition;
+
+ private DAPLineBreakpoint (FileObject fileObject, String filePath, int lineNumber) {
+ this.fileObject = fileObject;
+ this.lineNumber = lineNumber;
+ }
+
+ public static DAPLineBreakpoint create(Line line) {
+ int lineNumber = line.getLineNumber() + 1;
+ FileObject fileObject = line.getLookup().lookup(FileObject.class);
+ return create(fileObject, lineNumber);
+ }
+
+ /**
+ * Create a new DAP breakpoint based on a user file.
+ * @param fileObject the file path of the breakpoint
+ * @param lineNumber 1-based line number
+ * @return a new breakpoint.
+ */
+ public static DAPLineBreakpoint create(FileObject fileObject, int lineNumber) {
+ String filePath = FileUtil.toFile(fileObject).getAbsolutePath();
+ return new DAPLineBreakpoint(fileObject, filePath, lineNumber);
+ }
+
+ /**
+ * 1-based line number.
+ */
+ public int getLineNumber() {
+ return lineNumber;
+ }
+
+ @NonNull
+ public FileObject getFileObject() {
+ return fileObject;
+ }
+
+ @CheckForNull
+ public Line getLine() {
+ FileObject fo = fileObject;
+ if (fo == null) {
+ return null;
+ }
+ DataObject dataObject;
+ try {
+ dataObject = DataObject.find(fo);
+ } catch (DataObjectNotFoundException ex) {
+ return null;
+ }
+ LineCookie lineCookie = dataObject.getLookup().lookup(LineCookie.class);
+ if (lineCookie != null) {
+ Line.Set ls = lineCookie.getLineSet ();
+ if (ls != null) {
+ try {
+ return ls.getCurrent(lineNumber - 1);
+ } catch (IndexOutOfBoundsException | IllegalArgumentException e) {
+ }
+ }
+ }
+ return null;
+ }
+ /**
+ * Test whether the breakpoint is enabled.
+ *
+ * @return true if so
+ */
+ @Override
+ public boolean isEnabled () {
+ return enabled.get();
+ }
+
+ /**
+ * Disables the breakpoint.
+ */
+ @Override
+ public void disable () {
+ if (enabled.compareAndSet(true, false)) {
+ firePropertyChange (PROP_ENABLED, Boolean.TRUE, Boolean.FALSE);
+ }
+ }
+
+ /**
+ * Enables the breakpoint.
+ */
+ @Override
+ public void enable () {
+ if (enabled.compareAndSet(false, true)) {
+ firePropertyChange (PROP_ENABLED, Boolean.FALSE, Boolean.TRUE);
+ }
+ }
+
+ /**
+ * Get the breakpoint condition, or null.
+ */
+ public String getCondition() {
+ return condition;
+ }
+
+ /**
+ * Set the breakpoint condition.
+ */
+ public void setCondition(String condition) {
+ String oldCondition;
+ synchronized (this) {
+ oldCondition = this.condition;
+ if (Objects.equals(oldCondition, condition)) {
+ return ;
+ }
+ this.condition = condition;
+ }
+ firePropertyChange (PROP_CONDITION, oldCondition, condition);
+ }
+
+ /**
+ * Gets value of hidden property.
+ *
+ * @return value of hidden property
+ */
+ public boolean isHidden() {
+ return hidden.get();
+ }
+
+ /**
+ * Sets value of hidden property.
+ *
+ * @param h a new value of hidden property
+ */
+ public void setHidden(boolean h) {
+ boolean old = hidden.getAndSet(h);
+ if (old != h) {
+ firePropertyChange(PROP_HIDDEN, old, h);
+ }
+ }
+
+ @Override
+ public GroupProperties getGroupProperties() {
+ return new CPPGroupProperties();
+ }
+
+ private final class CPPGroupProperties extends GroupProperties {
+
+ private CPPEngineListener engineListener;
+
+ @Override
+ public String getLanguage() {
+ return "C/C++";
+ }
+
+ @Override
+ public String getType() {
+ return NbBundle.getMessage(DAPLineBreakpoint.class, "LineBrkp_Type");
+ }
+
+ private FileObject getFile() {
+ return getFileObject();
+ }
+
+ @Override
+ public FileObject[] getFiles() {
+ return new FileObject[] { getFileObject() };
+ }
+
+ @Override
+ public Project[] getProjects() {
+ FileObject f = getFile();
+ while (f != null) {
+ f = f.getParent();
+ if (f != null && ProjectManager.getDefault().isProject(f)) {
+ break;
+ }
+ }
+ if (f != null) {
+ try {
+ return new Project[] { ProjectManager.getDefault().findProject(f) };
+ } catch (IOException ex) {
+ } catch (IllegalArgumentException ex) {
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public DebuggerEngine[] getEngines() {
+ if (engineListener == null) {
+ engineListener = new CPPEngineListener();
+ DebuggerManager.getDebuggerManager().addDebuggerListener(
+ WeakListeners.create(DebuggerManagerListener.class,
+ engineListener,
+ DebuggerManager.getDebuggerManager()));
+ }
+ DebuggerEngine[] engines = DebuggerManager.getDebuggerManager().getDebuggerEngines();
+ if (engines.length == 0) {
+ return null;
+ }
+ if (engines.length == 1) {
+ if (isDAPEngine(engines[0])) {
+ return engines;
+ } else {
+ return null;
+ }
+ }
+ // Several running sessions
+ List antEngines = null;
+ for (DebuggerEngine e : engines) {
+ if (isDAPEngine(e)) {
+ if (antEngines == null) {
+ antEngines = new ArrayList<>();
+ }
+ antEngines.add(e);
+ }
+ }
+ if (antEngines == null) {
+ return null;
+ } else {
+ return antEngines.toArray(new DebuggerEngine[0]);
+ }
+ }
+
+ private boolean isDAPEngine(DebuggerEngine e) {
+ return e.lookupFirst(null, DAPDebugger.class) != null;
+ }
+
+ @Override
+ public boolean isHidden() {
+ return false;
+ }
+
+ private final class CPPEngineListener extends DebuggerManagerAdapter {
+
+ @Override
+ public void engineAdded(DebuggerEngine engine) {
+ if (isDAPEngine(engine)) {
+ firePropertyChange(PROP_GROUP_PROPERTIES, null, CPPGroupProperties.this);
+ }
+ }
+
+ @Override
+ public void engineRemoved(DebuggerEngine engine) {
+ if (isDAPEngine(engine)) {
+ firePropertyChange(PROP_GROUP_PROPERTIES, null, CPPGroupProperties.this);
+ }
+ }
+
+ }
+
+ }
+
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/DebuggerBreakpointAnnotation.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/DebuggerBreakpointAnnotation.java
new file mode 100644
index 000000000000..90f04fe246ae
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/DebuggerBreakpointAnnotation.java
@@ -0,0 +1,157 @@
+/*
+ * 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.lsp.client.debugger.breakpoints;
+
+import java.util.LinkedList;
+import java.util.List;
+import org.netbeans.api.annotations.common.CheckForNull;
+
+import org.netbeans.api.debugger.Breakpoint;
+import org.netbeans.api.debugger.Breakpoint.HIT_COUNT_FILTERING_STYLE;
+import org.netbeans.spi.debugger.ui.BreakpointAnnotation;
+import org.openide.text.Annotatable;
+import org.openide.text.Line;
+import org.openide.util.NbBundle;
+
+
+public final class DebuggerBreakpointAnnotation extends BreakpointAnnotation {
+
+ /** Annotation type constant. */
+ public static final String BREAKPOINT_ANNOTATION_TYPE = "Breakpoint"; // NOI18N
+ /** Annotation type constant. */
+ public static final String DISABLED_BREAKPOINT_ANNOTATION_TYPE = "DisabledBreakpoint"; // NOI18N
+ /** Annotation type constant. */
+ public static final String CONDITIONAL_BREAKPOINT_ANNOTATION_TYPE = "CondBreakpoint"; // NOI18N
+ /** Annotation type constant. */
+ public static final String DISABLED_CONDITIONAL_BREAKPOINT_ANNOTATION_TYPE = "DisabledCondBreakpoint"; // NOI18N
+
+ private final String type;
+ private final DAPLineBreakpoint breakpoint;
+
+ private DebuggerBreakpointAnnotation (String type, Annotatable annotatable, DAPLineBreakpoint b) {
+ this.type = type;
+ this.breakpoint = b;
+ attach (annotatable);
+ }
+
+ @CheckForNull
+ public static DebuggerBreakpointAnnotation create(String type, DAPLineBreakpoint b) {
+ Line line = b.getLine();
+ if (line == null) {
+ return null;
+ }
+ return new DebuggerBreakpointAnnotation(type, line, b);
+ }
+
+ @Override
+ public String getAnnotationType () {
+ return type;
+ }
+
+ @Override
+ @NbBundle.Messages({"TTP_Breakpoint_Hits=Hits when:",
+ "# {0} - hit count",
+ "TTP_Breakpoint_HitsEqual=Hit count \\= {0}",
+ "# {0} - hit count",
+ "TTP_Breakpoint_HitsGreaterThan=Hit count > {0}",
+ "# {0} - hit count",
+ "TTP_Breakpoint_HitsMultipleOf=Hit count is multiple of {0}"})
+ public String getShortDescription () {
+ List list = new LinkedList<>();
+ //add condition if available
+ String condition = breakpoint.getCondition();
+ if (condition != null) {
+ list.add(condition);
+ }
+
+ // add hit count if available
+ HIT_COUNT_FILTERING_STYLE hitCountFilteringStyle = breakpoint.getHitCountFilteringStyle();
+ if (hitCountFilteringStyle != null) {
+ int hcf = breakpoint.getHitCountFilter();
+ String tooltip;
+ switch (hitCountFilteringStyle) {
+ case EQUAL:
+ tooltip = Bundle.TTP_Breakpoint_HitsEqual(hcf);
+ break;
+ case GREATER:
+ tooltip = Bundle.TTP_Breakpoint_HitsGreaterThan(hcf);
+ break;
+ case MULTIPLE:
+ tooltip = Bundle.TTP_Breakpoint_HitsMultipleOf(hcf);
+ break;
+ default:
+ throw new IllegalStateException("Unknown HitCountFilteringStyle: "+hitCountFilteringStyle); // NOI18N
+ }
+ list.add(tooltip);
+ }
+
+ String typeDesc = getBPTypeDescription();
+ if (list.isEmpty()) {
+ return typeDesc;
+ }
+ StringBuilder result = new StringBuilder(typeDesc);
+ //append more information
+ result.append("\n"); // NOI18N
+ result.append(Bundle.TTP_Breakpoint_Hits());
+ for (String text : list) {
+ result.append("\n"); // NOI18N
+ result.append(text);
+ }
+ return result.toString();
+ }
+
+ @NbBundle.Messages({"TTP_Breakpoint=Breakpoint",
+ "TTP_BreakpointDisabled=Disabled Breakpoint",
+ "TTP_BreakpointConditional=Conditional Breakpoint",
+ "TTP_BreakpointDisabledConditional=Disabled Conditional Breakpoint",
+ "TTP_BreakpointBroken=Broken breakpoint - It is not possible to stop on this line.",
+ "# {0} - Reason for being invalid",
+ "TTP_BreakpointBrokenInvalid=Broken breakpoint: {0}",
+ "TTP_BreakpointStroke=Deactivated breakpoint"})
+ private String getBPTypeDescription () {
+ if (type.endsWith("_broken")) { // NOI18N
+ if (breakpoint.getValidity() == Breakpoint.VALIDITY.INVALID) {
+ String msg = breakpoint.getValidityMessage();
+ return Bundle.TTP_BreakpointBrokenInvalid(msg);
+ }
+ return Bundle.TTP_BreakpointBroken();
+ }
+ if (type.endsWith("_stroke")) { // NOI18N
+ return Bundle.TTP_BreakpointStroke();
+ }
+ switch (type) {
+ case BREAKPOINT_ANNOTATION_TYPE:
+ return Bundle.TTP_Breakpoint();
+ case DISABLED_BREAKPOINT_ANNOTATION_TYPE:
+ return Bundle.TTP_BreakpointDisabled();
+ case CONDITIONAL_BREAKPOINT_ANNOTATION_TYPE:
+ return Bundle.TTP_BreakpointConditional();
+ case DISABLED_CONDITIONAL_BREAKPOINT_ANNOTATION_TYPE:
+ return Bundle.TTP_BreakpointDisabledConditional();
+ default:
+ throw new IllegalStateException(type);
+ }
+ }
+
+ @Override
+ public Breakpoint getBreakpoint() {
+ return breakpoint;
+ }
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/PersistenceManager.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/PersistenceManager.java
new file mode 100644
index 000000000000..13cfe8aeda37
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/PersistenceManager.java
@@ -0,0 +1,168 @@
+/*
+ * 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.lsp.client.debugger.breakpoints;
+
+import java.beans.PropertyChangeEvent;
+import java.util.List;
+import java.util.ArrayList;
+import org.netbeans.api.debugger.Breakpoint;
+import org.netbeans.api.debugger.DebuggerEngine;
+
+import org.netbeans.api.debugger.DebuggerManager;
+import org.netbeans.api.debugger.LazyDebuggerManagerListener;
+import org.netbeans.api.debugger.Properties;
+import org.netbeans.api.debugger.Session;
+import org.netbeans.api.debugger.Watch;
+import org.netbeans.spi.debugger.DebuggerServiceRegistration;
+
+/**
+ * Listens on DebuggerManager and:
+ * - loads all breakpoints & watches on startup
+ * - listens on all changes of breakpoints and watches (like breakoint / watch
+ * added / removed, or some property change) and saves a new values
+ *
+ */
+@DebuggerServiceRegistration(types={LazyDebuggerManagerListener.class})
+public final class PersistenceManager implements LazyDebuggerManagerListener {
+
+ private static final String KEY = "dap";
+
+ private boolean areBreakpointsPersisted() {
+ Properties p = Properties.getDefault ().getProperties ("debugger");
+ p = p.getProperties("persistence");
+ return p.getBoolean("breakpoints", true);
+ }
+
+ @Override
+ public Breakpoint[] initBreakpoints () {
+ if (!areBreakpointsPersisted()) {
+ return new Breakpoint[]{};
+ }
+ Properties p = Properties.getDefault ().getProperties ("debugger").
+ getProperties (DebuggerManager.PROP_BREAKPOINTS);
+ Breakpoint[] breakpoints = (Breakpoint[]) p.getArray (
+ KEY,
+ new Breakpoint [0]
+ );
+ for (int i = 0; i < breakpoints.length; i++) {
+ if (breakpoints[i] == null) {
+ Breakpoint[] b2 = new Breakpoint[breakpoints.length - 1];
+ System.arraycopy(breakpoints, 0, b2, 0, i);
+ if (i < breakpoints.length - 1) {
+ System.arraycopy(breakpoints, i + 1, b2, i, breakpoints.length - i - 1);
+ }
+ breakpoints = b2;
+ i--;
+ continue;
+ }
+ breakpoints[i].addPropertyChangeListener(this);
+ }
+ return breakpoints;
+ }
+
+ @Override
+ public void initWatches () {
+ }
+
+ @Override
+ public String[] getProperties () {
+ return new String [] {
+ DebuggerManager.PROP_BREAKPOINTS_INIT,
+ DebuggerManager.PROP_BREAKPOINTS,
+ };
+ }
+
+ @Override
+ public void breakpointAdded (Breakpoint breakpoint) {
+ if (!areBreakpointsPersisted()) {
+ return ;
+ }
+ if (breakpoint instanceof DAPLineBreakpoint) {
+ Properties p = Properties.getDefault ().getProperties ("debugger").
+ getProperties (DebuggerManager.PROP_BREAKPOINTS);
+ p.setArray (
+ KEY,
+ getBreakpoints ()
+ );
+ breakpoint.addPropertyChangeListener(this);
+ }
+ }
+
+ @Override
+ public void breakpointRemoved (Breakpoint breakpoint) {
+ if (!areBreakpointsPersisted()) {
+ return ;
+ }
+ if (breakpoint instanceof DAPLineBreakpoint) {
+ Properties p = Properties.getDefault ().getProperties ("debugger").
+ getProperties (DebuggerManager.PROP_BREAKPOINTS);
+ p.setArray (
+ KEY,
+ getBreakpoints ()
+ );
+ breakpoint.removePropertyChangeListener(this);
+ }
+ }
+ @Override
+ public void watchAdded (Watch watch) {
+ }
+
+ @Override
+ public void watchRemoved (Watch watch) {
+ }
+
+ @Override
+ public void propertyChange (PropertyChangeEvent evt) {
+ if (evt.getSource() instanceof Breakpoint) {
+ Properties.getDefault ().getProperties ("debugger").
+ getProperties (DebuggerManager.PROP_BREAKPOINTS).setArray (
+ KEY,
+ getBreakpoints ()
+ );
+ }
+ }
+
+ @Override
+ public void sessionAdded (Session session) {}
+ @Override
+ public void sessionRemoved (Session session) {}
+ @Override
+ public void engineAdded (DebuggerEngine engine) {}
+ @Override
+ public void engineRemoved (DebuggerEngine engine) {}
+
+
+ private static Breakpoint[] getBreakpoints () {
+ Breakpoint[] bs = DebuggerManager.getDebuggerManager ().
+ getBreakpoints ();
+ List bb = new ArrayList<>();
+ for (Breakpoint b : bs) {
+ if (b instanceof DAPLineBreakpoint) {
+ // Don't store hidden breakpoints
+ if (!((DAPLineBreakpoint) b).isHidden()) {
+ bb.add(b);
+ }
+ }
+ }
+ bs = new Breakpoint [bb.size ()];
+ return bb.toArray(bs);
+ }
+}
+
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/debuggingview/DebuggingActionsProvider.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/debuggingview/DebuggingActionsProvider.java
new file mode 100644
index 000000000000..cc6c104fcfbd
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/debuggingview/DebuggingActionsProvider.java
@@ -0,0 +1,222 @@
+/*
+ * 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.lsp.client.debugger.debuggingview;
+
+import javax.swing.Action;
+import org.netbeans.modules.lsp.client.debugger.DAPDebugger;
+import org.netbeans.modules.lsp.client.debugger.DAPFrame;
+import org.netbeans.modules.lsp.client.debugger.DAPThread;
+import org.netbeans.modules.lsp.client.debugger.DAPStackTraceAnnotationHolder;
+
+import org.netbeans.spi.debugger.ContextProvider;
+import org.netbeans.spi.debugger.DebuggerServiceRegistration;
+import org.netbeans.spi.viewmodel.ModelListener;
+import org.netbeans.spi.viewmodel.Models;
+import org.netbeans.spi.viewmodel.NodeActionsProvider;
+import org.netbeans.spi.viewmodel.TreeModel;
+import org.netbeans.spi.viewmodel.UnknownTypeException;
+import org.openide.text.Line;
+import org.openide.util.NbBundle;
+import org.openide.util.RequestProcessor;
+
+
+@DebuggerServiceRegistration(path="DAPDebuggerSession/DebuggingView",
+ types=NodeActionsProvider.class)
+public class DebuggingActionsProvider implements NodeActionsProvider {
+
+ private final DAPDebugger debugger;
+ private final RequestProcessor requestProcessor = new RequestProcessor("Debugging View Actions", 1); // NOI18N
+ private final Action MAKE_CURRENT_ACTION;
+ private final Action GO_TO_SOURCE_ACTION;
+
+
+ public DebuggingActionsProvider (ContextProvider lookupProvider) {
+ debugger = lookupProvider.lookupFirst(null, DAPDebugger.class);
+ MAKE_CURRENT_ACTION = createMAKE_CURRENT_ACTION(requestProcessor);
+// SUSPEND_ACTION = createSUSPEND_ACTION(requestProcessor);
+// RESUME_ACTION = createRESUME_ACTION(requestProcessor);
+ GO_TO_SOURCE_ACTION = createGO_TO_SOURCE_ACTION(requestProcessor);
+ }
+
+ @NbBundle.Messages("CTL_ThreadAction_MakeCurrent_Label=Make Current")
+ private Action createMAKE_CURRENT_ACTION(RequestProcessor requestProcessor) {
+ return Models.createAction (
+ Bundle.CTL_ThreadAction_MakeCurrent_Label(),
+ new LazyActionPerformer (requestProcessor) {
+ @Override
+ public boolean isEnabled (Object node) {
+ if (node instanceof DAPThread) {
+ return debugger.getCurrentThread () != node;
+ }
+ if (node instanceof DAPFrame) {
+ DAPFrame frame = (DAPFrame) node;
+ return !frame.equals(debugger.getCurrentFrame());
+ }
+ return false;
+ }
+
+ @Override
+ public void run (Object[] nodes) {
+ if (nodes.length == 0) return ;
+ if (nodes[0] instanceof DAPThread) {
+ DAPThread thread = (DAPThread) nodes[0];
+ thread.makeCurrent ();
+ goToSource(thread);
+ }
+ if (nodes[0] instanceof DAPFrame) {
+ DAPFrame frame = (DAPFrame) nodes[0];
+ frame.makeCurrent ();
+ goToSource(frame);
+ }
+ }
+ },
+ Models.MULTISELECTION_TYPE_EXACTLY_ONE
+ );
+ }
+
+ @NbBundle.Messages("CTL_ThreadAction_GoToSource_Label=Go to Source")
+ static final Action createGO_TO_SOURCE_ACTION(final RequestProcessor requestProcessor) {
+ return Models.createAction (
+ Bundle.CTL_ThreadAction_GoToSource_Label(),
+ new Models.ActionPerformer () {
+ @Override
+ public boolean isEnabled (Object node) {
+ if (!(node instanceof DAPFrame)) {
+ return false;
+ }
+ return isGoToSourceSupported ((DAPFrame) node);
+ }
+
+ @Override
+ public void perform (final Object[] nodes) {
+ // Do not do expensive actions in AWT,
+ // It can also block if it can not procceed for some reason
+ requestProcessor.post(new Runnable() {
+ @Override
+ public void run() {
+ goToSource((DAPFrame) nodes [0]);
+ }
+ });
+ }
+ },
+ Models.MULTISELECTION_TYPE_EXACTLY_ONE
+
+ );
+ }
+
+ private abstract static class LazyActionPerformer implements Models.ActionPerformer {
+
+ private RequestProcessor rp;
+
+ public LazyActionPerformer(RequestProcessor rp) {
+ this.rp = rp;
+ }
+
+ @Override
+ public abstract boolean isEnabled (Object node);
+
+ @Override
+ public final void perform (final Object[] nodes) {
+ rp.post(new Runnable() {
+ @Override
+ public void run() {
+ LazyActionPerformer.this.run(nodes);
+ }
+ });
+ }
+
+ public abstract void run(Object[] nodes);
+ }
+
+ @Override
+ public Action[] getActions (Object node) throws UnknownTypeException {
+ if (node instanceof DAPThread) {
+ DAPThread thread = (DAPThread) node;
+ boolean suspended = thread.isSuspended ();
+ return new Action [] {
+ MAKE_CURRENT_ACTION,
+ };
+ } else if (node instanceof DAPFrame) {
+ return new Action [] {
+ MAKE_CURRENT_ACTION,
+ GO_TO_SOURCE_ACTION,
+ };
+ } else {
+ throw new UnknownTypeException (node);
+ }
+ }
+
+ @Override
+ public void performDefaultAction (final Object node) throws UnknownTypeException {
+ if (node == TreeModel.ROOT) {
+ return;
+ }
+ if (node instanceof DAPThread || node instanceof DAPFrame) {
+ requestProcessor.post(new Runnable() {
+ @Override
+ public void run() {
+ if (node instanceof DAPThread) {
+ ((DAPThread) node).makeCurrent ();
+ } else if (node instanceof DAPFrame) {
+ DAPFrame frame = (DAPFrame) node;
+ frame.makeCurrent();
+ goToSource(frame);
+ }
+ }
+ });
+ return ;
+ }
+ throw new UnknownTypeException (node);
+ }
+
+ /**
+ *
+ * @param l the listener to add
+ */
+ public void addModelListener (ModelListener l) {
+ }
+
+ /**
+ *
+ * @param l the listener to remove
+ */
+ public void removeModelListener (ModelListener l) {
+ }
+
+ private static boolean isGoToSourceSupported (DAPFrame frame) {
+ Line currentLine = frame.location();
+ return currentLine != null;
+ }
+
+ private static void goToSource(final DAPFrame frame) {
+ Line currentLine = frame.location();
+ if (currentLine != null) {
+ DAPStackTraceAnnotationHolder.showLine(new Line[] {currentLine});
+ }
+ }
+
+ private static void goToSource(final DAPThread thread) {
+ DAPFrame topFrame = thread.getCurrentFrame();
+ if (topFrame != null) {
+ goToSource(topFrame);
+ }
+ }
+
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/debuggingview/DebuggingModel.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/debuggingview/DebuggingModel.java
new file mode 100644
index 000000000000..d500fd754cc9
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/debuggingview/DebuggingModel.java
@@ -0,0 +1,380 @@
+/*
+ * 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.lsp.client.debugger.debuggingview;
+
+import java.awt.datatransfer.Transferable;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.io.IOException;
+import java.lang.ref.Reference;
+import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.WeakHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import org.netbeans.modules.lsp.client.debugger.DAPDebugger;
+import org.netbeans.modules.lsp.client.debugger.DAPFrame;
+import org.netbeans.modules.lsp.client.debugger.DAPThread;
+
+import org.netbeans.spi.debugger.ContextProvider;
+import org.netbeans.spi.debugger.DebuggerServiceRegistration;
+import org.netbeans.spi.debugger.ui.DebuggingView.DVThread;
+import org.netbeans.spi.viewmodel.CachedChildrenTreeModel;
+import org.netbeans.spi.viewmodel.ExtendedNodeModel;
+import org.netbeans.spi.viewmodel.ModelEvent;
+import org.netbeans.spi.viewmodel.ModelListener;
+import org.netbeans.spi.viewmodel.TableModel;
+import org.netbeans.spi.viewmodel.TreeExpansionModel;
+import org.netbeans.spi.viewmodel.TreeExpansionModelFilter;
+import org.netbeans.spi.viewmodel.TreeModel;
+import org.netbeans.spi.viewmodel.UnknownTypeException;
+
+import org.openide.util.RequestProcessor;
+import org.openide.util.WeakListeners;
+import org.openide.util.WeakSet;
+import org.openide.util.datatransfer.PasteType;
+
+@DebuggerServiceRegistration(path="DAPDebuggerSession/DebuggingView",
+ types={TreeModel.class, ExtendedNodeModel.class, TableModel.class, TreeExpansionModelFilter.class})
+public class DebuggingModel extends CachedChildrenTreeModel implements ExtendedNodeModel, TableModel, TreeExpansionModelFilter, ChangeListener {
+
+ private static final String RUNNING_THREAD_ICON =
+ "org/netbeans/modules/debugger/resources/threadsView/thread_running_16.png"; // NOI18N
+ private static final String SUSPENDED_THREAD_ICON =
+ "org/netbeans/modules/debugger/resources/threadsView/thread_suspended_16.png"; // NOI18N
+ private static final String CALL_STACK_ICON =
+ "org/netbeans/modules/debugger/resources/callStackView/NonCurrentFrame.gif"; // NOI18N
+ private static final String CURRENT_CALL_STACK_ICON =
+ "org/netbeans/modules/debugger/resources/callStackView/CurrentFrame.gif"; // NOI18N
+
+ private final DAPDebugger debugger;
+ private final List listeners = new CopyOnWriteArrayList<>();
+ private final Map threadStateListeners = new WeakHashMap<>();
+ private final Reference lastCurrentThreadRef = new WeakReference<>(null);
+ private final Reference lastCurrentFrameRef = new WeakReference<>(null);
+ private final Set expandedExplicitly = new WeakSet();
+ private final Set collapsedExplicitly = new WeakSet();
+ private final RequestProcessor RP = new RequestProcessor("Debugging Tree View Refresh", 1); // NOI18N
+
+ public DebuggingModel(ContextProvider contextProvider) {
+ debugger = contextProvider.lookupFirst(null, DAPDebugger.class);
+ debugger.addChangeListener(WeakListeners.change(this, debugger));
+ }
+
+ @Override
+ public Object getRoot() {
+ return TreeModel.ROOT;
+ }
+
+ @Override
+ public boolean isLeaf(Object node) throws UnknownTypeException {
+ if (node instanceof DAPFrame) {
+ return true;
+ }
+ if (node instanceof DAPThread) {
+ DAPThread thread = (DAPThread) node;
+ return !thread.isSuspended();
+ }
+ return false;
+ }
+
+ @Override
+ protected Object[] computeChildren(Object parent) throws UnknownTypeException {
+ if (parent == ROOT) {
+ DAPThread[] threads = debugger.getThreads();
+ for (DAPThread t : threads) {
+ watchState(t);
+ }
+ return threads;
+ }
+ if (parent instanceof DAPThread) {
+ DAPFrame[] stack = ((DAPThread) parent).getStack();
+ if (stack != null) {
+ return stack;
+ } else {
+ return new Object[]{};
+ }
+ }
+ throw new UnknownTypeException(parent);
+ }
+
+ @Override
+ public int getChildrenCount(Object node) throws UnknownTypeException {
+ return Integer.MAX_VALUE;
+ }
+
+ @Override
+ public String getDisplayName(Object node) throws UnknownTypeException {
+ if (node instanceof DAPThread) {
+ return ((DAPThread) node).getName();
+ } else if (node instanceof DAPFrame) {
+ DAPFrame frame = (DAPFrame) node;
+ return frame.getName();
+ }
+ throw new UnknownTypeException (node);
+ }
+
+ @Override
+ public String getShortDescription(Object node) throws UnknownTypeException {
+ if (node instanceof DAPThread) {
+ String details = ((DAPThread) node).getDetails();
+ if (details == null) {
+ details = ((DAPThread) node).getName();
+ }
+ return details;
+ } else if (node instanceof DAPFrame) {
+ DAPFrame frame = (DAPFrame) node;
+ return frame.getDescription();
+ }
+ throw new UnknownTypeException (node);
+ }
+
+ @Override
+ public String getIconBase(Object node) throws UnknownTypeException {
+ throw new UnknownTypeException(node);
+ }
+
+ @Override
+ public String getIconBaseWithExtension(Object node) throws UnknownTypeException {
+ if (node instanceof DAPFrame) {
+ DAPFrame currentFrame = debugger.getCurrentFrame();
+ if (node.equals(currentFrame)) {
+ return CURRENT_CALL_STACK_ICON;
+ } else {
+ return CALL_STACK_ICON;
+ }
+ }
+ if (node instanceof DAPThread) {
+ DAPThread thread = (DAPThread) node;
+ return thread.isSuspended () ? SUSPENDED_THREAD_ICON : RUNNING_THREAD_ICON;
+ }
+ if (node == TreeModel.ROOT) {
+ return ""; // will not be displayed
+ }
+ throw new UnknownTypeException (node);
+ }
+
+ @Override
+ public boolean canCopy(Object node) throws UnknownTypeException {
+ return false;
+ }
+
+ @Override
+ public boolean canCut(Object node) throws UnknownTypeException {
+ return false;
+ }
+
+ @Override
+ public boolean canRename(Object node) throws UnknownTypeException {
+ return false;
+ }
+
+ @Override
+ public Transferable clipboardCopy(Object node) throws IOException, UnknownTypeException {
+ throw new UnknownTypeException(node);
+ }
+
+ @Override
+ public Transferable clipboardCut(Object node) throws IOException, UnknownTypeException {
+ throw new UnknownTypeException(node);
+ }
+
+ @Override
+ public PasteType[] getPasteTypes(Object node, Transferable t) throws UnknownTypeException {
+ throw new UnknownTypeException(node);
+ }
+
+ @Override
+ public void setName(Object node, String name) throws UnknownTypeException {
+ throw new UnknownTypeException(node);
+ }
+
+ @Override
+ public boolean isReadOnly(Object node, String columnID) throws UnknownTypeException {
+ return true;
+ }
+
+ @Override
+ public Object getValueAt(Object node, String columnID) throws UnknownTypeException {
+ if (columnID.equals("suspend")) {
+ if (node instanceof DAPThread) {
+ DAPThread thread = (DAPThread) node;
+ DAPThread.Status status = thread.getStatus();
+ switch (status) {
+ case CREATED:
+ return Boolean.FALSE;
+ case EXITED:
+ return null;
+ case RUNNING:
+ return Boolean.FALSE;
+ case SUSPENDED:
+ return Boolean.TRUE;
+ default:
+ throw new IllegalStateException("Unknown status: " + status);
+ }
+ } else {
+ return null;
+ }
+ }
+ throw new UnknownTypeException(node.toString());
+ }
+
+ @Override
+ public void setValueAt(Object node, String columnID, Object value) throws UnknownTypeException {
+ throw new UnsupportedOperationException("Not supported.");
+ }
+
+ @Override
+ public boolean isExpanded(TreeExpansionModel original, Object node) throws UnknownTypeException {
+ synchronized (this) {
+ if (expandedExplicitly.contains(node)) {
+ return true;
+ }
+ if (collapsedExplicitly.contains(node)) {
+ return false;
+ }
+ }
+ if (node instanceof DAPThread) {
+ DAPThread thread = (DAPThread) node;
+ return thread.isSuspended() && debugger.getCurrentThread() == thread;
+ }
+ return original.isExpanded(node);
+ }
+
+ @Override
+ public void nodeExpanded(Object node) {
+ synchronized (this) {
+ expandedExplicitly.add(node);
+ collapsedExplicitly.remove(node);
+ }
+ if (node instanceof DAPThread) {
+ fireNodeChange(node, ModelEvent.NodeChanged.DISPLAY_NAME_MASK);
+ }
+ }
+
+ @Override
+ public void nodeCollapsed(Object node) {
+ synchronized (this) {
+ expandedExplicitly.remove(node);
+ collapsedExplicitly.add(node);
+ }
+ if (node instanceof DAPThread) {
+ fireNodeChange(node, ModelEvent.NodeChanged.DISPLAY_NAME_MASK);
+ }
+ }
+
+ @Override
+ public void addModelListener(ModelListener l) {
+ listeners.add(l);
+ }
+
+ @Override
+ public void removeModelListener (ModelListener l) {
+ listeners.remove(l);
+ }
+
+ private void watchState(DAPThread t) {
+ synchronized (threadStateListeners) {
+ if (!threadStateListeners.containsKey(t)) {
+ threadStateListeners.put(t, new ThreadStateListener(t));
+ }
+ }
+ }
+
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ if (debugger.getTerminated().isDone()) {
+ clearCache();
+ return ;
+ }
+ refreshCache(ROOT);
+ ModelEvent ev = new ModelEvent.NodeChanged(this, ROOT, ModelEvent.NodeChanged.CHILDREN_MASK);
+ fireModelChange(ev);
+ fireNodeChange(null, ModelEvent.NodeChanged.DISPLAY_NAME_MASK | ModelEvent.NodeChanged.ICON_MASK | ModelEvent.NodeChanged.EXPANSION_MASK);
+ }
+
+ private void fireModelChange(ModelEvent me) {
+ for (ModelListener ls : listeners) {
+ ls.modelChanged(me);
+ }
+ }
+
+ private void fireNodeChange(Object node, int mask) {
+ ModelEvent event = new ModelEvent.NodeChanged(this, node, mask);
+ for (ModelListener ml : listeners) {
+ ml.modelChanged (event);
+ }
+ }
+
+ private class ThreadStateListener implements PropertyChangeListener {
+
+ private final Reference tr;
+ // currently waiting / running refresh task
+ // there is at most one
+ private RequestProcessor.Task task;
+ private final PropertyChangeListener propertyChangeListener;
+
+ public ThreadStateListener(DAPThread t) {
+ this.tr = new WeakReference<>(t);
+ this.propertyChangeListener = WeakListeners.propertyChange(this, t);
+ t.addPropertyChangeListener(propertyChangeListener);
+ }
+
+ @Override
+ public void propertyChange(PropertyChangeEvent evt) {
+ if (!evt.getPropertyName().equals(DVThread.PROP_SUSPENDED)) return ;
+ DAPThread t = tr.get();
+ if (t == null) return ;
+ // Refresh the children of the thread (stack frames) when the thread
+ // gets suspended or is resumed
+ synchronized (this) {
+ if (task == null) {
+ task = RP.create(new Refresher());
+ }
+ int delay = 100;
+ task.schedule(delay);
+ }
+ }
+
+ PropertyChangeListener getThreadPropertyChangeListener() {
+ return propertyChangeListener;
+ }
+
+ private class Refresher extends Object implements Runnable {
+ @Override
+ public void run() {
+ DAPThread thread = tr.get();
+ if (thread != null) {
+ try {
+ recomputeChildren(thread);
+ } catch (UnknownTypeException ex) {
+ refreshCache(thread);
+ }
+ ModelEvent event = new ModelEvent.NodeChanged(this, thread);
+ fireModelChange(event);
+ }
+ }
+ }
+ }
+
+
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/models/CallStackModel.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/models/CallStackModel.java
new file mode 100644
index 000000000000..def79b632e50
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/models/CallStackModel.java
@@ -0,0 +1,374 @@
+/*
+ * 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.lsp.client.debugger.models;
+
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import javax.swing.Action;
+import org.netbeans.modules.lsp.client.debugger.DAPFrame;
+import org.netbeans.modules.lsp.client.debugger.DAPThread;
+import org.netbeans.modules.lsp.client.debugger.DAPStackTraceAnnotationHolder;
+
+import org.netbeans.spi.debugger.ContextProvider;
+import org.netbeans.spi.debugger.DebuggerServiceRegistration;
+import org.netbeans.spi.viewmodel.ModelEvent;
+import org.netbeans.spi.viewmodel.NodeActionsProvider;
+import org.netbeans.spi.viewmodel.NodeModel;
+import org.netbeans.spi.viewmodel.TableModel;
+import org.netbeans.spi.viewmodel.TreeModel;
+import org.netbeans.spi.viewmodel.ModelListener;
+import org.netbeans.spi.viewmodel.UnknownTypeException;
+import org.netbeans.spi.debugger.ui.Constants;
+import org.netbeans.spi.viewmodel.ColumnModel;
+import org.openide.filesystems.FileObject;
+import org.openide.filesystems.URLMapper;
+
+import org.openide.text.Line;
+import org.openide.util.NbBundle;
+
+@DebuggerServiceRegistration(path="DAPDebuggerSession/CallStackView", types={TreeModel.class, NodeModel.class, NodeActionsProvider.class, TableModel.class})
+public class CallStackModel extends CurrentFrameTracker
+ implements TreeModel, NodeModel, NodeActionsProvider, TableModel {
+
+ private static final String CALL_STACK =
+ "org/netbeans/modules/debugger/resources/callStackView/NonCurrentFrame";
+ private static final String CURRENT_CALL_STACK =
+ "org/netbeans/modules/debugger/resources/callStackView/CurrentFrame";
+
+ @NbBundle.Messages("CTL_CallStackModel_noStack=No Stack Information")
+ private static final Object[] NO_STACK = new Object[]{Bundle.CTL_CallStackModel_noStack()};
+
+ private final List listeners = new CopyOnWriteArrayList<>();
+
+ public CallStackModel (ContextProvider contextProvider) {
+ super(contextProvider);
+ }
+
+
+ // TreeModel implementation ................................................
+
+ /**
+ * Returns the root node of the tree or null, if the tree is empty.
+ *
+ * @return the root node of the tree or null
+ */
+ @Override
+ public Object getRoot () {
+ return ROOT;
+ }
+
+ /**
+ * Returns children for given parent on given indexes.
+ *
+ * @param parent a parent of returned nodes
+ * @param from a start index
+ * @param to a end index
+ *
+ * @throws NoInformationException if the set of children can not be
+ * resolved
+ * @throws ComputingException if the children resolving process
+ * is time consuming, and will be performed off-line
+ * @throws UnknownTypeException if this TreeModel implementation is not
+ * able to resolve children for given node type
+ *
+ * @return children for given parent on given indexes
+ */
+ @Override
+ public Object[] getChildren (Object parent, int from, int to)
+ throws UnknownTypeException {
+ if (parent == ROOT) {
+ DAPThread currentThread = debugger.getCurrentThread();
+ if (currentThread == null) {
+ return NO_STACK;
+ }
+ return currentThread.getStack();
+ }
+ throw new UnknownTypeException (parent);
+ }
+
+ /**
+ * Returns true if node is leaf.
+ *
+ * @throws UnknownTypeException if this TreeModel implementation is not
+ * able to resolve children for given node type
+ * @return true if node is leaf
+ */
+ @Override
+ public boolean isLeaf (Object node) throws UnknownTypeException {
+ if (node == ROOT)
+ return false;
+ if (node instanceof DAPFrame) {
+ return true;
+ }
+ throw new UnknownTypeException (node);
+ }
+
+ /**
+ * Returns number of children for given node.
+ *
+ * @param node the parent node
+ * @throws NoInformationException if the set of children can not be
+ * resolved
+ * @throws ComputingException if the children resolving process
+ * is time consuming, and will be performed off-line
+ * @throws UnknownTypeException if this TreeModel implementation is not
+ * able to resolve children for given node type
+ *
+ * @return true if node is leaf
+ * @since 1.1
+ */
+ @Override
+ public int getChildrenCount (Object node) throws UnknownTypeException {
+ if (node == ROOT) {
+ DAPThread currentThread = debugger.getCurrentThread();
+ if (currentThread == null) {
+ return 0;
+ }
+ DAPFrame[] stack = currentThread.getStack();
+ if (stack == null) {
+ return 1;
+ } else {
+ return stack.length;
+ }
+ }
+ throw new UnknownTypeException (node);
+ }
+
+ /**
+ * Registers given listener.
+ *
+ * @param l the listener to add
+ */
+ @Override
+ public void addModelListener (ModelListener l) {
+ listeners.add (l);
+ }
+
+ /**
+ * Unregisters given listener.
+ *
+ * @param l the listener to remove
+ */
+ @Override
+ public void removeModelListener (ModelListener l) {
+ listeners.remove (l);
+ }
+
+
+ // NodeModel implementation ................................................
+
+ /**
+ * Returns display name for given node.
+ *
+ * @throws ComputingException if the display name resolving process
+ * is time consuming, and the value will be updated later
+ * @throws UnknownTypeException if this NodeModel implementation is not
+ * able to resolve display name for given node type
+ * @return display name for given node
+ */
+ @Override
+ public String getDisplayName (Object node) throws UnknownTypeException {
+ if (node instanceof DAPFrame) {
+ DAPFrame frame = (DAPFrame) node;
+ return frame.getName();
+ }
+ if (node == ROOT) {
+ return ROOT;
+ }
+ throw new UnknownTypeException (node);
+ }
+
+ /**
+ * Returns icon for given node.
+ *
+ * @throws ComputingException if the icon resolving process
+ * is time consuming, and the value will be updated later
+ * @throws UnknownTypeException if this NodeModel implementation is not
+ * able to resolve icon for given node type
+ * @return icon for given node
+ */
+ @Override
+ public String getIconBase (Object node) throws UnknownTypeException {
+ if (node instanceof DAPFrame) {
+ DAPFrame frame = (DAPFrame) node;
+ return frame == debugger.getCurrentFrame() ? CURRENT_CALL_STACK : CALL_STACK;
+ }
+ if (node == ROOT) {
+ return null;
+ }
+ throw new UnknownTypeException (node);
+ }
+
+ /**
+ * Returns tooltip for given node.
+ *
+ * @throws ComputingException if the tooltip resolving process
+ * is time consuming, and the value will be updated later
+ * @throws UnknownTypeException if this NodeModel implementation is not
+ * able to resolve tooltip for given node type
+ * @return tooltip for given node
+ */
+ @Override
+ public String getShortDescription (Object node) throws UnknownTypeException {
+ if (node instanceof DAPFrame) {
+ DAPFrame frame = (DAPFrame) node;
+ return frame.getDescription();
+ }
+ throw new UnknownTypeException (node);
+ }
+
+ // NodeActionsProvider implementation ......................................
+
+ /**
+ * Performs default action for given node.
+ *
+ * @throws UnknownTypeException if this NodeActionsProvider implementation
+ * is not able to resolve actions for given node type
+ * @return display name for given node
+ */
+ @Override
+ public void performDefaultAction (Object node) throws UnknownTypeException {
+ if (node instanceof DAPFrame) {
+ Line line = ((DAPFrame) node).location();
+ if (line != null) {
+ DAPStackTraceAnnotationHolder.showLine(new Line[] {line});
+ }
+ ((DAPFrame) node).makeCurrent();
+ return;
+ }
+ throw new UnknownTypeException (node);
+ }
+
+ /**
+ * Returns set of actions for given node.
+ *
+ * @throws UnknownTypeException if this NodeActionsProvider implementation
+ * is not able to resolve actions for given node type
+ * @return display name for given node
+ */
+ @Override
+ public Action[] getActions (Object node) throws UnknownTypeException {
+ return new Action [] {};
+ }
+
+ // TableModel implementation ...............................................
+
+ /**
+ * Returns value to be displayed in column columnID
+ * and row identified by node. Column ID is defined in by
+ * {@link ColumnModel#getID}, and rows are defined by values returned from
+ * {@link org.netbeans.spi.viewmodel.TreeModel#getChildren}.
+ *
+ * @param node a object returned from
+ * {@link org.netbeans.spi.viewmodel.TreeModel#getChildren} for this row
+ * @param columnID a id of column defined by {@link ColumnModel#getID}
+ * @throws ComputingException if the value is not known yet and will
+ * be computed later
+ * @throws UnknownTypeException if there is no TableModel defined for given
+ * parameter type
+ *
+ * @return value of variable representing given position in tree table.
+ */
+ @Override
+ public Object getValueAt (Object node, String columnID) throws
+ UnknownTypeException {
+ if (columnID == Constants.CALL_STACK_FRAME_LOCATION_COLUMN_ID) {
+ if (node instanceof DAPFrame) {
+ DAPFrame frame = (DAPFrame) node;
+ URI sourceURI = frame.getSourceURI();
+ if (sourceURI == null) {
+ return "";
+ }
+ String sourceName;
+ try {
+ FileObject file = URLMapper.findFileObject(sourceURI.toURL());
+ sourceName = file.getPath();
+ } catch (MalformedURLException ex) {
+ sourceName = sourceURI.toString();
+ }
+ int line = frame.getLine();
+ if (line > 0) {
+ return sourceName + ':' + line;
+ } else {
+ return sourceName + ":?";
+ }
+ }
+ }
+ throw new UnknownTypeException (node);
+ }
+
+ /**
+ * Returns true if value displayed in column columnID
+ * and row node is read only. Column ID is defined in by
+ * {@link ColumnModel#getID}, and rows are defined by values returned from
+ * {@link TreeModel#getChildren}.
+ *
+ * @param node a object returned from {@link TreeModel#getChildren} for this row
+ * @param columnID a id of column defined by {@link ColumnModel#getID}
+ * @throws UnknownTypeException if there is no TableModel defined for given
+ * parameter type
+ *
+ * @return true if variable on given position is read only
+ */
+ @Override
+ public boolean isReadOnly (Object node, String columnID) throws UnknownTypeException {
+ if (columnID == Constants.CALL_STACK_FRAME_LOCATION_COLUMN_ID) {
+ if (node instanceof DAPFrame) {
+ return true;
+ }
+ }
+ throw new UnknownTypeException (node);
+ }
+
+ /**
+ * Changes a value displayed in column columnID
+ * and row node. Column ID is defined in by
+ * {@link ColumnModel#getID}, and rows are defined by values returned from
+ * {@link TreeModel#getChildren}.
+ *
+ * @param node a object returned from {@link TreeModel#getChildren} for this row
+ * @param columnID a id of column defined by {@link ColumnModel#getID}
+ * @param value a new value of variable on given position
+ * @throws UnknownTypeException if there is no TableModel defined for given
+ * parameter type
+ */
+ @Override
+ public void setValueAt (Object node, String columnID, Object value) throws UnknownTypeException {
+ throw new UnknownTypeException (node);
+ }
+
+
+ // other mothods ...........................................................
+
+ private void fireChanges() {
+ ModelEvent.TreeChanged event = new ModelEvent.TreeChanged(this);
+ for (ModelListener l : listeners) {
+ l.modelChanged(event);
+ }
+ }
+
+ @Override
+ protected void frameChanged() {
+ fireChanges();
+ }
+
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/models/CurrentFrameTracker.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/models/CurrentFrameTracker.java
new file mode 100644
index 000000000000..c25c4ff08b64
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/models/CurrentFrameTracker.java
@@ -0,0 +1,98 @@
+/*
+ * 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.lsp.client.debugger.models;
+
+import java.beans.PropertyChangeListener;
+import java.util.function.Supplier;
+import javax.swing.event.ChangeListener;
+import org.netbeans.modules.lsp.client.debugger.DAPDebugger;
+import org.netbeans.modules.lsp.client.debugger.DAPFrame;
+import org.netbeans.modules.lsp.client.debugger.DAPThread;
+
+import org.netbeans.spi.debugger.ContextProvider;
+
+import org.openide.util.WeakListeners;
+
+public class CurrentFrameTracker {
+
+ protected final DAPDebugger debugger;
+ private final ChangeListener threadListener;
+ private final PropertyChangeListener frameListener;
+ private volatile DAPThread currentThread;
+ private volatile DAPFrame currentFrame;
+
+
+ public CurrentFrameTracker (ContextProvider contextProvider) {
+ debugger = contextProvider.lookupFirst(null, DAPDebugger.class);
+ currentThread = debugger.getCurrentThread();
+ Supplier getCurrentThreadFrame = () -> {
+ DAPThread cachedCurrentThread = currentThread;
+ return cachedCurrentThread != null ? cachedCurrentThread.getCurrentFrame()
+ : null;
+ };
+ currentFrame = getCurrentThreadFrame.get();
+
+ Runnable frameChanged = () -> {
+ DAPFrame prevFrame = currentFrame;
+ DAPFrame newFrame = getCurrentThreadFrame.get();
+
+ if (prevFrame != newFrame) {
+ currentFrame = newFrame;
+ frameChanged();
+ }
+ };
+ frameListener = evt -> {
+ if (evt.getPropertyName() == null ||
+ DAPThread.PROP_CURRENT_FRAME.equals(evt.getPropertyName())) {
+ frameChanged.run();
+ }
+ };
+ threadListener = evt -> {
+ DAPThread prevThread;
+ DAPThread newThread;
+ boolean changed;
+
+ synchronized (this) {
+ prevThread = currentThread;
+ newThread = debugger.getCurrentThread();
+
+ if (changed = (prevThread != newThread)) {
+ currentThread = newThread;
+ if (prevThread != null) {
+ prevThread.removePropertyChangeListener(frameListener);
+ }
+ if (newThread != null) {
+ newThread.addPropertyChangeListener(frameListener);
+ }
+ }
+ }
+ if (changed) {
+ frameChanged.run();
+ }
+ };
+ debugger.addChangeListener(WeakListeners.change(threadListener, debugger));
+ }
+
+ protected final DAPFrame getCurrentFrame() {
+ return currentFrame;
+ }
+
+ protected void frameChanged() {}
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/models/VariablesModel.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/models/VariablesModel.java
new file mode 100644
index 000000000000..df5ce74585b1
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/models/VariablesModel.java
@@ -0,0 +1,328 @@
+/*
+ * 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.lsp.client.debugger.models;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import org.netbeans.modules.lsp.client.debugger.DAPDebugger;
+import org.netbeans.modules.lsp.client.debugger.DAPFrame;
+import org.netbeans.modules.lsp.client.debugger.DAPVariable;
+
+import org.netbeans.spi.debugger.ContextProvider;
+import org.netbeans.spi.debugger.DebuggerServiceRegistration;
+import org.netbeans.spi.viewmodel.ModelEvent;
+import org.netbeans.spi.viewmodel.NodeModel;
+import org.netbeans.spi.viewmodel.TableModel;
+import org.netbeans.spi.viewmodel.TreeModel;
+import org.netbeans.spi.viewmodel.ModelListener;
+import org.netbeans.spi.viewmodel.UnknownTypeException;
+import org.openide.util.NbBundle;
+
+import org.netbeans.spi.viewmodel.ColumnModel;
+
+/**
+ */
+@DebuggerServiceRegistration(path="DAPDebuggerSession/LocalsView", types={TreeModel.class, NodeModel.class, TableModel.class})
+public final class VariablesModel extends CurrentFrameTracker implements TreeModel, NodeModel, TableModel {
+
+ private static final String LOCAL =
+ "org/netbeans/modules/debugger/resources/localsView/LocalVariable";
+
+ @NbBundle.Messages("CTL_VariablesModel_noVars=No variables to display.") //better mesage?
+ private static final Object[] NO_VARS = new Object[]{Bundle.CTL_VariablesModel_noVars()};
+
+ private final DAPDebugger debugger;
+ private final List listeners = new CopyOnWriteArrayList<>();
+
+
+ public VariablesModel (ContextProvider contextProvider) {
+ super(contextProvider);
+ debugger = contextProvider.lookupFirst(null, DAPDebugger.class);
+ }
+
+
+ // TreeModel implementation ................................................
+
+ /**
+ * Returns the root node of the tree or null, if the tree is empty.
+ *
+ * @return the root node of the tree or null
+ */
+ @Override
+ public Object getRoot () {
+ return ROOT;
+ }
+
+ /**
+ * Returns children for given parent on given indexes.
+ *
+ * @param parent a parent of returned nodes
+ * @param from a start index
+ * @param to a end index
+ *
+ * @throws NoInformationException if the set of children can not be
+ * resolved
+ * @throws ComputingException if the children resolving process
+ * is time consuming, and will be performed off-line
+ * @throws UnknownTypeException if this TreeModel implementation is not
+ * able to resolve children for given node type
+ *
+ * @return children for given parent on given indexes
+ */
+ @Override
+ public Object[] getChildren (Object parent, int from, int to) throws UnknownTypeException {
+ DAPVariable parentVar;
+ if (parent == ROOT) {
+ parentVar = null;
+ } else if (parent instanceof DAPVariable) {
+ parentVar = (DAPVariable) parent;
+ } else {
+ throw new UnknownTypeException (parent);
+ }
+ DAPFrame frame = getCurrentFrame();
+ if (frame != null) {
+ if (parentVar == null) {
+ try {
+ return debugger.getFrameVariables(frame).get().toArray();
+ } catch (Throwable t) {
+ return new Object[] {t.getLocalizedMessage()};
+ }
+ } else {
+ return parentVar.getChildren(from, to);
+ }
+ } else {
+ return NO_VARS;
+ }
+ }
+
+ /**
+ * Returns true if node is leaf.
+ *
+ * @throws UnknownTypeException if this TreeModel implementation is not
+ * able to resolve children for given node type
+ * @return true if node is leaf
+ */
+ @Override
+ public boolean isLeaf (Object node) throws UnknownTypeException {
+ if (node == ROOT) {
+ return false;
+ }
+ if (node instanceof String) {
+ return true;
+ }
+ if (node instanceof DAPVariable) {
+ return ((DAPVariable) node).getTotalChildren() == 0;
+ }
+ throw new UnknownTypeException (node);
+ }
+
+ /**
+ * Returns number of children for given node.
+ *
+ * @param node the parent node
+ * @throws NoInformationException if the set of children can not be
+ * resolved
+ * @throws ComputingException if the children resolving process
+ * is time consuming, and will be performed off-line
+ * @throws UnknownTypeException if this TreeModel implementation is not
+ * able to resolve children for given node type
+ *
+ * @return true if node is leaf
+ * @since 1.1
+ */
+ @Override
+ public int getChildrenCount (Object node) throws UnknownTypeException {
+ if (node == ROOT) {
+ return Integer.MAX_VALUE;
+ } else if (node instanceof DAPVariable) {
+ return ((DAPVariable) node).getTotalChildren();
+ }
+ throw new UnknownTypeException (node);
+ }
+
+ /**
+ * Registers given listener.
+ *
+ * @param l the listener to add
+ */
+ @Override
+ public void addModelListener (ModelListener l) {
+ listeners.add (l);
+ }
+
+ /**
+ * Unregisters given listener.
+ *
+ * @param l the listener to remove
+ */
+ @Override
+ public void removeModelListener (ModelListener l) {
+ listeners.remove (l);
+ }
+
+
+ // NodeModel implementation ................................................
+
+ /**
+ * Returns display name for given node.
+ *
+ * @throws ComputingException if the display name resolving process
+ * is time consuming, and the value will be updated later
+ * @throws UnknownTypeException if this NodeModel implementation is not
+ * able to resolve display name for given node type
+ * @return display name for given node
+ */
+ @Override
+ public String getDisplayName (Object node) throws UnknownTypeException {
+ if (node instanceof String) {
+ return (String) node;
+ }
+ if (node instanceof DAPVariable) {
+ return ((DAPVariable) node).getName();
+ }
+ throw new UnknownTypeException (node);
+ }
+
+ /**
+ * Returns icon for given node.
+ *
+ * @throws ComputingException if the icon resolving process
+ * is time consuming, and the value will be updated later
+ * @throws UnknownTypeException if this NodeModel implementation is not
+ * able to resolve icon for given node type
+ * @return icon for given node
+ */
+ @Override
+ public String getIconBase (Object node) throws UnknownTypeException {
+ if (node instanceof DAPVariable) {
+ return LOCAL;
+ }
+ if (node instanceof String) {
+ return null;
+ }
+ throw new UnknownTypeException (node);
+ }
+
+ /**
+ * Returns tooltip for given node.
+ *
+ * @throws ComputingException if the tooltip resolving process
+ * is time consuming, and the value will be updated later
+ * @throws UnknownTypeException if this NodeModel implementation is not
+ * able to resolve tooltip for given node type
+ * @return tooltip for given node
+ */
+ @Override
+ public String getShortDescription (Object node) throws UnknownTypeException {
+ if (node instanceof String)
+ return null;
+ throw new UnknownTypeException (node);
+ }
+
+
+ // TableModel implementation ...............................................
+
+ /**
+ * Returns value to be displayed in column columnID
+ * and row identified by node. Column ID is defined in by
+ * {@link ColumnModel#getID}, and rows are defined by values returned from
+ * {@link org.netbeans.spi.viewmodel.TreeModel#getChildren}.
+ *
+ * @param node a object returned from
+ * {@link org.netbeans.spi.viewmodel.TreeModel#getChildren} for this row
+ * @param columnID a id of column defined by {@link ColumnModel#getID}
+ * @throws ComputingException if the value is not known yet and will
+ * be computed later
+ * @throws UnknownTypeException if there is no TableModel defined for given
+ * parameter type
+ *
+ * @return value of variable representing given position in tree table.
+ */
+ @Override
+ public Object getValueAt (Object node, String columnID) throws UnknownTypeException {
+ if (columnID.equals ("LocalsValue")) {
+ if (node instanceof DAPVariable) {
+ return ((DAPVariable) node).getValue();
+ }
+ }
+ if (columnID.equals ("LocalsType")) {
+ if (node instanceof DAPVariable) {
+ return ((DAPVariable) node).getType();
+ }
+ }
+ if (node instanceof String) {
+ return "";
+ }
+ throw new UnknownTypeException (node);
+ }
+
+ /**
+ * Returns true if value displayed in column columnID
+ * and row node is read only. Column ID is defined in by
+ * {@link ColumnModel#getID}, and rows are defined by values returned from
+ * {@link TreeModel#getChildren}.
+ *
+ * @param node a object returned from {@link TreeModel#getChildren} for this row
+ * @param columnID a id of column defined by {@link ColumnModel#getID}
+ * @throws UnknownTypeException if there is no TableModel defined for given
+ * parameter type
+ *
+ * @return true if variable on given position is read only
+ */
+ @Override
+ public boolean isReadOnly (Object node, String columnID) throws UnknownTypeException {
+ if ( (node instanceof String) &&
+ (columnID.equals ("LocalsValue"))
+ ) return true;
+ throw new UnknownTypeException (node);
+ }
+
+ /**
+ * Changes a value displayed in column columnID
+ * and row node. Column ID is defined in by
+ * {@link ColumnModel#getID}, and rows are defined by values returned from
+ * {@link TreeModel#getChildren}.
+ *
+ * @param node a object returned from {@link TreeModel#getChildren} for this row
+ * @param columnID a id of column defined by {@link ColumnModel#getID}
+ * @param value a new value of variable on given position
+ * @throws UnknownTypeException if there is no TableModel defined for given
+ * parameter type
+ */
+ @Override
+ public void setValueAt (Object node, String columnID, Object value) throws UnknownTypeException {
+ throw new UnknownTypeException (node);
+ }
+
+
+ // other mothods ...........................................................
+
+ void fireChanges () {
+ ModelEvent.TreeChanged event = new ModelEvent.TreeChanged(this);
+ for (ModelListener l : listeners) {
+ l.modelChanged(event);
+ }
+ }
+
+ @Override
+ protected void frameChanged() {
+ fireChanges();
+ }
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/models/WatchesModel.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/models/WatchesModel.java
new file mode 100644
index 000000000000..72730e7c678d
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/models/WatchesModel.java
@@ -0,0 +1,392 @@
+/*
+ * 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.lsp.client.debugger.models;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.netbeans.api.debugger.Watch;
+import org.netbeans.modules.lsp.client.debugger.DAPDebugger;
+import org.netbeans.modules.lsp.client.debugger.DAPFrame;
+import org.netbeans.modules.lsp.client.debugger.DAPVariable;
+import org.netbeans.spi.debugger.ContextProvider;
+import org.netbeans.spi.debugger.DebuggerServiceRegistration;
+import org.netbeans.spi.viewmodel.ModelEvent;
+import org.netbeans.spi.viewmodel.NodeModel;
+import org.netbeans.spi.viewmodel.TableModel;
+import org.netbeans.spi.viewmodel.ModelListener;
+import org.netbeans.spi.viewmodel.UnknownTypeException;
+import org.netbeans.spi.debugger.ui.Constants;
+import org.netbeans.spi.viewmodel.ColumnModel;
+import org.netbeans.spi.viewmodel.NodeModelFilter;
+import org.netbeans.spi.viewmodel.TreeModel;
+import org.netbeans.spi.viewmodel.TreeModelFilter;
+
+@DebuggerServiceRegistration(path="DAPDebuggerSession/WatchesView", types={TreeModelFilter.class, NodeModelFilter.class, TableModel.class})
+public class WatchesModel extends CurrentFrameTracker implements TreeModelFilter, NodeModelFilter, TableModel {
+
+ private static final String WATCH =
+ "org/netbeans/modules/debugger/resources/watchesView/Watch";
+
+ private final DAPDebugger debugger;
+ private final Map evalWatches = new HashMap<>();
+ private final List listeners = new CopyOnWriteArrayList<>();
+
+ public WatchesModel (ContextProvider contextProvider) {
+ super(contextProvider);
+ debugger = contextProvider.lookupFirst(null, DAPDebugger.class);
+ }
+
+ // TreeModelFilter implementation ................................................
+
+ @Override
+ public Object getRoot(TreeModel original) {
+ return original.getRoot();
+ }
+
+ @Override
+ public Object[] getChildren(TreeModel original, Object parent, int from, int to) throws UnknownTypeException {
+ Object[] watches = original.getChildren(parent, from, to);
+ synchronized (evalWatches) {
+ for (int i = 0; i < watches.length; i++) {
+ Object watchObj = watches[i];
+ if (watchObj instanceof Watch) {
+ Watch w = (Watch) watchObj;
+ EvalWatch ew = evalWatches.get(w);
+ if (ew == null) {
+ ew = new EvalWatch(w);
+ evalWatches.put(w, ew);
+ }
+ }
+ }
+ }
+ return watches;
+ }
+
+ @Override
+ public int getChildrenCount(TreeModel original, Object node) throws UnknownTypeException {
+ EvalWatch ew;
+ synchronized (evalWatches) {
+ ew = evalWatches.get(node);
+ }
+ if (ew != null) {
+ switch (ew.getStatus()) {
+ case READY:
+ DAPVariable result = ew.getResult();
+ return result.getTotalChildren();
+ }
+ }
+ return original.getChildrenCount(node);
+ }
+
+ @Override
+ public boolean isLeaf(TreeModel original, Object node) throws UnknownTypeException {
+ EvalWatch ew;
+ synchronized (evalWatches) {
+ ew = evalWatches.get(node);
+ }
+ if (ew != null) {
+ switch (ew.getStatus()) {
+ case READY:
+ DAPVariable result = ew.getResult();
+ return result.getTotalChildren() == 0;
+ }
+ }
+ return true;
+ }
+
+ // NodeModelFilter implementation ................................................
+
+ /**
+ * Returns display name for given node.
+ *
+ * @throws ComputingException if the display name resolving process
+ * is time consuming, and the value will be updated later
+ * @throws UnknownTypeException if this NodeModel implementation is not
+ * able to resolve display name for given node type
+ * @return display name for given node
+ */
+ @Override
+ public String getDisplayName (NodeModel model, Object node) throws UnknownTypeException {
+ return model.getDisplayName(node);
+ }
+
+ /**
+ * Returns icon for given node.
+ *
+ * @throws ComputingException if the icon resolving process
+ * is time consuming, and the value will be updated later
+ * @throws UnknownTypeException if this NodeModel implementation is not
+ * able to resolve icon for given node type
+ * @return icon for given node
+ */
+ @Override
+ public String getIconBase (NodeModel model, Object node) throws UnknownTypeException {
+ return model.getIconBase(node);
+ }
+
+ /**
+ * Returns tooltip for given node.
+ *
+ * @throws ComputingException if the tooltip resolving process
+ * is time consuming, and the value will be updated later
+ * @throws UnknownTypeException if this NodeModel implementation is not
+ * able to resolve tooltip for given node type
+ * @return tooltip for given node
+ */
+ @Override
+ public String getShortDescription (NodeModel model, Object node) throws UnknownTypeException {
+ EvalWatch ew;
+ synchronized (evalWatches) {
+ ew = evalWatches.get(node);
+ }
+ if (ew != null) {
+ ew.startEvaluate();
+ switch (ew.getStatus()) {
+ case READY:
+ DAPVariable result = ew.getResult();
+ return ew.getExpression() + " = " + result.getValue();
+ case FAILED:
+ Exception exc = ew.getException();
+ return exc.getLocalizedMessage();
+ }
+ }
+ return model.getShortDescription(node);
+ }
+
+
+ // TableModel implementation ...............................................
+
+ /**
+ * Returns value to be displayed in column columnID
+ * and row identified by node. Column ID is defined in by
+ * {@link ColumnModel#getID}, and rows are defined by values returned from
+ * {@link org.netbeans.spi.viewmodel.TreeModel#getChildren}.
+ *
+ * @param node a object returned from
+ * {@link org.netbeans.spi.viewmodel.TreeModel#getChildren} for this row
+ * @param columnID a id of column defined by {@link ColumnModel#getID}
+ * @throws ComputingException if the value is not known yet and will
+ * be computed later
+ * @throws UnknownTypeException if there is no TableModel defined for given
+ * parameter type
+ *
+ * @return value of variable representing given position in tree table.
+ */
+ @Override
+ public Object getValueAt (Object node, String columnID) throws UnknownTypeException {
+ boolean showValue = columnID == Constants.WATCH_VALUE_COLUMN_ID;
+ if (showValue || columnID == Constants.WATCH_TYPE_COLUMN_ID) {
+ EvalWatch ew;
+ synchronized (evalWatches) {
+ ew = evalWatches.get(node);
+ }
+ if (ew != null) {
+ ew.startEvaluate();
+ switch (ew.getStatus()) {
+ case READY:
+ DAPVariable result = ew.getResult();
+ if (showValue) {
+ return result.getValue();
+ } else {
+ return result.getType();
+ }
+ case FAILED:
+ if (showValue) {
+ Exception exc = ew.getException();
+ return exc.getLocalizedMessage();
+ }
+ }
+ return "";
+ }
+ }
+ throw new UnknownTypeException (node);
+ }
+
+ /**
+ * Returns true if value displayed in column columnID
+ * and row node is read only. Column ID is defined in by
+ * {@link ColumnModel#getID}, and rows are defined by values returned from
+ * {@link TreeModel#getChildren}.
+ *
+ * @param node a object returned from {@link TreeModel#getChildren} for this row
+ * @param columnID a id of column defined by {@link ColumnModel#getID}
+ * @throws UnknownTypeException if there is no TableModel defined for given
+ * parameter type
+ *
+ * @return true if variable on given position is read only
+ */
+ @Override
+ public boolean isReadOnly (Object node, String columnID) throws UnknownTypeException {
+ //TODO: possibility to set a value
+ if (columnID == Constants.WATCH_VALUE_COLUMN_ID ||
+ columnID == Constants.WATCH_TYPE_COLUMN_ID) {
+ if (node instanceof Watch) {
+ return true;
+ }
+ }
+ throw new UnknownTypeException (node);
+ }
+
+ /**
+ * Changes a value displayed in column columnID
+ * and row node. Column ID is defined in by
+ * {@link ColumnModel#getID}, and rows are defined by values returned from
+ * {@link TreeModel#getChildren}.
+ *
+ * @param node a object returned from {@link TreeModel#getChildren} for this row
+ * @param columnID a id of column defined by {@link ColumnModel#getID}
+ * @param value a new value of variable on given position
+ * @throws UnknownTypeException if there is no TableModel defined for given
+ * parameter type
+ */
+ @Override
+ public void setValueAt (Object node, String columnID, Object value) throws UnknownTypeException {
+ throw new UnknownTypeException (node);
+ }
+
+
+ /**
+ * Registers given listener.
+ *
+ * @param l the listener to add
+ */
+ @Override
+ public void addModelListener (ModelListener l) {
+ listeners.add (l);
+ }
+
+ /**
+ * Unregisters given listener.
+ *
+ * @param l the listener to remove
+ */
+ @Override
+ public void removeModelListener (ModelListener l) {
+ listeners.remove (l);
+ }
+
+
+ // other mothods ...........................................................
+
+ void fireChanges() {
+ ModelEvent.TreeChanged event = new ModelEvent.TreeChanged(this);
+ for (ModelListener l : listeners) {
+ l.modelChanged(event);
+ }
+ }
+
+ void fireChanged(Object node) {
+ ModelEvent.NodeChanged event = new ModelEvent.NodeChanged(this, node);
+ for (ModelListener l : listeners) {
+ l.modelChanged(event);
+ }
+ }
+
+ @Override
+ protected void frameChanged() {
+ synchronized (evalWatches) {
+ evalWatches.values().forEach(EvalWatch::ensureRecalculated);
+ }
+ fireChanges();
+ }
+
+ enum EvalStatus {
+ NEW,
+ EVALUATING,
+ READY,
+ FAILED,
+ SKIPPED
+ }
+
+ private final class EvalWatch implements PropertyChangeListener {
+
+ private final Watch watch;
+ private volatile AtomicReference status = new AtomicReference<>(EvalStatus.NEW);
+ private volatile String expression;
+ private volatile DAPVariable result;
+ private volatile Exception exception; //TODO: Throwable?
+
+ private EvalWatch(Watch watch) {
+ this.watch = watch;
+ watch.addPropertyChangeListener(this);
+ }
+
+ EvalStatus getStatus() {
+ return status.get();
+ }
+
+ void startEvaluate() {
+ DAPFrame frame = getCurrentFrame();
+ if (frame == null || !watch.isEnabled()) {
+ status.compareAndSet(EvalStatus.NEW, EvalStatus.SKIPPED);
+ return;
+ }
+ if (status.compareAndSet(EvalStatus.NEW, EvalStatus.EVALUATING)) {
+ result = null;
+ exception = null;
+ String expression = watch.getExpression();
+ this.expression = expression;
+ debugger.evaluate(frame, expression)
+ .thenAccept(
+ (DAPVariable variable) -> {
+ result = variable;
+ status.set(EvalStatus.READY);
+ fireChanged(watch);
+ })
+ .exceptionally(
+ exc -> {
+ exception = (Exception) exc;
+ status.set(EvalStatus.FAILED);
+ fireChanged(watch);
+ return null;
+ });
+ }
+ }
+
+ String getExpression() {
+ return expression;
+ }
+
+ DAPVariable getResult() {
+ return result;
+ }
+
+ Exception getException() {
+ return exception;
+ }
+
+ @Override
+ public void propertyChange(PropertyChangeEvent evt) {
+ ensureRecalculated();
+ }
+
+ private void ensureRecalculated() {
+ if (status.getAndSet(EvalStatus.NEW) != EvalStatus.NEW) {
+ startEvaluate();
+ }
+ }
+ }
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/spi/BreakpointConvertor.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/spi/BreakpointConvertor.java
new file mode 100644
index 000000000000..4ba3020b7c53
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/spi/BreakpointConvertor.java
@@ -0,0 +1,81 @@
+/*
+ * 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.lsp.client.debugger.spi;
+
+import java.net.URI;
+import java.util.List;
+import org.netbeans.api.annotations.common.NonNull;
+import org.netbeans.api.annotations.common.NullAllowed;
+import org.netbeans.modules.lsp.client.debugger.LineBreakpointData;
+import org.netbeans.modules.lsp.client.debugger.SPIAccessor;
+
+/**Convert language-specific breakpoints to format usable by the DAP debugger.
+ *
+ * Implementations should inspect the provided {@code Breakpoint}, and if they
+ * recognize it, and there's a corresponding method in the provided
+ * {@code ConvertedBreakpointConsumer} instance, the method should be called.
+ *
+ * The implementations should be registered in the global {@code Lookup}.
+ *
+ * @since 1.29
+ */
+public interface BreakpointConvertor {
+ /**
+ * Inspect the provided {@code Breakpoint}, and call an appropriate method
+ * on the provided {@code ConvertedBreakpointConsumer} if possible.
+ *
+ * @param b the breakpoint to inspect
+ * @param breakpointConsumer the consumer of which the appropriate method
+ * should be invoked
+ */
+ public void convert(org.netbeans.api.debugger.Breakpoint b,
+ ConvertedBreakpointConsumer breakpointConsumer);
+
+ /**
+ * Set of callbacks for converted breakpoints.
+ */
+ public static class ConvertedBreakpointConsumer {
+ private final List lineBreakpoints;
+
+ ConvertedBreakpointConsumer(List lineBreakpoints) {
+ this.lineBreakpoints = lineBreakpoints;
+ }
+
+ /**Report a line-based breakpoint, with the given properties
+ *
+ * @param uri the location of the file where the breakpoint is set
+ * @param lineNumber the line number on which the breakpoint is set
+ * @param condition an optional condition expression - the the debugger
+ * will only stop if this evaluates to a language-specific
+ * {@code true} representation; may be {@code null}
+ */
+ public void lineBreakpoint(@NonNull URI uri, int lineNumber, @NullAllowed String condition) {
+ lineBreakpoints.add(new LineBreakpointData(uri, lineNumber, condition));
+ }
+
+ static {
+ SPIAccessor.setInstance(new SPIAccessor() {
+ @Override
+ public ConvertedBreakpointConsumer createConvertedBreakpointConsumer(List lineBreakpoints) {
+ return new ConvertedBreakpointConsumer(lineBreakpoints);
+ }
+ });
+ }
+ }
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/views/DAPComponentsProvider.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/views/DAPComponentsProvider.java
new file mode 100644
index 000000000000..ca730789451a
--- /dev/null
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/views/DAPComponentsProvider.java
@@ -0,0 +1,101 @@
+/*
+ * 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.lsp.client.debugger.views;
+
+import java.awt.Component;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.prefs.Preferences;
+import org.netbeans.api.debugger.Properties;
+import org.netbeans.spi.debugger.ContextProvider;
+import org.netbeans.spi.debugger.DebuggerServiceRegistration;
+import org.netbeans.spi.debugger.ui.EngineComponentsProvider;
+import org.openide.util.NbPreferences;
+import org.openide.windows.TopComponent;
+import org.openide.windows.WindowManager;
+
+@DebuggerServiceRegistration(path="CPPLiteSession", types=EngineComponentsProvider.class)
+public class DAPComponentsProvider implements EngineComponentsProvider {
+
+ private static final String PROPERTY_CLOSED_TC = "closedTopComponents"; // NOI18N
+ private static final String PROPERTY_MINIMIZED_TC = "minimizedTopComponents"; // NOI18N
+ private static final String PROPERTY_BASE_NAME = "CPPLiteSession.EngineComponentsProvider"; // NOI18N
+
+ private static final String[] DBG_COMPONENTS_OPENED = {
+ "localsView", "watchesView", "breakpointsView", "debuggingView" // NOI18N
+ };
+ private static final String[] DBG_COMPONENTS_CLOSED = {
+ "callstackView", "evaluatorPane", "resultsView", "sessionsView" // NOI18N
+ };
+
+ @Override
+ public List getComponents() {
+ List components = new ArrayList<>(DBG_COMPONENTS_OPENED.length + DBG_COMPONENTS_CLOSED.length);
+ for (String cid : DBG_COMPONENTS_OPENED) {
+ components.add(EngineComponentsProvider.ComponentInfo.create(
+ cid, isOpened(cid, true), isMinimized(cid)));
+ }
+ for (String cid : DBG_COMPONENTS_CLOSED) {
+ components.add(EngineComponentsProvider.ComponentInfo.create(
+ cid, isOpened(cid, false), isMinimized(cid)));
+ }
+ return components;
+ }
+
+ private static boolean isOpened(String cid, boolean open) {
+ if (cid.equals("watchesView")) { // NOI18N
+ Preferences preferences = NbPreferences.forModule(ContextProvider.class).node("variables_view"); // NOI18N
+ open = !preferences.getBoolean("show_watches", true); // NOI18N
+ }
+ boolean wasClosed = Properties.getDefault().getProperties(PROPERTY_BASE_NAME).
+ getProperties(PROPERTY_CLOSED_TC).getBoolean(cid, false);
+ boolean wasOpened = !Properties.getDefault().getProperties(PROPERTY_BASE_NAME).
+ getProperties(PROPERTY_CLOSED_TC).getBoolean(cid, true);
+ open = (open && !wasClosed || !open && wasOpened);
+ return open;
+ }
+
+ private static boolean isMinimized(String cid) {
+ boolean wasMinimized = Properties.getDefault().getProperties(PROPERTY_BASE_NAME).
+ getProperties(PROPERTY_MINIMIZED_TC).getBoolean(cid, false);
+ boolean wasDeminim = !Properties.getDefault().getProperties(PROPERTY_BASE_NAME).
+ getProperties(PROPERTY_MINIMIZED_TC).getBoolean(cid, false);
+ boolean minimized = (wasMinimized || !wasDeminim);
+ return minimized;
+ }
+
+ @Override
+ public void willCloseNotify(List components) {
+ for (ComponentInfo ci : components) {
+ Component c = ci.getComponent();
+ if (c instanceof TopComponent) {
+ TopComponent tc = (TopComponent) c;
+ boolean isOpened = tc.isOpened();
+ String tcId = WindowManager.getDefault().findTopComponentID(tc);
+ Properties.getDefault().getProperties(PROPERTY_BASE_NAME).
+ getProperties(PROPERTY_CLOSED_TC).setBoolean(tcId, !isOpened);
+ boolean isMinimized = WindowManager.getDefault().isTopComponentMinimized(tc);
+ Properties.getDefault().getProperties(PROPERTY_BASE_NAME).
+ getProperties(PROPERTY_MINIMIZED_TC).setBoolean(tcId, isMinimized);
+ }
+ }
+ }
+
+}
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/Bundle.properties b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/Bundle.properties
index c3e7d50a7890..544513827ccd 100644
--- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/Bundle.properties
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/Bundle.properties
@@ -40,3 +40,4 @@ ACSD_OnOff_CB=Checkbox switching mark occurences on/off
ACSD_Marks_CB=Keep Marks Checkbox
text/x-generic-lsp=Language Server Protocol Client (generic)
+LanguageDescriptionPanel.debugger.text=Enable &Breakpoints
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageDescriptionPanel.form b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageDescriptionPanel.form
index d0ff829748f3..91c49f19e8cb 100644
--- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageDescriptionPanel.form
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageDescriptionPanel.form
@@ -221,5 +221,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageDescriptionPanel.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageDescriptionPanel.java
index db9da03aa3c7..84049154af88 100644
--- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageDescriptionPanel.java
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageDescriptionPanel.java
@@ -53,11 +53,12 @@ public LanguageDescriptionPanel(LanguageDescription desc, Set usedIds) {
this.server.setText(desc.languageServer);
this.name.setText(desc.name);
this.icon.setText(desc.icon);
+ this.debugger.setSelected(desc.debugger);
}
}
public LanguageDescription getDescription() {
- return new LanguageDescription(id, this.extensions.getText(), this.syntax.getText(), this.server.getText(), this.name.getText(), this.icon.getText());
+ return new LanguageDescription(id, this.extensions.getText(), this.syntax.getText(), this.server.getText(), this.name.getText(), this.icon.getText(), this.debugger.isSelected());
}
/**
@@ -85,6 +86,7 @@ private void initComponents() {
jLabel6 = new javax.swing.JLabel();
icon = new javax.swing.JTextField();
browseIcon = new javax.swing.JButton();
+ debugger = new javax.swing.JCheckBox();
setLayout(new java.awt.GridBagLayout());
@@ -250,6 +252,16 @@ public void actionPerformed(java.awt.event.ActionEvent evt) {
gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
gridBagConstraints.insets = new java.awt.Insets(12, 12, 94, 12);
add(browseIcon, gridBagConstraints);
+
+ org.openide.awt.Mnemonics.setLocalizedText(debugger, org.openide.util.NbBundle.getMessage(LanguageDescriptionPanel.class, "LanguageDescriptionPanel.debugger.text")); // NOI18N
+ gridBagConstraints = new java.awt.GridBagConstraints();
+ gridBagConstraints.gridx = 0;
+ gridBagConstraints.gridy = 7;
+ gridBagConstraints.gridwidth = 6;
+ gridBagConstraints.ipadx = 6;
+ gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
+ gridBagConstraints.insets = new java.awt.Insets(17, 12, 0, 0);
+ add(debugger, gridBagConstraints);
}// //GEN-END:initComponents
@Messages("DESC_JSONFilter=Grammars (.json, .xml, .tmLanguage)")
@@ -314,6 +326,7 @@ private void browseServerActionPerformed(java.awt.event.ActionEvent evt) {//GEN-
private javax.swing.JButton browseGrammar;
private javax.swing.JButton browseIcon;
private javax.swing.JButton browseServer;
+ private javax.swing.JCheckBox debugger;
private javax.swing.JTextField extensions;
private javax.swing.JTextField icon;
private javax.swing.JLabel jLabel1;
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageServersPanel.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageServersPanel.java
index 9728d7dfc133..8572d2807897 100644
--- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageServersPanel.java
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageServersPanel.java
@@ -38,7 +38,7 @@
final class LanguageServersPanel extends javax.swing.JPanel {
- private static final LanguageDescription PROTOTYPE = new LanguageDescription(null, null, null, null, "MMMMMMMMMMMMMMMMM", null);
+ private static final LanguageDescription PROTOTYPE = new LanguageDescription(null, null, null, null, "MMMMMMMMMMMMMMMMM", null, false);
private final LanguageServersOptionsPanelController controller;
private final DefaultListModel languages;
private final Set usedIds;
diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageStorage.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageStorage.java
index 3b90e4591f5e..a00dfde1909b 100644
--- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageStorage.java
+++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/options/LanguageStorage.java
@@ -33,6 +33,9 @@
import java.util.Set;
import java.util.stream.Collectors;
import javax.swing.event.ChangeEvent;
+import org.eclipse.tm4e.core.registry.IRegistryOptions;
+import org.eclipse.tm4e.core.registry.Registry;
+import org.netbeans.modules.lsp.client.debugger.api.RegisterDAPBreakpoints;
import org.eclipse.tm4e.core.internal.grammar.raw.RawGrammarReader;
import org.eclipse.tm4e.core.registry.IGrammarSource;
import org.netbeans.modules.textmate.lexer.TextmateTokenId;
@@ -152,7 +155,25 @@ static void store(List languages) {
langServer.setAttribute("name", description.name);
}
}
-
+
+ deleteConfigFileIfExists("Editors/" + description.mimeType + "/generic-breakpoints.instance");
+ deleteConfigFileIfExists("Editors/" + description.mimeType + "/GlyphGutterActions/generic-toggle-breakpoint.shadow");
+
+ if (description.debugger) {
+ FileObject genericBreakpoints = FileUtil.createData(FileUtil.getConfigRoot(), "Editors/" + description.mimeType + "/generic-breakpoints.instance");
+
+ genericBreakpoints.setAttribute("instanceOf", RegisterDAPBreakpoints.class.getName());
+ Method newInstance = RegisterDAPBreakpoints.class.getDeclaredMethod("newInstance");
+ genericBreakpoints.setAttribute("methodvalue:instanceCreate", newInstance);
+
+ FileObject genericGutterAction = FileUtil.createData(FileUtil.getConfigRoot(), "Editors/" + description.mimeType + "/GlyphGutterActions/generic-toggle-breakpoint.shadow");
+
+ genericGutterAction.setAttribute("originalFile", "Actions/Debug/org-netbeans-modules-debugger-ui-actions-ToggleBreakpointAction.instance");
+ genericGutterAction.setAttribute("position", 500);
+ } else {
+ //TODO: remove
+ }
+
mimeTypesToClear.remove(description.mimeType);
} catch (Exception ex) {
Exceptions.printStackTrace(ex);
@@ -161,18 +182,11 @@ static void store(List languages) {
for (String mimeType : mimeTypesToClear) {
try {
- FileObject syntax = FileUtil.getConfigFile("Editors/" + mimeType + "/syntax.json");
- if (syntax != null) {
- syntax.delete();
- }
- FileObject langServer = FileUtil.getConfigFile("Editors/" + mimeType + "/org-netbeans-modules-lsp-client-options-GenericLanguageServer.instance");
- if (langServer != null) {
- langServer.delete();
- }
- FileObject loader = FileUtil.getConfigFile("Loaders/" + mimeType + "/Factories/data-object.instance");
- if (loader != null) {
- loader.delete();
- }
+ deleteConfigFileIfExists("Editors/" + mimeType + "/syntax.json");
+ deleteConfigFileIfExists("Editors/" + mimeType + "/org-netbeans-modules-lsp-client-options-GenericLanguageServer.instance");
+ deleteConfigFileIfExists("Loaders/" + mimeType + "/Factories/data-object.instance");
+ deleteConfigFileIfExists("Editors/" + mimeType + "/generic-breakpoints.instance");
+ deleteConfigFileIfExists("Editors/" + mimeType + "/GlyphGutterActions/generic-toggle-breakpoint.shadow");
} catch (Exception ex) {
Exceptions.printStackTrace(ex);
}
@@ -212,6 +226,14 @@ static void store(List languages) {
NbPreferences.forModule(LanguageServersPanel.class).put(KEY, new Gson().toJson(languages));
}
+ private static void deleteConfigFileIfExists(String path) throws IOException {
+ FileObject file = FileUtil.getConfigFile(path);
+
+ if (file != null) {
+ file.delete();
+ }
+ }
+
private static String findScope(File grammar) throws Exception {
return RawGrammarReader.readGrammar(IGrammarSource.fromFile(grammar.toPath())).getScopeName();
}
@@ -225,6 +247,7 @@ public static class LanguageDescription {
public String name;
public String icon;
public String mimeType;
+ public boolean debugger;
public LanguageDescription() {
this.id = null;
@@ -233,16 +256,18 @@ public LanguageDescription() {
this.languageServer = null;
this.name = null;
this.icon = null;
+ this.debugger = false;
this.mimeType = null;
}
- public LanguageDescription(String id, String extensions, String syntaxGrammar, String languageServer, String name, String icon) {
+ public LanguageDescription(String id, String extensions, String syntaxGrammar, String languageServer, String name, String icon, boolean debugger) {
this.id = id;
this.extensions = extensions;
this.syntaxGrammar = syntaxGrammar;
this.languageServer = languageServer;
this.name = name;
this.icon = icon;
+ this.debugger = debugger;
this.mimeType = "text/x-ext-" + id;
}
diff --git a/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/debugger/DebuggerTest.java b/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/debugger/DebuggerTest.java
new file mode 100644
index 000000000000..3718f1f2edb4
--- /dev/null
+++ b/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/debugger/DebuggerTest.java
@@ -0,0 +1,755 @@
+/*
+ * 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.lsp.client.debugger;
+
+import java.io.ByteArrayOutputStream;
+import org.netbeans.modules.lsp.client.debugger.api.DAPConfiguration;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.UncheckedIOException;
+import java.io.Writer;
+import java.lang.ProcessBuilder.Redirect;
+import java.net.Socket;
+import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.TreeSet;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import javax.swing.text.Document;
+import junit.framework.Test;
+import org.eclipse.lsp4j.ConfigurationParams;
+import org.eclipse.lsp4j.InitializeParams;
+import org.eclipse.lsp4j.InitializedParams;
+import org.eclipse.lsp4j.MessageActionItem;
+import org.eclipse.lsp4j.MessageParams;
+import org.eclipse.lsp4j.PublishDiagnosticsParams;
+import org.eclipse.lsp4j.ShowMessageRequestParams;
+import org.eclipse.lsp4j.jsonrpc.Launcher;
+import org.eclipse.lsp4j.launch.LSPLauncher;
+import org.eclipse.lsp4j.services.LanguageClient;
+import org.eclipse.lsp4j.services.LanguageServer;
+import org.junit.Assert;
+import org.netbeans.Main;
+import org.netbeans.api.debugger.ActionsManager;
+import org.netbeans.api.debugger.DebuggerManager;
+import org.netbeans.api.debugger.Session;
+import org.netbeans.editor.AnnotationDesc;
+import org.netbeans.editor.Annotations;
+import org.netbeans.editor.BaseDocument;
+import org.netbeans.junit.Manager;
+import org.netbeans.junit.NbModuleSuite;
+import org.netbeans.junit.NbTestCase;
+import org.netbeans.modules.lsp.client.debugger.breakpoints.DAPLineBreakpoint;
+import org.netbeans.spi.viewmodel.NodeModel;
+import org.netbeans.spi.viewmodel.TableModel;
+import org.netbeans.spi.viewmodel.TreeModel;
+import org.netbeans.spi.viewmodel.UnknownTypeException;
+import org.openide.cookies.EditorCookie;
+import org.openide.filesystems.FileObject;
+import org.openide.filesystems.FileUtil;
+import org.openide.util.Exceptions;
+import org.openide.util.RequestProcessor;
+
+public class DebuggerTest extends NbTestCase {
+
+ private static final String javaLauncher = new File(new File(System.getProperty("java.home"), "bin"), "java").getAbsolutePath();
+ private FileObject project;
+ private FileObject srcDir;
+ private String srcDirURL;
+ private FileObject testFile;
+
+ public DebuggerTest(String name) {
+ super(name);
+ }
+
+ public void testStartDebugger() throws Exception {
+ writeTestFile("""
+ package test; // 1
+ public class Test { // 2
+ public static void main(String... args) { // 3
+ System.err.println(1); // 4
+ nestedPrint("2"); // 5
+ nestedPrint("3"); // 6
+ nestedPrint("4"); // 7
+ nestedPrint("5"); // 8
+ } // 9
+ private static void nestedPrint(String toPrint) { //10
+ System.err.println(toPrint); //11
+ } //12
+ } //13
+ """);
+
+ DebuggerManager manager = DebuggerManager.getDebuggerManager();
+
+ manager.addBreakpoint(DAPLineBreakpoint.create(testFile, 4));
+ DAPLineBreakpoint line6Breakpoint = DAPLineBreakpoint.create(testFile, 6);
+ manager.addBreakpoint(line6Breakpoint);
+ int backendPort = startBackend();
+ Socket socket = new Socket("localhost", backendPort);
+ DAPConfiguration.create(socket.getInputStream(), socket.getOutputStream())
+ .addConfiguration(Map.of("type", "java+",
+ "request", "launch",
+ "file", FileUtil.toFile(testFile).getAbsolutePath(),
+ "classPaths", List.of("any")))
+ .launch();
+ waitFor(true, () -> DebuggerManager.getDebuggerManager().getSessions().length > 0);
+ assertEquals(1, DebuggerManager.getDebuggerManager().getSessions().length);
+ Session session = DebuggerManager.getDebuggerManager().getSessions()[0];
+ assertNotNull(session);
+ ActionsManager am = session.getCurrentEngine().getActionsManager();
+ //wait until it stops at breakpoint:
+ waitFor(START_TIMEOUT, List.of("4: CurrentPC"), () -> readAnnotations());
+
+ //step over a statement:
+ waitFor(true, () -> am.isEnabled(ActionsManager.ACTION_STEP_OVER));
+ am.postAction(ActionsManager.ACTION_STEP_OVER);
+
+ //wait until it stops after the step:
+ waitFor(List.of("5: CurrentPC"), () -> readAnnotations());
+
+ //step into the method
+ waitFor(true, () -> am.isEnabled(ActionsManager.ACTION_STEP_INTO));
+ am.postAction(ActionsManager.ACTION_STEP_INTO);
+
+ //wait until it stops:
+ waitFor(List.of("5: CallSite", "11: CurrentPC"), () -> readAnnotations());
+ //and verify Variables view contain an expected variable, with an expected value:
+ waitFor("Local/toPrint:String:\"2\"", () -> getVariableNameTypeValue(session, "Local/toPrint"));
+
+ //tweak breakpoints:
+ manager.removeBreakpoint(line6Breakpoint);
+ manager.addBreakpoint(DAPLineBreakpoint.create(testFile, 7));
+ //continue to debugging - should finish at line 7, not 6:
+ waitFor(true, () -> am.isEnabled(ActionsManager.ACTION_CONTINUE));
+ am.postAction(ActionsManager.ACTION_CONTINUE);
+
+ //wait until it stops after the step:
+ waitFor(List.of("7: CurrentPC"), () -> readAnnotations());
+
+ //continue to finish debugging:
+ waitFor(true, () -> am.isEnabled(ActionsManager.ACTION_CONTINUE));
+ am.postAction(ActionsManager.ACTION_CONTINUE);
+
+ //verify things are cleaned up:
+ waitFor(0, () -> DebuggerManager.getDebuggerManager().getSessions().length);
+ waitFor(List.of(), () -> readAnnotations());
+ }
+
+ public void testStopDebuggerAndBreakpointConditions() throws Exception {
+ writeTestFile("""
+ package test; // 1
+ public class Test { // 2
+ public static void main(String... args) { // 3
+ System.err.println(1); // 4
+ nestedPrint("2"); // 5
+ nestedPrint("3"); // 6
+ nestedPrint("4"); // 7
+ nestedPrint("5"); // 8
+ } // 9
+ private static void nestedPrint(String toPrint) { //10
+ System.err.println(toPrint); //11
+ } //12
+ } //13
+ """);
+
+ DebuggerManager manager = DebuggerManager.getDebuggerManager();
+ DAPLineBreakpoint breakpoint = DAPLineBreakpoint.create(testFile, 11);
+
+ breakpoint.setCondition("\"4\".equals(toPrint)");
+ manager.addBreakpoint(breakpoint);
+ int backendPort = startBackend();
+ Socket socket = new Socket("localhost", backendPort);
+ DAPConfiguration.create(socket.getInputStream(), socket.getOutputStream())
+ .addConfiguration(Map.of("type", "java+",
+ "request", "launch",
+ "file", FileUtil.toFile(testFile).getAbsolutePath(),
+ "classPaths", List.of("any")))
+ .launch();
+ waitFor(true, () -> DebuggerManager.getDebuggerManager().getSessions().length > 0);
+ assertEquals(1, DebuggerManager.getDebuggerManager().getSessions().length);
+ Session session = DebuggerManager.getDebuggerManager().getSessions()[0];
+ assertNotNull(session);
+ ActionsManager am = session.getCurrentEngine().getActionsManager();
+ //wait until it stops at breakpoint:
+ waitFor(START_TIMEOUT, List.of("7: CallSite", "11: CurrentPC"), () -> readAnnotations());
+
+ //step over a statement:
+ waitFor(true, () -> am.isEnabled(ActionsManager.ACTION_KILL));
+ am.postAction(ActionsManager.ACTION_KILL);
+
+ //verify things are cleaned up:
+ waitFor(0, () -> DebuggerManager.getDebuggerManager().getSessions().length);
+ waitFor(List.of(), () -> readAnnotations());
+ }
+
+ private void writeTestFile(String code) throws IOException {
+ project = FileUtil.createFolder(new File(getWorkDir(), "prj"));
+ srcDir = FileUtil.createFolder(project, "src/main/java");
+ srcDirURL = srcDir.toURL().toString();
+ testFile = FileUtil.createData(srcDir, "test/Test.java");
+ try (OutputStream out = testFile.getOutputStream();
+ Writer w = new OutputStreamWriter(out)) {
+ w.write(code);
+ }
+ try (OutputStream out = FileUtil.createData(project, "pom.xml").getOutputStream();
+ Writer w = new OutputStreamWriter(out)) {
+ w.write("""
+
+
+ 4.0.0
+ test
+ test
+ 1.0-SNAPSHOT
+ jar
+
+ UTF-8
+ 17
+
+
+ """);
+ }
+ }
+
+ private static final int DEFAULT_TIMEOUT = 10_000;
+// private static final int DEFAULT_TIMEOUT = 1_000_000; //for debugging
+ private static final int START_TIMEOUT = Math.max(60_000, DEFAULT_TIMEOUT);
+ private static final int DELAY = 100;
+
+ private void waitFor(T expectedValue, Supplier actualValue) {
+ waitFor(DEFAULT_TIMEOUT, expectedValue, actualValue);
+ }
+
+ private void waitFor(int timeout, T expectedValue, Supplier actualValue) {
+ long s = System.currentTimeMillis();
+ T lastActualvalue = null;
+
+ while (true) {
+ if (Objects.equals(lastActualvalue = actualValue.get(), expectedValue)) {
+ return ;
+ }
+ if ((System.currentTimeMillis() - s) > timeout) {
+ break;
+ }
+ try {
+ Thread.sleep(DELAY);
+ } catch (InterruptedException ex) {
+ Exceptions.printStackTrace(ex);
+ }
+ }
+
+ fail("Didn't finish in time, last actual value: " + lastActualvalue);
+ }
+
+ private List readAnnotations() {
+ List result = new ArrayList<>();
+
+ assertNotNull(testFile);
+ EditorCookie ec = testFile.getLookup().lookup(EditorCookie.class);
+ Document doc = ec.getDocument();
+
+ if (doc == null) {
+ return result;
+ }
+
+ Annotations annos = ((BaseDocument) doc).getAnnotations();
+ int currentLine = -1;
+
+ while (true) {
+ int prevLine = currentLine;
+
+ currentLine = annos.getNextLineWithAnnotation(currentLine + 1);
+
+ if (currentLine == prevLine + 1) {
+ break;
+ }
+
+ List annotations = new ArrayList<>();
+ AnnotationDesc active = annos.getActiveAnnotation(currentLine);
+
+ if (active != null) {
+ annotations.add(active);
+ }
+
+ AnnotationDesc[] passive = annos.getPassiveAnnotationsForLine(currentLine);
+
+ if (passive != null) {
+ annotations.addAll(Arrays.asList(passive));
+ }
+
+ if (annotations.isEmpty()) {
+ break;
+ }
+
+ result.add("" + (currentLine + 1) + ": " + annotations.stream().map(desc -> desc.getAnnotationType()).collect(Collectors.joining(", ")));
+ }
+
+ return result;
+ }
+
+ private String getVariableNameTypeValue(Session session, String variablePath) {
+ try {
+ TreeModel variablesTree = session.lookupFirst("LocalsView", TreeModel.class);
+ Element found = findTreeNode(variablesTree, variablePath);
+ TableModel variablesTable = (TableModel) variablesTree;
+
+ if (found == null) {
+ return "";
+ }
+
+ return found.path + ":" +
+ variablesTable.getValueAt(found.key, "LocalsType") + ":" +
+ variablesTable.getValueAt(found.key, "LocalsValue");
+ } catch (UnknownTypeException ex) {
+ throw new AssertionError(ex);
+ }
+ }
+
+
+ private Element findTreeNode(TreeModel treeModel, String findPath) {
+ try {
+ NodeModel nodeModel = (NodeModel) treeModel;
+ List todo = new ArrayList<>();
+ todo.add(new Element("", treeModel.getRoot()));
+ while (!todo.isEmpty()) {
+ Element current = todo.remove(0);
+ if (findPath.equals(current.path)) {
+ return current;
+ }
+ int childrenCount = treeModel.getChildrenCount(current.key);
+ Object[] children = treeModel.getChildren(current.key, 0, childrenCount);
+ for (Object child : children) {
+ String displayName = nodeModel.getDisplayName(child);
+ String path = current.path.isEmpty() ? displayName
+ : current.path + "/" + displayName;
+
+ todo.add(new Element(path, child));
+ }
+ }
+ } catch (UnknownTypeException ex) {
+ throw new AssertionError(ex);
+ }
+ return null;
+ }
+
+ record Element(String path, Object key) {}
+
+ private static File toFile(URI uri) {
+ return Paths.get(uri).toFile();
+ }
+
+ private static final Pattern PORT = Pattern.compile("Listening for transport dt_socket at address: ([0-9]+)\n");
+ private int startDebugee() throws Exception {
+ //XXX: should not use a hard-coded port
+ Process p = new ProcessBuilder(javaLauncher, "-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address=0", FileUtil.toFile(testFile).getAbsolutePath())
+ .inheritIO()
+ .redirectOutput(Redirect.PIPE)
+ .start();
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+ p.destroyForcibly();
+ }));
+ CountDownLatch portFound = new CountDownLatch(1);
+ AtomicInteger port = new AtomicInteger();
+ new Thread(() -> {
+ InputStream in = p.getInputStream();
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ int r;
+ try {
+ while ((r = in.read()) != (-1)) {
+ output.write(r);
+ System.out.write(r);
+ Matcher m = PORT.matcher(new String(output.toByteArray()));
+ if (m.find()) {
+ port.set(Integer.parseInt(m.group(1)));
+ portFound.countDown();
+ break;
+ }
+ }
+ while ((r = in.read()) != (-1)) {
+ System.out.write(r);
+ }
+ } catch (IOException ex) {
+ ex.printStackTrace();
+ }
+ }).start();
+ if (!portFound.await(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS)) {
+ p.destroyForcibly();
+ throw new IllegalStateException("Didn't detect port before timeout.");
+ }
+ return port.get();
+ }
+
+ public static Test suite() {
+ return NbModuleSuite.create(NbModuleSuite.createConfiguration(DebuggerTest.class)
+ .enableModules(".*").clusters("platform|ide").gui(false));
+ }
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ clearWorkDir();
+ getWorkDir();
+ }
+
+//
+ private static final Pattern DAP_PORT = Pattern.compile("Java Debug Server Adapter listening at port ([0-9]+)\n");
+ private static final Pattern LSP_PORT = Pattern.compile("Java Language Server listening at port ([0-9]+)\n");
+
+ private static int backendPort = -1;
+
+ private int startBackend() throws Exception {
+ if (backendPort == (-1)) {
+ backendPort = doStartBackend();
+ }
+
+ return backendPort;
+ }
+
+ private static int doStartBackend() throws Exception {
+ List options = new ArrayList<>();
+ options.add(javaLauncher);
+ options.add("--add-opens=java.base/java.net=ALL-UNNAMED");
+
+ File platform = findPlatform();
+ List bootCP = new ArrayList<>();
+ List dirs = new ArrayList<>();
+ dirs.add(new File(platform, "lib"));
+
+ File jdkHome = new File(System.getProperty("java.home"));
+ if (new File(jdkHome.getParentFile(), "lib").exists()) {
+ jdkHome = jdkHome.getParentFile();
+ }
+ dirs.add(new File(jdkHome, "lib"));
+
+ //in case we're running code coverage, load the coverage libraries
+ if (System.getProperty("code.coverage.classpath") != null) {
+ dirs.add(new File(System.getProperty("code.coverage.classpath")));
+ }
+
+ for (File dir: dirs) {
+ File[] jars = dir.listFiles();
+ if (jars != null) {
+ for (File jar : jars) {
+ if (jar.getName().endsWith(".jar")) {
+ bootCP.add(jar);
+ }
+ }
+ }
+ }
+
+ options.add("-cp"); options.add(bootCP.stream().map(jar -> jar.getAbsolutePath()).collect(Collectors.joining(System.getProperty("path.separator"))));
+
+ options.add("-Djava.util.logging.config=-");
+ options.add("-Dnetbeans.logger.console=true");
+ options.add("-Dnetbeans.logger.noSystem=true");
+ options.add("-Dnetbeans.home=" + platform.getPath());
+ options.add("-Dnetbeans.full.hack=true");
+ options.add("-DTopSecurityManager.disable=true");
+
+ String branding = System.getProperty("branding.token"); // NOI18N
+ if (branding != null) {
+ options.add("-Dbranding.token=" + branding);
+ }
+
+ File ud = new File(new File(Manager.getWorkDirPath()), "userdir");
+
+ deleteRecursivelly(ud);
+
+ ud.mkdirs();
+
+ options.add("-Dnetbeans.user=" + ud.getPath());
+
+ StringBuilder sb = new StringBuilder();
+ String sep = "";
+ for (File f : findClusters()) {
+ if (f.getPath().endsWith("ergonomics")) {
+ continue;
+ }
+ sb.append(sep);
+ sb.append(f.getPath());
+ sep = File.pathSeparator;
+ }
+ options.add("-Dnetbeans.dirs=" + sb.toString());
+
+ options.add("-Dnetbeans.security.nocheck=true");
+
+// options.add("-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address=8000");
+
+ options.add(Main.class.getName());
+ options.add("--nosplash");
+ options.add("--nogui");
+
+ options.add("--start-java-language-server=listen:0");
+ options.add("--start-java-debug-adapter-server=listen:0");
+
+ Process p = new ProcessBuilder(options).redirectError(Redirect.INHERIT).start();
+
+ Runtime.getRuntime().addShutdownHook(new Thread() {
+ @Override
+ public void run() {
+ p.destroyForcibly();
+ }
+ });
+
+ ByteArrayOutputStream outData = new ByteArrayOutputStream();
+ new RequestProcessor(DebuggerTest.class.getName(), 1, false, false).post(() -> {
+ try {
+ InputStream in = p.getInputStream();
+ int r;
+ while ((r = in.read()) != (-1)) {
+ synchronized (outData) {
+ outData.write(r);
+ outData.notifyAll();
+ }
+ }
+ } catch (IOException ex) {
+ throw new UncheckedIOException(ex);
+ }
+ synchronized (outData) {
+ outData.notifyAll();
+ }
+ });
+
+ synchronized (outData) {
+ int backendPort = (-1);
+ boolean lspServerConnected = false;
+
+ while (p.isAlive()) {
+ Matcher dapMatcher = DAP_PORT.matcher(new String(outData.toByteArray()));
+ if (dapMatcher.find()) {
+ backendPort = Integer.parseInt(dapMatcher.group(1));
+ }
+ Matcher lspMatcher = LSP_PORT.matcher(new String(outData.toByteArray()));
+ if (lspMatcher.find()) {
+ //must connect a (dummy) LSP client, so that the Java debugger's "launch" works:
+ Socket lspSocket = new Socket("localhost", Integer.parseInt(lspMatcher.group(1)));
+ Launcher serverLauncher = LSPLauncher.createClientLauncher(new LanguageClient() {
+ @Override
+ public void telemetryEvent(Object object) {}
+ @Override
+ public void publishDiagnostics(PublishDiagnosticsParams diagnostics) {}
+ @Override
+ public void showMessage(MessageParams messageParams) {}
+ @Override
+ public CompletableFuture showMessageRequest(ShowMessageRequestParams requestParams) {
+ throw new UnsupportedOperationException("Not supported yet.");
+ }
+
+ @Override
+ public void logMessage(MessageParams message) {}
+
+ @Override
+ public CompletableFuture> configuration(ConfigurationParams configurationParams) {
+ CompletableFuture> result = new CompletableFuture<>();
+ result.complete(List.of());
+ return result;
+ }
+
+ }, lspSocket.getInputStream(), lspSocket.getOutputStream());
+ serverLauncher.startListening();
+ serverLauncher.getRemoteProxy().initialize(new InitializeParams()).get();
+ serverLauncher.getRemoteProxy().initialized(new InitializedParams());
+ lspServerConnected = true;
+ }
+ if (lspServerConnected && backendPort != (-1)) {
+ return backendPort;
+ }
+ outData.wait();
+ }
+ }
+
+ throw new AssertionError("Cannot start backend");
+ }
+
+ static File findPlatform() {
+ String clusterPath = System.getProperty("cluster.path.final"); // NOI18N
+ if (clusterPath != null) {
+ for (String piece : tokenizePath(clusterPath)) {
+ File d = new File(piece);
+ if (d.getName().matches("platform\\d*")) {
+ return d;
+ }
+ }
+ }
+ String allClusters = System.getProperty("all.clusters"); // #194794
+ if (allClusters != null) {
+ File d = new File(allClusters, "platform"); // do not bother with old numbered variants
+ if (d.isDirectory()) {
+ return d;
+ }
+ }
+ try {
+ Class> lookup = Class.forName("org.openide.util.Lookup"); // NOI18N
+ File util = toFile(lookup.getProtectionDomain().getCodeSource().getLocation().toURI());
+ Assert.assertTrue("Util exists: " + util, util.exists());
+
+ return util.getParentFile().getParentFile();
+ } catch (Exception ex) {
+ try {
+ File nbjunit = toFile(NbModuleSuite.class.getProtectionDomain().getCodeSource().getLocation().toURI());
+ File harness = nbjunit.getParentFile().getParentFile();
+ Assert.assertEquals(nbjunit + " is in a folder named 'harness'", "harness", harness.getName());
+ TreeSet sorted = new TreeSet();
+ for (File p : harness.getParentFile().listFiles()) {
+ if (p.getName().startsWith("platform")) {
+ sorted.add(p);
+ }
+ }
+ Assert.assertFalse("Platform shall be found in " + harness.getParent(), sorted.isEmpty());
+ return sorted.last();
+ } catch (Exception ex2) {
+ Assert.fail("Cannot find utilities JAR: " + ex + " and: " + ex2);
+ }
+ return null;
+ }
+ }
+
+ private static File[] findClusters() throws IOException {
+ Collection clusters = new LinkedHashSet();
+
+ //not apisupport, so that the apisupport project do not recognize the test workdirs, so that the multi-source support can work on it:
+ findClusters(clusters, List.of("platform|ide|extide|java"));
+
+ return clusters.toArray(new File[0]);
+ }
+
+ private static String[] tokenizePath(String path) {
+ List l = new ArrayList();
+ StringTokenizer tok = new StringTokenizer(path, ":;", true); // NOI18N
+ char dosHack = '\0';
+ char lastDelim = '\0';
+ int delimCount = 0;
+ while (tok.hasMoreTokens()) {
+ String s = tok.nextToken();
+ if (s.length() == 0) {
+ // Strip empty components.
+ continue;
+ }
+ if (s.length() == 1) {
+ char c = s.charAt(0);
+ if (c == ':' || c == ';') {
+ // Just a delimiter.
+ lastDelim = c;
+ delimCount++;
+ continue;
+ }
+ }
+ if (dosHack != '\0') {
+ // #50679 - "C:/something" is also accepted as DOS path
+ if (lastDelim == ':' && delimCount == 1 && (s.charAt(0) == '\\' || s.charAt(0) == '/')) {
+ // We had a single letter followed by ':' now followed by \something or /something
+ s = "" + dosHack + ':' + s;
+ // and use the new token with the drive prefix...
+ } else {
+ // Something else, leave alone.
+ l.add(Character.toString(dosHack));
+ // and continue with this token too...
+ }
+ dosHack = '\0';
+ }
+ // Reset count of # of delimiters in a row.
+ delimCount = 0;
+ if (s.length() == 1) {
+ char c = s.charAt(0);
+ if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
+ // Probably a DOS drive letter. Leave it with the next component.
+ dosHack = c;
+ continue;
+ }
+ }
+ l.add(s);
+ }
+ if (dosHack != '\0') {
+ //the dosHack was the last letter in the input string (not followed by the ':')
+ //so obviously not a drive letter.
+ //Fix for issue #57304
+ l.add(Character.toString(dosHack));
+ }
+ return l.toArray(new String[0]);
+ }
+
+ static void findClusters(Collection clusters, List regExps) throws IOException {
+ File plat = findPlatform().getCanonicalFile();
+ String selectiveClusters = System.getProperty("cluster.path.final"); // NOI18N
+ Set path;
+ if (selectiveClusters != null) {
+ path = new TreeSet();
+ for (String p : tokenizePath(selectiveClusters)) {
+ File f = new File(p);
+ path.add(f.getCanonicalFile());
+ }
+ } else {
+ File parent;
+ String allClusters = System.getProperty("all.clusters"); // #194794
+ if (allClusters != null) {
+ parent = new File(allClusters);
+ } else {
+ parent = plat.getParentFile();
+ }
+ path = new TreeSet(Arrays.asList(parent.listFiles()));
+ }
+ for (String c : regExps) {
+ for (File f : path) {
+ if (f.equals(plat)) {
+ continue;
+ }
+ if (!f.getName().matches(c)) {
+ continue;
+ }
+ File m = new File(new File(f, "config"), "Modules");
+ if (m.exists()) {
+ clusters.add(f);
+ }
+ }
+ }
+ }
+
+ private static void deleteRecursivelly(File ud) {
+ if (ud.isDirectory()) {
+ File[] list = ud.listFiles();
+
+ if (list != null) {
+ for (File c : list) {
+ deleteRecursivelly(c);
+ }
+ }
+ }
+
+ ud.delete();
+ }
+//
+}
diff --git a/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/options/LanguageStorageTest.java b/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/options/LanguageStorageTest.java
index 46bad5ae7065..8cd5eea1b9c3 100644
--- a/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/options/LanguageStorageTest.java
+++ b/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/options/LanguageStorageTest.java
@@ -93,7 +93,7 @@ public FileObject findResource(String name) {
DataObject testDO = DataObject.find(testFO);
assertEquals("org.openide.loaders.DefaultDataObject", testDO.getClass().getName());
- LanguageStorage.store(Arrays.asList(new LanguageDescription("t", "txt", FileUtil.toFile(grammar).getAbsolutePath(), null, "txt", null)));
+ LanguageStorage.store(Arrays.asList(new LanguageDescription("t", "txt", FileUtil.toFile(grammar).getAbsolutePath(), null, "txt", null, false)));
assertEquals("text/x-ext-t", FileUtil.getMIMEType(testFO));
DataObject recognized = DataObject.find(testFO);
@@ -107,7 +107,7 @@ public FileObject findResource(String name) {
Language> l = MimeLookup.getLookup("text/x-ext-t").lookup(Language.class);
assertNotNull(l);
- LanguageStorage.store(Arrays.asList(new LanguageDescription("t", "txt", FileUtil.toFile(grammar).getAbsolutePath(), null, "txt", null)));
+ LanguageStorage.store(Arrays.asList(new LanguageDescription("t", "txt", FileUtil.toFile(grammar).getAbsolutePath(), null, "txt", null, false)));
LanguageStorage.store(Collections.emptyList());
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/ConnectionSpec.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/ConnectionSpec.java
index 9ab389564cc2..bb8a4e818303 100644
--- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/ConnectionSpec.java
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/ConnectionSpec.java
@@ -129,7 +129,7 @@ public void run() {
}
};
listeningThread.start();
- out.write((prefix + " listening at port " + localPort).getBytes());
+ out.write((prefix + " listening at port " + localPort + "\n").getBytes());
out.flush();
} else {
// connect to TCP
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/Utils.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/Utils.java
index f9fcc69431e1..ec8f1da4627a 100644
--- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/Utils.java
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/Utils.java
@@ -597,4 +597,8 @@ public static Predicate codeActionKindFilter(List only) {
only.stream()
.anyMatch(o -> k.equals(o) || k.startsWith(o + "."));
}
+
+ public static boolean wrappedBoolean2Boolean(Boolean b, boolean defaultValue) {
+ return b != null ? b : defaultValue;
+ }
}
diff --git a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/breakpoints/NbBreakpointsRequestHandler.java b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/breakpoints/NbBreakpointsRequestHandler.java
index 5de0d9a87232..6d63a47f4272 100644
--- a/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/breakpoints/NbBreakpointsRequestHandler.java
+++ b/java/java.lsp.server/src/org/netbeans/modules/java/lsp/server/debugging/breakpoints/NbBreakpointsRequestHandler.java
@@ -37,6 +37,7 @@
import org.eclipse.lsp4j.debug.SourceBreakpoint;
import org.eclipse.lsp4j.jsonrpc.messages.ResponseErrorCode;
import org.netbeans.modules.java.lsp.server.URITranslator;
+import org.netbeans.modules.java.lsp.server.Utils;
import org.netbeans.modules.java.lsp.server.debugging.DebugAdapterContext;
import org.netbeans.modules.java.lsp.server.debugging.utils.ErrorUtilities;
@@ -80,7 +81,7 @@ public CompletableFuture setBreakpoints(SetBreakpointsAr
List res = new ArrayList<>();
NbBreakpoint[] toAdds = this.convertClientBreakpointsToDebugger(source, sourcePath, arguments.getBreakpoints(), context);
// Decode the URI if it comes encoded:
- NbBreakpoint[] added = context.getBreakpointManager().setBreakpoints(decodeURI(sourcePath), toAdds, arguments.getSourceModified());
+ NbBreakpoint[] added = context.getBreakpointManager().setBreakpoints(decodeURI(sourcePath), toAdds, Utils.wrappedBoolean2Boolean(arguments.getSourceModified(), false));
for (int i = 0; i < arguments.getBreakpoints().length; i++) {
// For newly added breakpoint, should install it to debuggee first.
if (toAdds[i] == added[i]) {
diff --git a/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/ConnectionSpecTest.java b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/ConnectionSpecTest.java
index 1035cf9bc89a..f538c953f63b 100644
--- a/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/ConnectionSpecTest.java
+++ b/java/java.lsp.server/test/unit/src/org/netbeans/modules/java/lsp/server/ConnectionSpecTest.java
@@ -121,7 +121,7 @@ public void testParseListenAndConnect() throws Exception {
String reply = os.toString("UTF-8");
String exp = "Pipe server listening at port ";
assertTrue(reply, reply.startsWith(exp));
- int port = Integer.parseInt(reply.substring(exp.length()));
+ int port = Integer.parseInt(reply.substring(exp.length(), reply.indexOf('\n', exp.length())));
assertTrue("port is specified: " + port, port >= 1024);
try (ConnectionSpec second = ConnectionSpec.parse("connect:" + port)) {
second.prepare("Pipe client", in, os, new LspSession(), ConnectionSpecTest::setCopy, ConnectionSpecTest::copy);