validLines = new java.util.HashSet<>();
+ for (int line : executableLines) {
+ validLines.add(line);
+ }
+
+ // Add native breakpoints with optional conditions
+ IBreakpoint[] result = new Breakpoint[lines.length];
+ for (int i = 0; i < lines.length; i++) {
+ String condition = (exprs != null && i < exprs.length) ? exprs[i] : null;
+ int requestedLine = lines[i];
+
+ if (validLines.contains(requestedLine)) {
+ // Valid executable line - add breakpoint and mark as bound
+ NativeDebuggerListener.addBreakpoint(serverPath.get(), requestedLine, condition);
+ result[i] = Breakpoint.Bound(requestedLine, nextDapBreakpointID());
+ } else {
+ // Not an executable line - mark as unbound (unverified)
+ result[i] = Breakpoint.Unbound(requestedLine, nextDapBreakpointID());
+ }
+ }
+
+ return result;
+ }
+
+ @Override
+ public void clearAllBreakpoints() {
+ NativeDebuggerListener.clearAllBreakpoints();
+ }
+
+ // ========== Execution control ==========
+
+ @Override
+ public void continue_(long threadID) {
+ NativeDebuggerListener.resumeNativeThread(threadID);
+ }
+
+ @Override
+ public void continueAll() {
+ NativeDebuggerListener.resumeAllNativeThreads();
+ }
+
+ @Override
+ public void stepIn(long threadID) {
+ int currentDepth = getStackDepthForThread(threadID);
+ NativeDebuggerListener.startStepping(threadID, StepMode.STEP_INTO, currentDepth);
+ continue_(threadID);
+ }
+
+ @Override
+ public void stepOver(long threadID) {
+ int currentDepth = getStackDepthForThread(threadID);
+ NativeDebuggerListener.startStepping(threadID, StepMode.STEP_OVER, currentDepth);
+ continue_(threadID);
+ }
+
+ @Override
+ public void stepOut(long threadID) {
+ int currentDepth = getStackDepthForThread(threadID);
+ NativeDebuggerListener.startStepping(threadID, StepMode.STEP_OUT, currentDepth);
+ continue_(threadID);
+ }
+
+ /**
+ * Get the current stack depth for a thread using native debugger frames.
+ * Uses NativeDebuggerListener.getStackDepth() to count only real frames (not synthetic).
+ */
+ private int getStackDepthForThread(long threadID) {
+ PageContext pc = NativeDebuggerListener.getPageContext(threadID);
+ return pc != null ? NativeDebuggerListener.getStackDepth(pc) : 0;
+ }
+
+ // ========== Debug utilities ==========
+
+ @Override
+ public String dump(int dapVariablesReference) {
+ return doDumpNative(dapVariablesReference, false);
+ }
+
+ @Override
+ public String dumpAsJSON(int dapVariablesReference) {
+ return doDumpNative(dapVariablesReference, true);
+ }
+
+ @Override
+ public String getMetadata(int dapVariablesReference) {
+ // Get the object from valTracker
+ var maybeObj = valTracker.maybeGetFromId(dapVariablesReference);
+ if (maybeObj.isEmpty()) {
+ return "\"Variable not found\"";
+ }
+ Object obj = maybeObj.get().obj;
+
+ // Unwrap MarkerTrait.Scope if needed
+ if (obj instanceof CfValueDebuggerBridge.MarkerTrait.Scope) {
+ obj = ((CfValueDebuggerBridge.MarkerTrait.Scope) obj).scopelike;
+ }
+
+ // Get PageContext from a cached frame
+ PageContext pc = null;
+ Long frameId = valTracker.getFrameId(dapVariablesReference);
+ if (frameId != null) {
+ IDebugFrame frame = frameCache.get(frameId);
+ if (frame instanceof NativeDebugFrame) {
+ pc = ((NativeDebugFrame) frame).getPageContext();
+ }
+ }
+
+ // Fallback: try any suspended frame's PageContext
+ if (pc == null) {
+ for (IDebugFrame frame : frameCache.values()) {
+ if (frame instanceof NativeDebugFrame) {
+ pc = ((NativeDebugFrame) frame).getPageContext();
+ if (pc != null) break;
+ }
+ }
+ }
+
+ if (pc == null) {
+ return "\"No PageContext available\"";
+ }
+
+ return doGetMetadataWithPageContext(pc, obj);
+ }
+
+ /**
+ * Execute getMetadata on a separate thread (required for PageContext registration).
+ */
+ private String doGetMetadataWithPageContext(PageContext sourcePC, Object target) {
+ final var result = new Object() {
+ String value = "\"getMetadata failed\"";
+ };
+
+ final PageContext pc = sourcePC;
+ final Object obj = target;
+
+ Thread thread = new Thread(() -> {
+ try {
+ ClassLoader cl = luceeClassLoader != null ? luceeClassLoader : pc.getClass().getClassLoader();
+
+ // Register the existing PageContext with ThreadLocal
+ Class> tlpcClass = cl.loadClass("lucee.runtime.engine.ThreadLocalPageContext");
+ java.lang.reflect.Method registerMethod = tlpcClass.getMethod("register", PageContext.class);
+ java.lang.reflect.Method releaseMethod = tlpcClass.getMethod("release");
+ registerMethod.invoke(null, pc);
+
+ try {
+ // Call GetMetaData.call(PageContext, Object)
+ Class> getMetaDataClass = cl.loadClass("lucee.runtime.functions.system.GetMetaData");
+ java.lang.reflect.Method callMethod = getMetaDataClass.getMethod("call",
+ PageContext.class, Object.class);
+ Object metadata = callMethod.invoke(null, pc, obj);
+
+ // Serialize the metadata to JSON
+ Class> serializeClass = cl.loadClass("lucee.runtime.functions.conversion.SerializeJSON");
+ java.lang.reflect.Method serializeMethod = serializeClass.getMethod("call",
+ PageContext.class, Object.class, Object.class);
+ result.value = (String) serializeMethod.invoke(null, pc, metadata, "struct");
+ } finally {
+ releaseMethod.invoke(null);
+ }
+ } catch (Throwable e) {
+ Log.debug("getMetadata failed: " + e.getMessage());
+ result.value = "\"Error: " + e.getMessage().replace("\"", "\\\"") + "\"";
+ }
+ });
+
+ thread.start();
+ try {
+ thread.join();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+
+ return result.value;
+ }
+
+ /**
+ * Native mode dump implementation using reflection to call Lucee functions.
+ * @param dapVariablesReference The variablesReference from DAP
+ * @param asJson If true, returns JSON; if false, returns HTML dump
+ */
+ private String doDumpNative(int dapVariablesReference, boolean asJson) {
+ // Get the object from valTracker
+ var maybeObj = valTracker.maybeGetFromId(dapVariablesReference);
+ if (maybeObj.isEmpty()) {
+ return asJson ? "\"Variable not found\"" : "Variable not found
";
+ }
+ Object obj = maybeObj.get().obj;
+
+ // Unwrap MarkerTrait.Scope if needed
+ if (obj instanceof CfValueDebuggerBridge.MarkerTrait.Scope) {
+ obj = ((CfValueDebuggerBridge.MarkerTrait.Scope) obj).scopelike;
+ }
+
+ // Get the frameId for this variablesReference to get its PageContext
+ Long frameId = valTracker.getFrameId(dapVariablesReference);
+ PageContext pc = null;
+ if (frameId != null) {
+ IDebugFrame frame = frameCache.get(frameId);
+ if (frame instanceof NativeDebugFrame) {
+ pc = ((NativeDebugFrame) frame).getPageContext();
+ }
+ }
+
+ // If no PageContext from frame, try to find any suspended frame's PageContext
+ if (pc == null) {
+ for (IDebugFrame frame : frameCache.values()) {
+ if (frame instanceof NativeDebugFrame) {
+ pc = ((NativeDebugFrame) frame).getPageContext();
+ if (pc != null) break;
+ }
+ }
+ }
+
+ if (pc == null) {
+ return asJson ? "\"No PageContext available\"" : "No PageContext available
";
+ }
+
+ return doDumpWithPageContext(pc, obj, asJson);
+ }
+
+ /**
+ * Execute dump on a separate thread (required for PageContext registration).
+ */
+ private String doDumpWithPageContext(PageContext sourcePC, Object someDumpable, boolean asJson) {
+ final var result = new Object() {
+ String value = asJson ? "\"dump failed\"" : "dump failed
";
+ };
+
+ final PageContext pc = sourcePC;
+ final Object dumpable = someDumpable;
+
+ Thread thread = new Thread(() -> {
+ try {
+ ClassLoader cl = luceeClassLoader != null ? luceeClassLoader : pc.getClass().getClassLoader();
+
+ // Register the existing PageContext with ThreadLocal
+ Class> tlpcClass = cl.loadClass("lucee.runtime.engine.ThreadLocalPageContext");
+ java.lang.reflect.Method registerMethod = tlpcClass.getMethod("register", PageContext.class);
+ java.lang.reflect.Method releaseMethod = tlpcClass.getMethod("release");
+ registerMethod.invoke(null, pc);
+
+ try {
+ if (asJson) {
+ // Call SerializeJSON
+ Class> serializeClass = cl.loadClass("lucee.runtime.functions.conversion.SerializeJSON");
+ java.lang.reflect.Method callMethod = serializeClass.getMethod("call",
+ PageContext.class, Object.class, Object.class);
+ result.value = (String) callMethod.invoke(null, pc, dumpable, "struct");
+ } else {
+ // Use DumpUtil to get DumpData, then HTMLDumpWriter to render
+ result.value = wrapDumpInHtmlDoc(dumpObjectAsHtml(pc, cl, dumpable));
+ }
+ } finally {
+ releaseMethod.invoke(null);
+ }
+ } catch (Throwable e) {
+ Log.debug("dump failed: " + e.getMessage());
+ result.value = asJson
+ ? "\"Error: " + e.getMessage().replace("\"", "\\\"") + "\""
+ : "Error: " + e.getMessage() + "
";
+ }
+ });
+
+ thread.start();
+ try {
+ thread.join();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+
+ return result.value;
+ }
+
+ /**
+ * Dump an object to HTML string using Lucee's HTMLDumpWriter.
+ */
+ private String dumpObjectAsHtml(PageContext pc, ClassLoader cl, Object obj) throws Exception {
+ // Use DumpUtil to get DumpData, then HTMLDumpWriter to render
+ Class> dumpUtilClass = cl.loadClass("lucee.runtime.dump.DumpUtil");
+ Class> dumpPropertiesClass = cl.loadClass("lucee.runtime.dump.DumpProperties");
+ Class> dumpDataClass = cl.loadClass("lucee.runtime.dump.DumpData");
+
+ // Get default dump properties - use DEFAULT_RICH field
+ java.lang.reflect.Field defaultField = dumpPropertiesClass.getField("DEFAULT_RICH");
+ Object dumpProps = defaultField.get(null);
+
+ // toDumpData(PageContext, Object, int maxlevel, DumpProperties)
+ java.lang.reflect.Method toDumpDataMethod = dumpUtilClass.getMethod("toDumpData",
+ PageContext.class, Object.class, int.class, dumpPropertiesClass);
+ Object dumpData = toDumpDataMethod.invoke(null, pc, obj, 9999, dumpProps);
+
+ // Create HTMLDumpWriter and render
+ Class> htmlDumpWriterClass = cl.loadClass("lucee.runtime.dump.HTMLDumpWriter");
+ Object htmlWriter = htmlDumpWriterClass.getConstructor().newInstance();
+
+ // DumpWriter.toString(PageContext, DumpData)
+ java.lang.reflect.Method toStringMethod = htmlDumpWriterClass.getMethod("toString",
+ PageContext.class, dumpDataClass);
+ return (String) toStringMethod.invoke(htmlWriter, pc, dumpData);
+ }
+
+ private static String wrapDumpInHtmlDoc(String dumpHtml) {
+ return "\n" +
+ "\n" +
+ "\n" +
+ "\n" +
+ "\n" +
+ "\n" +
+ dumpHtml +
+ "\n" +
+ "\n";
+ }
+
+ @Override
+ public String[] getTrackedCanonicalFileNames() {
+ // No class tracking in native mode
+ return new String[0];
+ }
+
+ @Override
+ public String[][] getBreakpointDetail() {
+ return NativeDebuggerListener.getBreakpointDetails();
+ }
+
+ @Override
+ public String getApplicationSettings() {
+ // Get PageContext from any suspended frame
+ PageContext pc = null;
+ for (IDebugFrame frame : frameCache.values()) {
+ if (frame instanceof NativeDebugFrame) {
+ pc = ((NativeDebugFrame) frame).getPageContext();
+ if (pc != null) break;
+ }
+ }
+
+ if (pc == null) {
+ return "\"No PageContext available\"";
+ }
+
+ return doGetApplicationSettingsWithPageContext(pc);
+ }
+
+ /**
+ * Execute getApplicationSettings on a separate thread (required for PageContext registration).
+ */
+ private String doGetApplicationSettingsWithPageContext(PageContext sourcePC) {
+ final var result = new Object() {
+ String value = "\"getApplicationSettings failed\"";
+ };
+
+ final PageContext pc = sourcePC;
+
+ Thread thread = new Thread(() -> {
+ try {
+ ClassLoader cl = luceeClassLoader != null ? luceeClassLoader : pc.getClass().getClassLoader();
+
+ // Register the existing PageContext with ThreadLocal
+ Class> tlpcClass = cl.loadClass("lucee.runtime.engine.ThreadLocalPageContext");
+ java.lang.reflect.Method registerMethod = tlpcClass.getMethod("register", PageContext.class);
+ java.lang.reflect.Method releaseMethod = tlpcClass.getMethod("release");
+ registerMethod.invoke(null, pc);
+
+ try {
+ // Call GetApplicationSettings.call(PageContext)
+ Class> getAppSettingsClass = cl.loadClass("lucee.runtime.functions.system.GetApplicationSettings");
+ java.lang.reflect.Method callMethod = getAppSettingsClass.getMethod("call", PageContext.class);
+ Object settings = callMethod.invoke(null, pc);
+
+ // Serialize the settings to JSON
+ Class> serializeClass = cl.loadClass("lucee.runtime.functions.conversion.SerializeJSON");
+ java.lang.reflect.Method serializeMethod = serializeClass.getMethod("call",
+ PageContext.class, Object.class, Object.class);
+ result.value = (String) serializeMethod.invoke(null, pc, settings, "struct");
+ } finally {
+ releaseMethod.invoke(null);
+ }
+ } catch (Throwable e) {
+ Log.debug("getApplicationSettings failed: " + e.getMessage());
+ result.value = "\"Error: " + e.getMessage().replace("\"", "\\\"") + "\"";
+ }
+ });
+
+ thread.start();
+ try {
+ thread.join(5000); // 5 second timeout
+ } catch (InterruptedException e) {
+ return "\"Timeout getting application settings\"";
+ }
+
+ return result.value;
+ }
+
+ @Override
+ public String getSourcePathForVariablesRef(int variablesRef) {
+ return valTracker
+ .maybeGetFromId(variablesRef)
+ .map(taggedObj -> CfValueDebuggerBridge.getSourcePath(taggedObj.obj))
+ .orElse(null);
+ }
+
+ @Override
+ public org.eclipse.lsp4j.debug.CompletionItem[] getCompletions(int frameId, String partialExpr) {
+ // Get PageContext from frame or any suspended frame
+ PageContext pc = null;
+ IDebugFrame frame = frameCache.get((long) frameId);
+ if (frame instanceof NativeDebugFrame) {
+ pc = ((NativeDebugFrame) frame).getPageContext();
+ }
+ if (pc == null) {
+ for (IDebugFrame f : frameCache.values()) {
+ if (f instanceof NativeDebugFrame) {
+ pc = ((NativeDebugFrame) f).getPageContext();
+ if (pc != null) break;
+ }
+ }
+ }
+
+ if (pc == null) {
+ return new org.eclipse.lsp4j.debug.CompletionItem[0];
+ }
+
+ return doGetCompletionsWithPageContext(pc, partialExpr);
+ }
+
+ private org.eclipse.lsp4j.debug.CompletionItem[] doGetCompletionsWithPageContext(PageContext pc, String partialExpr) {
+ final java.util.List results = new java.util.ArrayList<>();
+
+ try {
+ ClassLoader cl = luceeClassLoader != null ? luceeClassLoader : pc.getClass().getClassLoader();
+
+ // Parse the expression: "local.foo.ba" -> base="local.foo", prefix="ba"
+ // Or just "va" -> base=null, prefix="va"
+ String base = null;
+ String prefix = partialExpr.toLowerCase();
+ int lastDot = partialExpr.lastIndexOf('.');
+
+ if (lastDot > 0) {
+ base = partialExpr.substring(0, lastDot);
+ prefix = partialExpr.substring(lastDot + 1).toLowerCase();
+ }
+
+ if (base != null) {
+ // Evaluate the base to get keys
+ try {
+ Class> tlpcClass = cl.loadClass("lucee.runtime.engine.ThreadLocalPageContext");
+ java.lang.reflect.Method registerMethod = tlpcClass.getMethod("register", PageContext.class);
+ java.lang.reflect.Method releaseMethod = tlpcClass.getMethod("release");
+ Class> evaluateClass = cl.loadClass("lucee.runtime.functions.dynamicEvaluation.Evaluate");
+ java.lang.reflect.Method callMethod = evaluateClass.getMethod("call", PageContext.class, Object[].class);
+
+ registerMethod.invoke(null, pc);
+ try {
+ Object result = callMethod.invoke(null, pc, new Object[]{base});
+ if (result instanceof java.util.Map) {
+ @SuppressWarnings("unchecked")
+ java.util.Map