From a91a5f279a72a4f56af1c0d4c0164ccab41a3e9b Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Sun, 29 Dec 2024 08:00:15 +0100 Subject: [PATCH 01/11] Extending lsp.client with support for DAP --- ide/lsp.client/nbproject/project.properties | 2 +- ide/lsp.client/nbproject/project.xml | 29 + ...ns.modules.lsp.client.debugger.DAPDebugger | 1 + ...tbeans.spi.debugger.DebuggerEngineProvider | 1 + .../client/debugger/DAPActionsProvider.java | 156 ++++ .../debugger/DAPConfigurationAccessor.java | 49 ++ .../lsp/client/debugger/DAPDebugger.java | 575 +++++++++++++ .../debugger/DAPDebuggerEngineProvider.java | 56 ++ .../modules/lsp/client/debugger/DAPFrame.java | 130 +++ .../lsp/client/debugger/DAPThread.java | 202 +++++ .../lsp/client/debugger/DAPVariable.java | 103 +++ .../client/debugger/DebuggerAnnotation.java | 72 ++ .../client/debugger/LineBreakpointData.java | 22 + .../RegisterDAPDebuggerProcessor.java | 56 ++ .../lsp/client/debugger/SPIAccessor.java | 42 + .../modules/lsp/client/debugger/Utils.java | 127 +++ .../client/debugger/api/DAPConfiguration.java | 144 ++++ .../debugger/api/RegisterDAPBreakpoints.java | 38 + .../debugger/api/RegisterDAPDebugger.java | 40 + .../client/debugger/attach/Bundle.properties | 9 + .../debugger/attach/DAPAttachPanel.form | 186 +++++ .../debugger/attach/DAPAttachPanel.java | 246 ++++++ .../client/debugger/attach/DAPAttachType.java | 141 ++++ .../BreakpointAnnotationProvider.java | 270 +++++++ .../debugger/breakpoints/BreakpointModel.java | 159 ++++ .../breakpoints/BreakpointsReader.java | 117 +++ .../debugger/breakpoints/Bundle.properties | 18 + .../DAPBreakpointActionProvider.java | 144 ++++ .../breakpoints/DAPBreakpointConvertor.java | 35 + .../breakpoints/DAPLineBreakpoint.java | 335 ++++++++ .../DebuggerBreakpointAnnotation.java | 157 ++++ .../breakpoints/PersistenceManager.java | 168 ++++ .../DebuggingActionsProvider.java | 222 +++++ .../debuggingview/DebuggingModel.java | 380 +++++++++ .../debugger/models/CallStackModel.java | 374 +++++++++ .../debugger/models/CurrentFrameTracker.java | 98 +++ .../debugger/models/VariablesModel.java | 328 ++++++++ .../client/debugger/models/WatchesModel.java | 392 +++++++++ .../debugger/spi/BreakpointConvertor.java | 51 ++ .../debugger/views/DAPComponentsProvider.java | 101 +++ .../lsp/client/options/Bundle.properties | 1 + .../options/LanguageDescriptionPanel.form | 12 + .../options/LanguageDescriptionPanel.java | 15 +- .../client/options/LanguageServersPanel.java | 2 +- .../lsp/client/options/LanguageStorage.java | 51 +- .../lsp/client/debugger/DebuggerTest.java | 757 ++++++++++++++++++ .../client/options/LanguageStorageTest.java | 4 +- .../java/lsp/server/ConnectionSpec.java | 2 +- .../modules/java/lsp/server/Utils.java | 4 + .../NbBreakpointsRequestHandler.java | 3 +- 50 files changed, 6606 insertions(+), 21 deletions(-) create mode 100644 ide/lsp.client/src/META-INF/debugger/DAPDebuggerSession/org.netbeans.modules.lsp.client.debugger.DAPDebugger create mode 100644 ide/lsp.client/src/META-INF/debugger/DAPDebuggerSession/org.netbeans.spi.debugger.DebuggerEngineProvider create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPActionsProvider.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPConfigurationAccessor.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPDebugger.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPDebuggerEngineProvider.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPFrame.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPThread.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPVariable.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DebuggerAnnotation.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/LineBreakpointData.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/RegisterDAPDebuggerProcessor.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/SPIAccessor.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/Utils.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/api/DAPConfiguration.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/api/RegisterDAPBreakpoints.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/api/RegisterDAPDebugger.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/attach/Bundle.properties create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/attach/DAPAttachPanel.form create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/attach/DAPAttachPanel.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/attach/DAPAttachType.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/BreakpointAnnotationProvider.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/BreakpointModel.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/BreakpointsReader.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/Bundle.properties create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/DAPBreakpointActionProvider.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/DAPBreakpointConvertor.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/DAPLineBreakpoint.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/DebuggerBreakpointAnnotation.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/PersistenceManager.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/debuggingview/DebuggingActionsProvider.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/debuggingview/DebuggingModel.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/models/CallStackModel.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/models/CurrentFrameTracker.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/models/VariablesModel.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/models/WatchesModel.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/spi/BreakpointConvertor.java create mode 100644 ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/views/DAPComponentsProvider.java create mode 100644 ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/debugger/DebuggerTest.java 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..71a2400ebd5b --- /dev/null +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPActionsProvider.java @@ -0,0 +1,156 @@ +/* + * 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(); +// } else +// if (action == ActionsManager.ACTION_START) { +// return ; +// } else +// if ( action == ActionsManager.ACTION_STEP_INTO || +// action == ActionsManager.ACTION_STEP_OUT || +// action == ActionsManager.ACTION_STEP_OVER +// ) { +// debugger.doStep(action); + } + } + + @Override + public void postAction(final Object action, final Runnable actionPerformedNotifier) { +// if (action == ActionsManager.ACTION_KILL) { +// synchronized (DAPDebugger.class) { +// if (killRequestProcessor == null) { +// killRequestProcessor = new RequestProcessor("CPPLite debugger finish RP", 1); +// } +// } +// killRequestProcessor.post(new Runnable() { +// @Override +// public void run() { +// try { +// doAction(action); +// } finally { +// actionPerformedNotifier.run(); +// } +// } +// }); +// return ; +// } + 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..d2ab6cb4c061 --- /dev/null +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPDebugger.java @@ -0,0 +1,575 @@ +/* + * 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.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; + +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 URLPathConvertor 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.url()) + .collect(Collectors.toSet()); + + try { + setBreakpoints(d -> modifiedURLs.contains(d.url())); + } 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.url()); + + 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.covert(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? + Utils.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(); + Utils.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); + 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 URLPathConvertor DEFAULT_CONVERTOR = new URLPathConvertor() { + @Override + public String toPath(String file) { + if (file.startsWith("file:")) { + return URI.create(file).getPath(); + } + + return null; + } + + @Override + public String toURL(String path) { + return "file:" + path; + } + }; + + public interface URLPathConvertor { + public String toPath(String url); + public String toURL(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..c8c6c53df25d --- /dev/null +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPFrame.java @@ -0,0 +1,130 @@ +/* + * 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.URLPathConvertor; +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 URLPathConvertor fileConvertor; + private final DAPThread thread; + private final StackFrame frame; + + public DAPFrame(URLPathConvertor 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() { + try { + //XXX: frame.getSource().getPath() may not work(!) + if (frame.getSource() == null || frame.getSource().getPath() == null) { + return null; + } + return new URI(fileConvertor.toURL(frame.getSource().getPath())); + } catch (URISyntaxException ex) { + return null; + } + } + + @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/DAPThread.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPThread.java new file mode 100644 index 000000000000..a9acbf2a1b0a --- /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) { + Utils.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); + + Utils.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..390c95a39b9f --- /dev/null +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/LineBreakpointData.java @@ -0,0 +1,22 @@ +/* + * 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; + +public record LineBreakpointData(String url, 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 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/Utils.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/Utils.java new file mode 100644 index 000000000000..ef8293bc922f --- /dev/null +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/Utils.java @@ -0,0 +1,127 @@ +/* + * 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.logging.Logger; +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 Utils { + + private static final Logger logger = Logger.getLogger(Utils.class.getName()); + + private static Object currentLine; + + private Utils() { + } + + static synchronized void markCurrent (final Object line) { + unmarkCurrent (); + + Annotatable[] annotatables = (Annotatable[]) line; + 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] + ); + currentLine = annotations; + + showLine (line); + } + + static synchronized void unmarkCurrent () { + if (currentLine != null) { + +// ((DebuggerAnnotation) currentLine).detach (); + int i, k = ((DebuggerAnnotation[]) currentLine).length; + for (i = 0; i < k; i++) { + ((DebuggerAnnotation[]) currentLine) [i].detach (); + } + currentLine = null; + } + } + + public static void showLine (final Object line) { + final Annotatable[] a = (Annotatable[]) line; + 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()); + } + } + }); + } + + static int getLineNumber (Object line) { + final Annotatable[] a = (Annotatable[]) line; + if (a [0] instanceof Line) + return ((Line) a [0]).getLineNumber (); + else + if (a [0] instanceof Line.Part) + return ((Line.Part) a [0]).getLine ().getLineNumber (); + else + throw new InternalError (); + } + + 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/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..5107ba81bc10 --- /dev/null +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/attach/Bundle.properties @@ -0,0 +1,9 @@ +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..289c05fcd16b --- /dev/null +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/attach/DAPAttachPanel.form @@ -0,0 +1,186 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 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..ef53e2d91c53 --- /dev/null +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/BreakpointAnnotationProvider.java @@ -0,0 +1,270 @@ +/* + * 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.HashSet; +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.WeakListeners; +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 Set dataObjectListeners; + 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(WeakListeners.propertyChange(pchl, dobj)); + synchronized (this) { + if (dataObjectListeners == null) { + dataObjectListeners = new HashSet<>(); + } + // Prevent from GC. + dataObjectListeners.add(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..c39713043081 --- /dev/null +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/BreakpointModel.java @@ -0,0 +1,159 @@ +/* + * 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.Utils; + +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(); + if (fileObject != null) { + nameExt = fileObject.getNameExt(); + } else { + File file = new File(breakpoint.getFilePath()); + nameExt = file.getName(); + } + 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 && + Utils.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.getFilePath() + ":" + 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..cad02b4068c7 --- /dev/null +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/BreakpointsReader.java @@ -0,0 +1,117 @@ +/* + * 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); + if (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); + } else { + String filePath = properties.getString ("filePath", null); + if (filePath == null) { + return null; + } + b = DAPLineBreakpoint.create(filePath, 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(); + if (fo != null) { + properties.setString("url", fo.toURL().toString()); + } + properties.setString("filePath", b.getFilePath()); + 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..053f07b2a1b5 --- /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..03ca7a5ca7f9 --- /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 covert(Breakpoint b, ConvertedBreakpointConsumer breakpointConsumer) { + if (b instanceof DAPLineBreakpoint lb) { + breakpointConsumer.lineBreakpoint("file://" + lb.getFilePath(), 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..14fc5cf06e97 --- /dev/null +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/DAPLineBreakpoint.java @@ -0,0 +1,335 @@ +/* + * 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.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.NullAllowed; +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); + @NullAllowed + private final FileObject fileObject; // The user file that contains the breakpoint + private final String filePath; // Path of the file to which MI breakpoint is submitted + private final int lineNumber; // The breakpoint line number + private volatile String condition; + + private DAPLineBreakpoint (FileObject fileObject, String filePath, int lineNumber) { + this.fileObject = fileObject; + this.filePath = filePath; + this.lineNumber = lineNumber; + } + + public static DAPLineBreakpoint create(Line line) { + int lineNumber = line.getLineNumber() + 1; + FileObject fileObject = line.getLookup().lookup(FileObject.class); + String filePath = FileUtil.toFile(fileObject).getAbsolutePath(); + return new DAPLineBreakpoint(fileObject, filePath, lineNumber); + } + + /** + * Create a new CPP lite 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); + } + + /** + * Create a new CPP lite breakpoint, that is not associated with a user file. + * @param filePath the file path of the breakpoint in the debuggee + * @param lineNumber 1-based line number + * @return a new breakpoint. + */ + public static DAPLineBreakpoint create(String filePath, int lineNumber) { + return new DAPLineBreakpoint(null, filePath, lineNumber); + } + + /** + * Get the file path of the breakpoint in the debuggee. + */ + public String getFilePath() { + return filePath; + } + + /** + * 1-based line number. + */ + public int getLineNumber() { + return lineNumber; + } + + @CheckForNull + 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); + } + + public void setCPPValidity(VALIDITY validity, String reason) { + setValidity(validity, reason); + } + + /** + * 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 FileUtil.toFileObject(new File(filePath)); + } + + @Override + public FileObject[] getFiles() { + FileObject fo = getFile(); + if (fo != null) { + return new FileObject[] { fo }; + } else { + return null; + } + } + + @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..024a2b53f302 --- /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.Utils; + +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) { + Utils.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..1d79f49715d0 --- /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.Utils; + +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) { + Utils.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..714e82fb4acf --- /dev/null +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/spi/BreakpointConvertor.java @@ -0,0 +1,51 @@ +/* + * 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.util.List; +import org.netbeans.modules.lsp.client.debugger.LineBreakpointData; +import org.netbeans.modules.lsp.client.debugger.SPIAccessor; + +/** + * @since 1.29 + */ +public interface BreakpointConvertor { + public void covert(org.netbeans.api.debugger.Breakpoint b, + ConvertedBreakpointConsumer breakpointConsumer); + public static class ConvertedBreakpointConsumer { + private final List lineBreakpoints; + + ConvertedBreakpointConsumer(List lineBreakpoints) { + this.lineBreakpoints = lineBreakpoints; + } + + public void lineBreakpoint(String url, int lineNumner, String condition) { + lineBreakpoints.add(new LineBreakpointData(url, lineNumner, 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 41bdfd612e00..4db9fa4f7ac6 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 @@ -36,6 +36,7 @@ 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.netbeans.modules.textmate.lexer.TextmateTokenId; import org.netbeans.spi.navigator.NavigatorPanel; import org.openide.filesystems.FileObject; @@ -153,7 +154,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); @@ -162,18 +181,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); } @@ -213,6 +225,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 { IRegistryOptions opts = new IRegistryOptions() { @Override @@ -240,6 +260,7 @@ public static class LanguageDescription { public String name; public String icon; public String mimeType; + public boolean debugger; public LanguageDescription() { this.id = null; @@ -248,16 +269,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..4240bacadf1e --- /dev/null +++ b/ide/lsp.client/test/unit/src/org/netbeans/modules/lsp/client/debugger/DebuggerTest.java @@ -0,0 +1,757 @@ +/* + * 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; + private String testFileURL; + + 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(testFileURL, 4)); + DAPLineBreakpoint line6Breakpoint = DAPLineBreakpoint.create(testFileURL, 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(testFileURL, 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(testFileURL, 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); + } + testFileURL = testFile.toURL().toString(); + 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]) { From 29b40aae13ef27c7073dc3b2609b8f1b9ebb3877 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Mon, 30 Dec 2024 18:08:23 +0100 Subject: [PATCH 02/11] Fixing tests after the DAP Debugger. --- .../lsp/client/debugger/DAPDebugger.java | 2 ++ .../client/debugger/attach/Bundle.properties | 17 +++++++++++++++ .../debugger/attach/DAPAttachPanel.form | 21 +++++++++++++++++++ .../java/lsp/server/ConnectionSpecTest.java | 2 +- 4 files changed, 41 insertions(+), 1 deletion(-) 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 index d2ab6cb4c061..3dff711b296f 100644 --- 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 @@ -318,6 +318,8 @@ public void resume() { //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); } 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 index 5107ba81bc10..ba39d6a5ba1c 100644 --- 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 @@ -1,3 +1,20 @@ +# 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: 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 index 289c05fcd16b..608a44803642 100644 --- 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 @@ -1,5 +1,26 @@ + +
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); From d9314e51bbeef2d8ae37ba32f9be3e344c278c9c Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Wed, 1 Jan 2025 08:19:52 +0100 Subject: [PATCH 03/11] Fixing typo. --- .../netbeans/modules/lsp/client/debugger/DAPDebugger.java | 2 +- .../client/debugger/breakpoints/DAPBreakpointConvertor.java | 2 +- .../lsp/client/debugger/spi/BreakpointConvertor.java | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) 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 index 3dff711b296f..d2f50da71536 100644 --- 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 @@ -241,7 +241,7 @@ private List convertBreakpoints(Breakpoint... breakpoints) { //TODO: could cache the convertors: for (BreakpointConvertor convertor : Lookup.getDefault().lookupAll(BreakpointConvertor.class)) { for (Breakpoint b : breakpoints) { - convertor.covert(b, consumer); + convertor.convert(b, consumer); } } 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 index 03ca7a5ca7f9..74738160ae5d 100644 --- 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 @@ -26,7 +26,7 @@ public class DAPBreakpointConvertor implements BreakpointConvertor { @Override - public void covert(Breakpoint b, ConvertedBreakpointConsumer breakpointConsumer) { + public void convert(Breakpoint b, ConvertedBreakpointConsumer breakpointConsumer) { if (b instanceof DAPLineBreakpoint lb) { breakpointConsumer.lineBreakpoint("file://" + lb.getFilePath(), lb.getLineNumber(), lb.getCondition()); } 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 index 714e82fb4acf..ab58257c2fb3 100644 --- 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 @@ -26,7 +26,7 @@ * @since 1.29 */ public interface BreakpointConvertor { - public void covert(org.netbeans.api.debugger.Breakpoint b, + public void convert(org.netbeans.api.debugger.Breakpoint b, ConvertedBreakpointConsumer breakpointConsumer); public static class ConvertedBreakpointConsumer { private final List lineBreakpoints; @@ -35,8 +35,8 @@ public static class ConvertedBreakpointConsumer { this.lineBreakpoints = lineBreakpoints; } - public void lineBreakpoint(String url, int lineNumner, String condition) { - lineBreakpoints.add(new LineBreakpointData(url, lineNumner, condition)); + public void lineBreakpoint(String url, int lineNumber, String condition) { + lineBreakpoints.add(new LineBreakpointData(url, lineNumber, condition)); } static { From 68f284fedbbabaea0dc6e307c3821969f7a1e49a Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Sat, 4 Jan 2025 19:54:17 +0100 Subject: [PATCH 04/11] Removing commented out code --- .../client/debugger/DAPActionsProvider.java | 29 +------------------ 1 file changed, 1 insertion(+), 28 deletions(-) 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 index 71a2400ebd5b..51560713c61c 100644 --- 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 @@ -69,7 +69,7 @@ public DAPActionsProvider(ContextProvider contextProvider) { } @Override - public Set getActions () { + public Set getActions () { return ACTIONS; } @@ -88,38 +88,11 @@ public void doAction (Object action) { debugger.stepOut(); } else if (action == ActionsManager.ACTION_PAUSE) { debugger.pause(); -// } else -// if (action == ActionsManager.ACTION_START) { -// return ; -// } else -// if ( action == ActionsManager.ACTION_STEP_INTO || -// action == ActionsManager.ACTION_STEP_OUT || -// action == ActionsManager.ACTION_STEP_OVER -// ) { -// debugger.doStep(action); } } @Override public void postAction(final Object action, final Runnable actionPerformedNotifier) { -// if (action == ActionsManager.ACTION_KILL) { -// synchronized (DAPDebugger.class) { -// if (killRequestProcessor == null) { -// killRequestProcessor = new RequestProcessor("CPPLite debugger finish RP", 1); -// } -// } -// killRequestProcessor.post(new Runnable() { -// @Override -// public void run() { -// try { -// doAction(action); -// } finally { -// actionPerformedNotifier.run(); -// } -// } -// }); -// return ; -// } setDebugActionsEnabled(false); ACTIONS_WORKER.post(new Runnable() { @Override From 819cf96933609e6ca88ebbc5fa91c233ee91f237 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Sat, 4 Jan 2025 20:41:45 +0100 Subject: [PATCH 05/11] Call the utilities DAPUtils --- .../netbeans/modules/lsp/client/debugger/DAPDebugger.java | 4 ++-- .../org/netbeans/modules/lsp/client/debugger/DAPThread.java | 4 ++-- .../lsp/client/debugger/{Utils.java => DAPUtils.java} | 6 +++--- .../lsp/client/debugger/breakpoints/BreakpointModel.java | 4 ++-- .../debugger/debuggingview/DebuggingActionsProvider.java | 4 ++-- .../modules/lsp/client/debugger/models/CallStackModel.java | 4 ++-- 6 files changed, 13 insertions(+), 13 deletions(-) rename ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/{Utils.java => DAPUtils.java} (96%) 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 index d2f50da71536..b6e91d2291df 100644 --- 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 @@ -262,7 +262,7 @@ private void continued() { } currentThreadId = -1; cs.fireChange(); //TODO: in a different thread? - Utils.unmarkCurrent(); + DAPUtils.unmarkCurrent(); } @Override @@ -293,7 +293,7 @@ public void terminated(TerminatedEventArguments args) { 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(); - Utils.unmarkCurrent(); //TODO: can this be done cleaner? + DAPUtils.unmarkCurrent(); //TODO: can this be done cleaner? DebuggerManager.getDebuggerManager().removeDebuggerListener(DebuggerManager.PROP_BREAKPOINTS, updateBreakpointsListener); launched.cancel(true); try { 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 index a9acbf2a1b0a..a7f4984f3ae0 100644 --- 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 @@ -179,7 +179,7 @@ private void refreshAnnotations() { DAPFrame[] frames = getStack(); if (frames.length == 0) { - Utils.unmarkCurrent(); + DAPUtils.unmarkCurrent(); return ; } @@ -196,7 +196,7 @@ private void refreshAnnotations() { .filter(l -> l != currentLine) .forEach(stack::add); - Utils.markCurrent(stack.toArray(Line[]::new)); + DAPUtils.markCurrent(stack.toArray(Line[]::new)); } } diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/Utils.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPUtils.java similarity index 96% rename from ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/Utils.java rename to ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPUtils.java index ef8293bc922f..77ef7db527d9 100644 --- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/Utils.java +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPUtils.java @@ -27,13 +27,13 @@ import org.openide.text.Line.ShowOpenType; import org.openide.text.Line.ShowVisibilityType; -public final class Utils { +public final class DAPUtils { - private static final Logger logger = Logger.getLogger(Utils.class.getName()); + private static final Logger logger = Logger.getLogger(DAPUtils.class.getName()); private static Object currentLine; - private Utils() { + private DAPUtils() { } static synchronized void markCurrent (final Object line) { 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 index c39713043081..ac9bfa05ef85 100644 --- 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 @@ -25,7 +25,7 @@ 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.Utils; +import org.netbeans.modules.lsp.client.debugger.DAPUtils; import org.netbeans.spi.debugger.DebuggerServiceRegistration; import org.netbeans.spi.viewmodel.ModelEvent; @@ -92,7 +92,7 @@ public String getIconBase (Object node) throws UnknownTypeException { return DISABLED_LINE_BREAKPOINT; DAPDebugger debugger = getDebugger (); if ( debugger != null && - Utils.contains ( + DAPUtils.contains ( debugger.getCurrentLine (), breakpoint.getLine () ) 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 index 024a2b53f302..5d94d1a8b4e1 100644 --- 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 @@ -23,7 +23,7 @@ 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.Utils; +import org.netbeans.modules.lsp.client.debugger.DAPUtils; import org.netbeans.spi.debugger.ContextProvider; import org.netbeans.spi.debugger.DebuggerServiceRegistration; @@ -208,7 +208,7 @@ private static boolean isGoToSourceSupported (DAPFrame frame) { private static void goToSource(final DAPFrame frame) { Line currentLine = frame.location(); if (currentLine != null) { - Utils.showLine(new Line[] {currentLine}); + DAPUtils.showLine(new Line[] {currentLine}); } } 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 index 1d79f49715d0..718f0f94c27b 100644 --- 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 @@ -26,7 +26,7 @@ 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.Utils; +import org.netbeans.modules.lsp.client.debugger.DAPUtils; import org.netbeans.spi.debugger.ContextProvider; import org.netbeans.spi.debugger.DebuggerServiceRegistration; @@ -250,7 +250,7 @@ public void performDefaultAction (Object node) throws UnknownTypeException { if (node instanceof DAPFrame) { Line line = ((DAPFrame) node).location(); if (line != null) { - Utils.showLine(new Line[] {line}); + DAPUtils.showLine(new Line[] {line}); } ((DAPFrame) node).makeCurrent(); return; From ad198730ef6fc90fc04ed4cad855e629be3006cb Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Sat, 4 Jan 2025 20:44:38 +0100 Subject: [PATCH 06/11] Removing unused method --- .../modules/lsp/client/debugger/DAPUtils.java | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPUtils.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPUtils.java index 77ef7db527d9..ab3c72a7191d 100644 --- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPUtils.java +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPUtils.java @@ -100,17 +100,6 @@ public void run () { }); } - static int getLineNumber (Object line) { - final Annotatable[] a = (Annotatable[]) line; - if (a [0] instanceof Line) - return ((Line) a [0]).getLineNumber (); - else - if (a [0] instanceof Line.Part) - return ((Line.Part) a [0]).getLine ().getLineNumber (); - else - throw new InternalError (); - } - public static boolean contains (Object currentLine, Line line) { if (currentLine == null) return false; final Annotatable[] a = (Annotatable[]) currentLine; From 2d326bd9f81bc8398179d3bf7ffad00e116666fe Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Sat, 4 Jan 2025 20:47:13 +0100 Subject: [PATCH 07/11] Adding curly braces --- .../org/netbeans/modules/lsp/client/debugger/DAPUtils.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPUtils.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPUtils.java index ab3c72a7191d..6f71bda7fa12 100644 --- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPUtils.java +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPUtils.java @@ -44,16 +44,17 @@ static synchronized void markCurrent (final Object line) { // first line with icon in gutter DebuggerAnnotation[] annotations = new DebuggerAnnotation [k]; - if (annotatables [i] instanceof Line.Part) + if (annotatables [i] instanceof Line.Part) { annotations [i] = new DebuggerAnnotation ( DebuggerAnnotation.CURRENT_LINE_PART_ANNOTATION_TYPE, annotatables [i] ); - else + } else { annotations [i] = new DebuggerAnnotation ( DebuggerAnnotation.CURRENT_LINE_ANNOTATION_TYPE, annotatables [i] ); + } // other lines for (i = 1; i < k; i++) From 7d770a6ed0aa3ba65d6f21d12fe5e4f58b9d2295 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Sat, 4 Jan 2025 20:53:22 +0100 Subject: [PATCH 08/11] Make DAPUtils methods work with Annotatable[] --- .../netbeans/modules/lsp/client/debugger/DAPUtils.java | 8 +++----- .../debugger/breakpoints/DAPBreakpointActionProvider.java | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPUtils.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPUtils.java index 6f71bda7fa12..fdf9bf83fc83 100644 --- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPUtils.java +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPUtils.java @@ -36,10 +36,9 @@ public final class DAPUtils { private DAPUtils() { } - static synchronized void markCurrent (final Object line) { + static synchronized void markCurrent (Annotatable[] annotatables) { unmarkCurrent (); - Annotatable[] annotatables = (Annotatable[]) line; int i = 0, k = annotatables.length; // first line with icon in gutter @@ -70,7 +69,7 @@ static synchronized void markCurrent (final Object line) { ); currentLine = annotations; - showLine (line); + showLine(annotatables); } static synchronized void unmarkCurrent () { @@ -85,8 +84,7 @@ static synchronized void unmarkCurrent () { } } - public static void showLine (final Object line) { - final Annotatable[] a = (Annotatable[]) line; + public static void showLine (Annotatable[] a) { SwingUtilities.invokeLater (new Runnable () { @Override public void run () { 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 index 053f07b2a1b5..2f0af51b3b5a 100644 --- 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 @@ -113,7 +113,7 @@ public void doAction (Object action) { * @return set of actions supported by this ActionsProvider */ @Override - public Set getActions () { + public Set getActions () { return ACTIONS; } From 1bcff8eb4419f79e8f515a1abc552fd390ed52e2 Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Sun, 5 Jan 2025 13:35:02 +0100 Subject: [PATCH 09/11] Attempting to do further cleanup the DAP debugger, as per comments by matthiasblaesing: - cleaning up use of String urls, using URIs internally - cleaning up the breakpoint to not hold paths, but rather only hold FileObjects - cleaning up the BreakpointAnnotationProvider to not use a weak listener, and holder for the listeners. Using ordinary listeners, which will be held as long as the DataObject exists, and should be automatically GCed when the DO is removed, which is presumably the intent - renaming (DAP)Utils to DAPStackTraceAnnotationHolder --- .../lsp/client/debugger/DAPDebugger.java | 34 ++++++++------- .../modules/lsp/client/debugger/DAPFrame.java | 16 +++---- ...ava => DAPStackTraceAnnotationHolder.java} | 32 +++++++------- .../lsp/client/debugger/DAPThread.java | 4 +- .../client/debugger/LineBreakpointData.java | 4 +- .../BreakpointAnnotationProvider.java | 12 +----- .../debugger/breakpoints/BreakpointModel.java | 13 ++---- .../breakpoints/BreakpointsReader.java | 33 +++++--------- .../breakpoints/DAPBreakpointConvertor.java | 2 +- .../breakpoints/DAPLineBreakpoint.java | 43 +++---------------- .../DebuggingActionsProvider.java | 4 +- .../debugger/models/CallStackModel.java | 4 +- .../debugger/spi/BreakpointConvertor.java | 5 ++- .../lsp/client/debugger/DebuggerTest.java | 10 ++--- 14 files changed, 78 insertions(+), 138 deletions(-) rename ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/{DAPUtils.java => DAPStackTraceAnnotationHolder.java} (83%) 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 index b6e91d2291df..d3c04add5be4 100644 --- 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 @@ -18,6 +18,7 @@ */ package org.netbeans.modules.lsp.client.debugger; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.Socket; @@ -89,6 +90,7 @@ 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"; @@ -109,7 +111,7 @@ public final class DAPDebugger implements IDebugProtocolClient { private final AtomicBoolean suspended = new AtomicBoolean(); private final Map id2Thread = new HashMap<>(); //TODO: concurrent/synchronization!!! private final AtomicReference runAfterConfigureDone = new AtomicReference<>(); - private URLPathConvertor fileConvertor; + private URIPathConvertor fileConvertor; private InputStream in; private Future launched; private IDebugProtocolServer server; @@ -130,13 +132,13 @@ public void breakpointRemoved(Breakpoint breakpoint) { updateAfterBreakpointChange(breakpoint); } private void updateAfterBreakpointChange(Breakpoint breakpoint) { - Set modifiedURLs = + Set modifiedURLs = convertBreakpoints(breakpoint).stream() - .map(b -> b.url()) + .map(b -> b.uri()) .collect(Collectors.toSet()); try { - setBreakpoints(d -> modifiedURLs.contains(d.url())); + setBreakpoints(d -> modifiedURLs.contains(d.uri())); } catch (InterruptedException | ExecutionException ex) { Exceptions.printStackTrace(ex); } @@ -212,7 +214,7 @@ private void setBreakpoints(Predicate filter) throws Interru lb.setLine(data.lineNumber()); lb.setCondition(data.condition()); - String path = fileConvertor.toPath(data.url()); + String path = fileConvertor.toPath(data.uri()); if (path != null) { url2Breakpoints.computeIfAbsent(path, x -> new ArrayList<>()) @@ -262,7 +264,7 @@ private void continued() { } currentThreadId = -1; cs.fireChange(); //TODO: in a different thread? - DAPUtils.unmarkCurrent(); + DAPStackTraceAnnotationHolder.unmarkCurrent(); } @Override @@ -293,7 +295,7 @@ public void terminated(TerminatedEventArguments args) { 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(); - DAPUtils.unmarkCurrent(); //TODO: can this be done cleaner? + DAPStackTraceAnnotationHolder.unmarkCurrent(); //TODO: can this be done cleaner? DebuggerManager.getDebuggerManager().removeDebuggerListener(DebuggerManager.PROP_BREAKPOINTS, updateBreakpointsListener); launched.cancel(true); try { @@ -554,24 +556,24 @@ public CompletableFuture attachedChildSession(Map args) public enum Type {LAUNCH, ATTACH} - private static final URLPathConvertor DEFAULT_CONVERTOR = new URLPathConvertor() { + private static final URIPathConvertor DEFAULT_CONVERTOR = new URIPathConvertor() { @Override - public String toPath(String file) { - if (file.startsWith("file:")) { - return URI.create(file).getPath(); + public String toPath(URI uri) { + if ("file".equals(uri.getScheme())) { + return uri.getPath(); } return null; } @Override - public String toURL(String path) { - return "file:" + path; + public URI toURI(String path) { + return Utilities.toURI(new File(path)); } }; - public interface URLPathConvertor { - public String toPath(String url); - public String toURL(String path); + public 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/DAPFrame.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPFrame.java index c8c6c53df25d..d78b8fea4021 100644 --- 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 @@ -25,7 +25,7 @@ import org.eclipse.lsp4j.debug.StackFrame; import org.netbeans.api.annotations.common.CheckForNull; -import org.netbeans.modules.lsp.client.debugger.DAPDebugger.URLPathConvertor; +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; @@ -38,11 +38,11 @@ public final class DAPFrame implements DVFrame { private static final Logger LOGGER = Logger.getLogger(DAPFrame.class.getName()); - private final URLPathConvertor fileConvertor; + private final URIPathConvertor fileConvertor; private final DAPThread thread; private final StackFrame frame; - public DAPFrame(URLPathConvertor fileConvertor, DAPThread thread, StackFrame frame) { + public DAPFrame(URIPathConvertor fileConvertor, DAPThread thread, StackFrame frame) { this.fileConvertor = fileConvertor; this.thread = thread; this.frame = frame; @@ -71,15 +71,11 @@ public void makeCurrent() { @Override public URI getSourceURI() { - try { - //XXX: frame.getSource().getPath() may not work(!) - if (frame.getSource() == null || frame.getSource().getPath() == null) { - return null; - } - return new URI(fileConvertor.toURL(frame.getSource().getPath())); - } catch (URISyntaxException ex) { + //XXX: frame.getSource().getPath() may not work(!) + if (frame.getSource() == null || frame.getSource().getPath() == null) { return null; } + return fileConvertor.toURI(frame.getSource().getPath()); } @Override diff --git a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPUtils.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPStackTraceAnnotationHolder.java similarity index 83% rename from ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPUtils.java rename to ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPStackTraceAnnotationHolder.java index fdf9bf83fc83..12361910f109 100644 --- a/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPUtils.java +++ b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/DAPStackTraceAnnotationHolder.java @@ -19,7 +19,6 @@ package org.netbeans.modules.lsp.client.debugger; -import java.util.logging.Logger; import javax.swing.SwingUtilities; import org.openide.text.Annotatable; @@ -27,13 +26,11 @@ import org.openide.text.Line.ShowOpenType; import org.openide.text.Line.ShowVisibilityType; -public final class DAPUtils { +public final class DAPStackTraceAnnotationHolder { - private static final Logger logger = Logger.getLogger(DAPUtils.class.getName()); + private static DebuggerAnnotation[] currentAnnotations; - private static Object currentLine; - - private DAPUtils() { + private DAPStackTraceAnnotationHolder() { } static synchronized void markCurrent (Annotatable[] annotatables) { @@ -56,31 +53,32 @@ static synchronized void markCurrent (Annotatable[] annotatables) { } // other lines - for (i = 1; i < k; i++) - if (annotatables [i] instanceof Line.Part) + 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 + } else { annotations [i] = new DebuggerAnnotation ( DebuggerAnnotation.CALL_STACK_FRAME_ANNOTATION_TYPE, annotatables [i] ); - currentLine = annotations; + } + } + + currentAnnotations = annotations; showLine(annotatables); } static synchronized void unmarkCurrent () { - if (currentLine != null) { - -// ((DebuggerAnnotation) currentLine).detach (); - int i, k = ((DebuggerAnnotation[]) currentLine).length; - for (i = 0; i < k; i++) { - ((DebuggerAnnotation[]) currentLine) [i].detach (); + if (currentAnnotations != null) { + int k = currentAnnotations.length; + for (int i = 0; i < k; i++) { + currentAnnotations[i].detach(); } - currentLine = null; + currentAnnotations = null; } } 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 index a7f4984f3ae0..f537a2a3480f 100644 --- 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 @@ -179,7 +179,7 @@ private void refreshAnnotations() { DAPFrame[] frames = getStack(); if (frames.length == 0) { - DAPUtils.unmarkCurrent(); + DAPStackTraceAnnotationHolder.unmarkCurrent(); return ; } @@ -196,7 +196,7 @@ private void refreshAnnotations() { .filter(l -> l != currentLine) .forEach(stack::add); - DAPUtils.markCurrent(stack.toArray(Line[]::new)); + DAPStackTraceAnnotationHolder.markCurrent(stack.toArray(Line[]::new)); } } 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 index 390c95a39b9f..04a37604e5ca 100644 --- 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 @@ -18,5 +18,7 @@ */ package org.netbeans.modules.lsp.client.debugger; -public record LineBreakpointData(String url, int lineNumber, String condition) { +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/breakpoints/BreakpointAnnotationProvider.java b/ide/lsp.client/src/org/netbeans/modules/lsp/client/debugger/breakpoints/BreakpointAnnotationProvider.java index ef53e2d91c53..308d8223f7d1 100644 --- 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 @@ -22,7 +22,6 @@ import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; -import java.util.HashSet; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; @@ -40,7 +39,6 @@ import org.openide.text.Line; import org.openide.util.Lookup; import org.openide.util.RequestProcessor; -import org.openide.util.WeakListeners; import org.openide.util.WeakSet; @@ -54,7 +52,6 @@ public final class BreakpointAnnotationProvider extends DebuggerManagerAdapter i private final Map> breakpointToAnnotations = new IdentityHashMap<>(); private final Set annotatedFiles = new WeakSet<>(); - private Set dataObjectListeners; private volatile boolean breakpointsActive = true; private final RequestProcessor annotationProcessor = new RequestProcessor("CPP BP Annotation Refresh", 1); @@ -84,14 +81,7 @@ public void run() { } } }; - dobj.addPropertyChangeListener(WeakListeners.propertyChange(pchl, dobj)); - synchronized (this) { - if (dataObjectListeners == null) { - dataObjectListeners = new HashSet<>(); - } - // Prevent from GC. - dataObjectListeners.add(pchl); - } + dobj.addPropertyChangeListener(pchl); } annotate(fo); } 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 index ac9bfa05ef85..f82c95cf5edf 100644 --- 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 @@ -25,7 +25,7 @@ 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.DAPUtils; +import org.netbeans.modules.lsp.client.debugger.DAPStackTraceAnnotationHolder; import org.netbeans.spi.debugger.DebuggerServiceRegistration; import org.netbeans.spi.viewmodel.ModelEvent; @@ -64,12 +64,7 @@ public String getDisplayName (Object node) throws UnknownTypeException { DAPLineBreakpoint breakpoint = (DAPLineBreakpoint) node; String nameExt; FileObject fileObject = breakpoint.getFileObject(); - if (fileObject != null) { - nameExt = fileObject.getNameExt(); - } else { - File file = new File(breakpoint.getFilePath()); - nameExt = file.getName(); - } + nameExt = fileObject.getNameExt(); return nameExt + ":" + breakpoint.getLineNumber(); } throw new UnknownTypeException (node); @@ -92,7 +87,7 @@ public String getIconBase (Object node) throws UnknownTypeException { return DISABLED_LINE_BREAKPOINT; DAPDebugger debugger = getDebugger (); if ( debugger != null && - DAPUtils.contains ( + DAPStackTraceAnnotationHolder.contains ( debugger.getCurrentLine (), breakpoint.getLine () ) @@ -117,7 +112,7 @@ public String getShortDescription (Object node) throws UnknownTypeException { if (node instanceof DAPLineBreakpoint) { DAPLineBreakpoint breakpoint = (DAPLineBreakpoint) node; - return breakpoint.getFilePath() + ":" + breakpoint.getLineNumber(); + return breakpoint.getFileObject().getPath() + ":" + breakpoint.getLineNumber(); } throw new UnknownTypeException (node); } 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 index cad02b4068c7..5d8de65f0275 100644 --- 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 @@ -46,25 +46,17 @@ public Object read (String typeID, Properties properties) { DAPLineBreakpoint b; int lineNumber = properties.getInt("lineNumber", 0) + 1; String url = properties.getString ("url", null); - if (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); - } else { - String filePath = properties.getString ("filePath", null); - if (filePath == null) { - return null; - } - b = DAPLineBreakpoint.create(filePath, lineNumber); + 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, "") ); @@ -92,10 +84,7 @@ public Object read (String typeID, Properties properties) { public void write (Object object, Properties properties) { DAPLineBreakpoint b = (DAPLineBreakpoint) object; FileObject fo = b.getFileObject(); - if (fo != null) { - properties.setString("url", fo.toURL().toString()); - } - properties.setString("filePath", b.getFilePath()); + properties.setString("url", fo.toURL().toString()); properties.setInt ( "lineNumber", b.getLineNumber() - 1 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 index 74738160ae5d..f553252b47ac 100644 --- 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 @@ -28,7 +28,7 @@ public class DAPBreakpointConvertor implements BreakpointConvertor { @Override public void convert(Breakpoint b, ConvertedBreakpointConsumer breakpointConsumer) { if (b instanceof DAPLineBreakpoint lb) { - breakpointConsumer.lineBreakpoint("file://" + lb.getFilePath(), lb.getLineNumber(), lb.getCondition()); + 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 index 14fc5cf06e97..9339658cfbf4 100644 --- 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 @@ -19,7 +19,6 @@ package org.netbeans.modules.lsp.client.debugger.breakpoints; -import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -27,7 +26,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import org.netbeans.api.annotations.common.CheckForNull; -import org.netbeans.api.annotations.common.NullAllowed; +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; @@ -54,27 +53,23 @@ public final class DAPLineBreakpoint extends Breakpoint { private final AtomicBoolean enabled = new AtomicBoolean(true); private final AtomicBoolean hidden = new AtomicBoolean(false); - @NullAllowed private final FileObject fileObject; // The user file that contains the breakpoint - private final String filePath; // Path of the file to which MI breakpoint is submitted private final int lineNumber; // The breakpoint line number private volatile String condition; private DAPLineBreakpoint (FileObject fileObject, String filePath, int lineNumber) { this.fileObject = fileObject; - this.filePath = filePath; this.lineNumber = lineNumber; } public static DAPLineBreakpoint create(Line line) { int lineNumber = line.getLineNumber() + 1; FileObject fileObject = line.getLookup().lookup(FileObject.class); - String filePath = FileUtil.toFile(fileObject).getAbsolutePath(); - return new DAPLineBreakpoint(fileObject, filePath, lineNumber); + return create(fileObject, lineNumber); } /** - * Create a new CPP lite breakpoint based on a user file. + * 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. @@ -84,23 +79,6 @@ public static DAPLineBreakpoint create(FileObject fileObject, int lineNumber) { return new DAPLineBreakpoint(fileObject, filePath, lineNumber); } - /** - * Create a new CPP lite breakpoint, that is not associated with a user file. - * @param filePath the file path of the breakpoint in the debuggee - * @param lineNumber 1-based line number - * @return a new breakpoint. - */ - public static DAPLineBreakpoint create(String filePath, int lineNumber) { - return new DAPLineBreakpoint(null, filePath, lineNumber); - } - - /** - * Get the file path of the breakpoint in the debuggee. - */ - public String getFilePath() { - return filePath; - } - /** * 1-based line number. */ @@ -108,7 +86,7 @@ public int getLineNumber() { return lineNumber; } - @CheckForNull + @NonNull public FileObject getFileObject() { return fileObject; } @@ -189,10 +167,6 @@ public void setCondition(String condition) { firePropertyChange (PROP_CONDITION, oldCondition, condition); } - public void setCPPValidity(VALIDITY validity, String reason) { - setValidity(validity, reason); - } - /** * Gets value of hidden property. * @@ -234,17 +208,12 @@ public String getType() { } private FileObject getFile() { - return FileUtil.toFileObject(new File(filePath)); + return getFileObject(); } @Override public FileObject[] getFiles() { - FileObject fo = getFile(); - if (fo != null) { - return new FileObject[] { fo }; - } else { - return null; - } + return new FileObject[] { getFileObject() }; } @Override 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 index 5d94d1a8b4e1..cc6c104fcfbd 100644 --- 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 @@ -23,7 +23,7 @@ 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.DAPUtils; +import org.netbeans.modules.lsp.client.debugger.DAPStackTraceAnnotationHolder; import org.netbeans.spi.debugger.ContextProvider; import org.netbeans.spi.debugger.DebuggerServiceRegistration; @@ -208,7 +208,7 @@ private static boolean isGoToSourceSupported (DAPFrame frame) { private static void goToSource(final DAPFrame frame) { Line currentLine = frame.location(); if (currentLine != null) { - DAPUtils.showLine(new Line[] {currentLine}); + DAPStackTraceAnnotationHolder.showLine(new Line[] {currentLine}); } } 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 index 718f0f94c27b..def79b632e50 100644 --- 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 @@ -26,7 +26,7 @@ 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.DAPUtils; +import org.netbeans.modules.lsp.client.debugger.DAPStackTraceAnnotationHolder; import org.netbeans.spi.debugger.ContextProvider; import org.netbeans.spi.debugger.DebuggerServiceRegistration; @@ -250,7 +250,7 @@ public void performDefaultAction (Object node) throws UnknownTypeException { if (node instanceof DAPFrame) { Line line = ((DAPFrame) node).location(); if (line != null) { - DAPUtils.showLine(new Line[] {line}); + DAPStackTraceAnnotationHolder.showLine(new Line[] {line}); } ((DAPFrame) node).makeCurrent(); return; 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 index ab58257c2fb3..cc681bedb629 100644 --- 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 @@ -18,6 +18,7 @@ */ package org.netbeans.modules.lsp.client.debugger.spi; +import java.net.URI; import java.util.List; import org.netbeans.modules.lsp.client.debugger.LineBreakpointData; import org.netbeans.modules.lsp.client.debugger.SPIAccessor; @@ -35,8 +36,8 @@ public static class ConvertedBreakpointConsumer { this.lineBreakpoints = lineBreakpoints; } - public void lineBreakpoint(String url, int lineNumber, String condition) { - lineBreakpoints.add(new LineBreakpointData(url, lineNumber, condition)); + public void lineBreakpoint(URI uri, int lineNumber, String condition) { + lineBreakpoints.add(new LineBreakpointData(uri, lineNumber, condition)); } static { 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 index 4240bacadf1e..3718f1f2edb4 100644 --- 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 @@ -92,7 +92,6 @@ public class DebuggerTest extends NbTestCase { private FileObject srcDir; private String srcDirURL; private FileObject testFile; - private String testFileURL; public DebuggerTest(String name) { super(name); @@ -117,8 +116,8 @@ private static void nestedPrint(String toPrint) { //10 DebuggerManager manager = DebuggerManager.getDebuggerManager(); - manager.addBreakpoint(DAPLineBreakpoint.create(testFileURL, 4)); - DAPLineBreakpoint line6Breakpoint = DAPLineBreakpoint.create(testFileURL, 6); + manager.addBreakpoint(DAPLineBreakpoint.create(testFile, 4)); + DAPLineBreakpoint line6Breakpoint = DAPLineBreakpoint.create(testFile, 6); manager.addBreakpoint(line6Breakpoint); int backendPort = startBackend(); Socket socket = new Socket("localhost", backendPort); @@ -154,7 +153,7 @@ private static void nestedPrint(String toPrint) { //10 //tweak breakpoints: manager.removeBreakpoint(line6Breakpoint); - manager.addBreakpoint(DAPLineBreakpoint.create(testFileURL, 7)); + 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); @@ -189,7 +188,7 @@ private static void nestedPrint(String toPrint) { //10 """); DebuggerManager manager = DebuggerManager.getDebuggerManager(); - DAPLineBreakpoint breakpoint = DAPLineBreakpoint.create(testFileURL, 11); + DAPLineBreakpoint breakpoint = DAPLineBreakpoint.create(testFile, 11); breakpoint.setCondition("\"4\".equals(toPrint)"); manager.addBreakpoint(breakpoint); @@ -227,7 +226,6 @@ private void writeTestFile(String code) throws IOException { Writer w = new OutputStreamWriter(out)) { w.write(code); } - testFileURL = testFile.toURL().toString(); try (OutputStream out = FileUtil.createData(project, "pom.xml").getOutputStream(); Writer w = new OutputStreamWriter(out)) { w.write(""" From b74a3a4273cd5dc332cb3c41dae75d5f098851aa Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Wed, 8 Jan 2025 06:26:41 +0100 Subject: [PATCH 10/11] No need (right now) for the interface to be public --- .../org/netbeans/modules/lsp/client/debugger/DAPDebugger.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index d3c04add5be4..8049bec650f4 100644 --- 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 @@ -572,7 +572,7 @@ public URI toURI(String path) { } }; - public interface URIPathConvertor { + interface URIPathConvertor { public String toPath(URI uri); public URI toURI(String path); } From bf75e981c952fb7e24e810b333d88b0375299e5e Mon Sep 17 00:00:00 2001 From: Jan Lahoda Date: Wed, 8 Jan 2025 06:27:45 +0100 Subject: [PATCH 11/11] Adding documentation for the BreakpointConvertor --- .../debugger/spi/BreakpointConvertor.java | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) 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 index cc681bedb629..4ba3020b7c53 100644 --- 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 @@ -20,15 +20,36 @@ 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; @@ -36,7 +57,15 @@ public static class ConvertedBreakpointConsumer { this.lineBreakpoints = lineBreakpoints; } - public void lineBreakpoint(URI uri, int lineNumber, String condition) { + /**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)); }