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