Skip to content

Commit f39a1b3

Browse files
committed
Materialize jdt:// sources to cache files for clients that only open file paths
Stack frames paused in classes packaged in jars or the JRT runtime are reported with a jdt://contents/... source uri, which only VS Code can render through the jdt TextDocumentContentProvider registered by the Red Hat Java extension. Any other DAP client can only open absolute file paths, so those frames are rendered without source. Materialize the source of such classes (attached source, or decompiled source when java.debug.settings.debugSupportOnDecompiledSource is on) into a cache file under {java.io.tmpdir}/java-debug-sources/{jarName}/{package}/{Type}.java and report its path instead. The decompiled line mapping APIs accept the cache path and resolve it back to the class file. When getFileURI returns null for a class file (JDTUtils.toUri() returns null for jar class files in some language server versions, e.g. the jdt.ls 1.63.0 used by the Zed extension), fall back to resolving the class file via the source containers so the source can still be materialized. Unresolvable sources fall back to SourceType.REMOTE, matching the contract documented on ISourceLookUpProvider#getSource. Refs zed-extensions/java#326
1 parent 5d63891 commit f39a1b3

1 file changed

Lines changed: 158 additions & 2 deletions

File tree

‎com.microsoft.java.debug.plugin/src/main/java/com/microsoft/java/debug/plugin/internal/JdtSourceLookUpProvider.java‎

Lines changed: 158 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
import java.io.InputStreamReader;
1818
import java.net.URI;
1919
import java.net.URISyntaxException;
20+
import java.nio.charset.StandardCharsets;
2021
import java.nio.file.Files;
22+
import java.nio.file.Path;
2123
import java.nio.file.Paths;
2224
import java.util.ArrayList;
2325
import java.util.Arrays;
@@ -82,6 +84,8 @@
8284
import com.microsoft.java.debug.core.adapter.Constants;
8385
import com.microsoft.java.debug.core.adapter.IDebugAdapterContext;
8486
import com.microsoft.java.debug.core.adapter.ISourceLookUpProvider;
87+
import com.microsoft.java.debug.core.adapter.Source;
88+
import com.microsoft.java.debug.core.adapter.SourceType;
8589
import com.microsoft.java.debug.core.protocol.Types.BreakpointLocation;
8690
import com.microsoft.java.debug.core.protocol.Types.SourceBreakpoint;
8791

@@ -404,6 +408,158 @@ public String getSourceFileURI(String fullyQualifiedName, String sourcePath) {
404408
return null;
405409
}
406410

411+
@Override
412+
public Source getSource(String fullyQualifiedName, String sourcePath) {
413+
String uri = getSourceFileURI(fullyQualifiedName, sourcePath);
414+
if (uri == null || uri.startsWith("file:")) {
415+
if (uri == null && sourcePath != null) {
416+
// On newer revisions getFileURI delegates to JDTUtils.toUri(), which may return
417+
// null for class files (depending on the language server version). Fall back to
418+
// resolving the class file directly so its source can still be materialized.
419+
Object sourceElement = JdtUtils.findSourceElement(sourcePath, getSourceContainers());
420+
if (sourceElement instanceof IClassFile) {
421+
String directUri = materializeClassFile((IClassFile) sourceElement);
422+
if (directUri != null) {
423+
return new Source(directUri, SourceType.LOCAL);
424+
}
425+
}
426+
}
427+
return new Source(uri, SourceType.LOCAL);
428+
}
429+
430+
// The jdt:// uri (for classes packaged in a jar or the JRT runtime, with or without
431+
// source attachment) can only be rendered by clients that implement a jdt content
432+
// provider (e.g. VS Code with the Red Hat Java extension). Clients that can only open
433+
// absolute file paths (e.g. Zed) cannot display it. Materialize the source contents
434+
// (attached source, or decompiled source) into a local cache file and hand out its
435+
// path instead, so that every DAP client can display the source of library classes.
436+
String cachedFileUri = materializeSourceToCache(uri);
437+
if (cachedFileUri != null) {
438+
return new Source(cachedFileUri, SourceType.LOCAL);
439+
}
440+
441+
// Fall back to the jdt:// uri marked as remote source, so that clients able to fetch
442+
// contents through the DAP 'source' request can still display it.
443+
return new Source(uri, SourceType.REMOTE);
444+
}
445+
446+
private static final String SOURCE_CACHE_DIR_NAME = "java-debug-sources";
447+
448+
/**
449+
* Maps the file uri of a materialized source cache file back to the handle identifier of
450+
* the underlying class file, so that the decompiled line mapping APIs keep working when
451+
* clients report the cache path.
452+
*/
453+
private final Map<String, String> cachedFileToHandleId = new HashMap<>();
454+
455+
/**
456+
* For non-file (jdt://) source uris, cache the resolved contents (attached source or
457+
* decompiled source) into a local file and return the file uri of the cached file.
458+
* Returns null when the contents cannot be resolved.
459+
*/
460+
private String materializeSourceToCache(String uri) {
461+
IClassFile classFile = resolveClassFile(uri);
462+
if (classFile == null) {
463+
return null;
464+
}
465+
466+
return materializeClassFile(classFile);
467+
}
468+
469+
/**
470+
* Materializes the source of a class file (attached source, or decompiled source when
471+
* debugging on decompiled source is enabled) under
472+
* {java.io.tmpdir}/java-debug-sources/{jarName}/{package}/{Type}.java and returns the
473+
* file uri of the cache file. Returns null when the contents cannot be resolved.
474+
*/
475+
private String materializeClassFile(IClassFile classFile) {
476+
String contents = null;
477+
try {
478+
if (classFile.getSourceRange() != null) {
479+
// Source attachment (e.g. JDK src.zip, maven *-sources.jar) is available.
480+
// Its line numbers match the line table of the compiled class.
481+
contents = getContents(classFile);
482+
} else if (DebugSettings.getCurrent().debugSupportOnDecompiledSource == Switch.ON) {
483+
// No source attachment. When debugging on decompiled source is enabled,
484+
// decompile it with the same decompiler that the VS Code client uses, so that
485+
// clients without a jdt content provider can still display the source.
486+
ContentProviderManager contentProvider = JavaLanguageServerPlugin.getContentProviderManager();
487+
String decompiled = contentProvider.getSource(classFile, new NullProgressMonitor());
488+
if (decompiled != null && !decompiled.isBlank()) {
489+
contents = decompiled;
490+
}
491+
}
492+
} catch (JavaModelException e) {
493+
logger.log(Level.WARNING, String.format("Failed to load the source contents of %s: %s", classFile.getElementName(), e.toString()), e);
494+
} catch (Exception e) {
495+
logger.log(Level.WARNING, String.format("Failed to decompile %s: %s", classFile.getElementName(), e.toString()), e);
496+
}
497+
498+
if (contents == null || contents.isEmpty()) {
499+
return null;
500+
}
501+
502+
try {
503+
String jarName = sanitizeFileName(classFile.getParent().getParent().getElementName());
504+
String packageName = classFile.getParent().getElementName().replace('.', '/');
505+
String typeName = sanitizeFileName(classFile.getElementName().replaceFirst("\\.class$", ".java"));
506+
Path cachedPath = Paths.get(System.getProperty("java.io.tmpdir", "."), SOURCE_CACHE_DIR_NAME, jarName, packageName, typeName);
507+
if (!Files.exists(cachedPath)) {
508+
Files.createDirectories(cachedPath.getParent());
509+
Files.write(cachedPath, contents.getBytes(StandardCharsets.UTF_8));
510+
}
511+
String cachedUri = cachedPath.toUri().toString();
512+
cachedFileToHandleId.put(cachedUri, classFile.getHandleIdentifier());
513+
return cachedUri;
514+
} catch (IOException e) {
515+
logger.log(Level.WARNING, String.format("Failed to cache the source of %s: %s", classFile.getElementName(), e.toString()), e);
516+
return null;
517+
}
518+
}
519+
520+
/**
521+
* Resolves the class file for a source uri that is either a jdt:// uri or the file uri of
522+
* a materialized cache file. Returns null when it cannot be resolved.
523+
*/
524+
private IClassFile resolveClassFileForUri(String uri) {
525+
if (uri == null) {
526+
return null;
527+
}
528+
if (uri.startsWith(JDT_SCHEME)) {
529+
return resolveClassFile(uri);
530+
}
531+
String handleId = cachedFileToHandleId.get(uri);
532+
if (handleId == null) {
533+
// The cache files are reported back by clients as plain file paths or file uris.
534+
try {
535+
String fileName = Paths.get(uri).getFileName().toString();
536+
for (Map.Entry<String, String> entry : cachedFileToHandleId.entrySet()) {
537+
if (isSameURI(entry.getKey(), uri) || entry.getKey().endsWith("/" + fileName)) {
538+
handleId = entry.getValue();
539+
break;
540+
}
541+
}
542+
} catch (Exception e) {
543+
// not a valid file path, ignore.
544+
}
545+
}
546+
if (handleId != null) {
547+
IJavaElement element = JavaCore.create(handleId);
548+
if (element != null) {
549+
return (IClassFile) element.getAncestor(IJavaElement.CLASS_FILE);
550+
}
551+
}
552+
return null;
553+
}
554+
555+
/**
556+
* For the class names loaded from the jrt runtime, the element name may contain
557+
* characters (e.g. '!') that are invalid in file names.
558+
*/
559+
private static String sanitizeFileName(String name) {
560+
return name.replaceAll("[\\\\/:*?\"<>|!]", "_");
561+
}
562+
407563
@Override
408564
public String getJavaRuntimeVersion(String projectName) {
409565
IJavaProject project = JdtUtils.getJavaProject(projectName);
@@ -612,7 +768,7 @@ private boolean isSameURI(String uri1, String uri2) {
612768
}
613769

614770
public int[] getOriginalLineMappings(String uri) {
615-
IClassFile classFile = resolveClassFile(uri);
771+
IClassFile classFile = resolveClassFileForUri(uri);
616772
try {
617773
if (classFile == null) {
618774
return null;
@@ -643,7 +799,7 @@ public int[] getOriginalLineMappings(String uri) {
643799
}
644800

645801
public int[] getDecompiledLineMappings(String uri) {
646-
IClassFile classFile = resolveClassFile(uri);
802+
IClassFile classFile = resolveClassFileForUri(uri);
647803
try {
648804
if (classFile != null && classFile.getSourceRange() == null) {
649805
ContentProviderManager contentProvider = JavaLanguageServerPlugin.getContentProviderManager();

0 commit comments

Comments
 (0)