Skip to content

improvement: Read zip inside the plugin - #2073

Open
tgodzik wants to merge 2 commits into
scalameta:mainfrom
tgodzik:read-zip
Open

improvement: Read zip inside the plugin#2073
tgodzik wants to merge 2 commits into
scalameta:mainfrom
tgodzik:read-zip

Conversation

@tgodzik

@tgodzik tgodzik commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Instead of reading the files in Metals and sending heavy data using LSP it's much easier to just read the jar in VS Code itself. Virtual docs are anyway only supported in VS Code, so any other editors will just get the normal filesystem breadcrumbs.

This has an added benefit of being able to quickly switch between siblings.

Screen.Recording.2026-08-06.at.19.22.53.mov

TODO: Metals focus command needs to be fixed.

Summary by CodeRabbit

  • New Features
    • Added read-only browsing for files and directories inside JAR archives.
    • Scala and Java language features now work with documents opened from JAR files.
    • Protobuf and prototext support now includes JAR-based documents when enabled.
    • Navigation and code intelligence correctly resolve locations within JAR contents.
    • Added reliable handling for missing or invalid JAR resources.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b2be07d1-eda1-4bf1-b001-d61a8e69310b

📥 Commits

Reviewing files that changed from the base of the PR and between 738467b and 1f2c65f.

📒 Files selected for processing (1)
  • src/jarFileSystemProvider.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/jarFileSystemProvider.ts

📝 Walkthrough

Walkthrough

The extension adds a read-only jar-fs filesystem provider. It converts JAR URIs, reads archive entries, registers language-client support, updates navigation, and recognizes supported documents under jar-fs.

Changes

JAR filesystem support

Layer / File(s) Summary
JAR URI parsing and translation
src/jarFileSystemProvider.ts
JAR URIs are parsed and converted to self-contained jar-fs URIs. Reverse resolution uses embedded parameters and registered mappings.
Read-only JAR filesystem provider
src/jarFileSystemProvider.ts
JarFileSystemProvider lists directories, reads files, caches archive data, reports missing entries, ignores file watching, and rejects mutations.
Extension registration and language integration
package.json, src/extension.ts, src/goToLocation.ts, src/documentSelector.ts, src/util.ts, src/test/unit/documentSelector.test.ts
The extension activates on jar-fs, registers the provider, converts language-client and position URIs, updates navigation, registers Scala code lenses, and adds tested jar-fs document selectors.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant VSCode
  participant Extension
  participant LanguageClient
  participant JarFileSystemProvider
  participant yauzl
  VSCode->>Extension: activate onFileSystem:jar-fs
  Extension->>JarFileSystemProvider: register read-only provider
  LanguageClient->>Extension: request JAR document
  Extension->>JarFileSystemProvider: open translated jar-fs URI
  JarFileSystemProvider->>yauzl: load archive entry
  yauzl-->>JarFileSystemProvider: return entry bytes or metadata
  JarFileSystemProvider-->>VSCode: return document content
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: reading JAR files directly inside the VS Code extension.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@tgodzik
tgodzik marked this pull request as ready for review August 6, 2026 17:16

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/jarFileSystemProvider.ts`:
- Around line 130-144: Update the archive indexing logic in getJarEntries so
that, for every archive entry, each missing parent path is added to the entries
map as a FileType.Directory entry. Preserve explicit archive entries and
existing stat/readDirectory behavior, allowing stat() to resolve implicit
package directories such as com/ and com/example/.
- Around line 42-55: Update translateJarToJarFs and translateJarFsToJar to
preserve the original serialized jar Uri in jarFsToJarUri instead of storing
uri.toString() after decoding. Construct the reverse jar-fs value through the
Uri API so encoded archive paths and query components serialize correctly, and
add round-trip coverage for encoded JAR paths.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6733f7c9-d08b-49a0-8862-674eb5fffab4

📥 Commits

Reviewing files that changed from the base of the PR and between f6ccc34 and 73d6904.

📒 Files selected for processing (6)
  • package.json
  • src/documentSelector.ts
  • src/extension.ts
  • src/goToLocation.ts
  • src/jarFileSystemProvider.ts
  • src/test/unit/documentSelector.test.ts

Comment on lines +42 to +55
export function translateJarFsToJar(uri: Uri): string {
if (uri.scheme !== "jar-fs") {
return uri.toString();
}

// First, try to reconstruct from query parameter (self-contained URI)
if (uri.query) {
const params = new URLSearchParams(uri.query);
const jarPath = params.get("jarPath");
if (jarPath) {
// Path format: /jarName.jar/internal/path
const pathParts = uri.path.split("/").filter((p) => p);
const internalPath = pathParts.slice(1).join("/");
return `jar:file://${jarPath}!/${internalPath}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== repository files =="
git ls-files | sed -n '1,120p'

echo "== locate target file =="
fd -a 'jarFileSystemProvider\.ts$' . || true

echo "== target outline =="
ast-grep outline src/jarFileSystemProvider.ts || true

echo "== relevant line ranges with context =="
wc -l src/jarFileSystemProvider.ts
sed -n '1,120p' src/jarFileSystemProvider.ts
sed -n '440,530p' src/jarFileSystemProvider.ts

Repository: scalameta/metals-vscode

Length of output: 10331


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package info for vscode dependency =="
node - <<'JS'
const fs = requirefs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
console.log(JSON.stringify({dependencies: pkg.dependencies && pkg.dependencies.vscode, devDependencies: pkg.devDependencies && pkg.devDependencies.vscode}));
JS

echo "== uri parser / translation usage =="
rg -n "translateJar(Fs)?To(Jar|Fs)|parseJarUri|jarFsToJarUri|JarFileSystemProvider" src test tests . --glob '!**/node_modules/**' || true

echo "== deterministic encode/decode probe =="
node - <<'JS'
const cases = [
  "jar:file:///path/to/my jar.jar!/a b%c.scala",
  "jar:file:///C:/Users/Foo Bar/docs%2Fthing.jar!/src%2FFile.scala",
  "jar:file:///path/to/file#name.jar!/path#name.scala",
  "jar:file:///path/to/file?name.jar!/query?name.scala",
];
for (const jarUri of cases) {
  const decoded = decodeURIComponent(jarUri);
  const sep = decoded.indexOf("!/");
  const fullJarPath = decoded.substring(0, sep);
  const internalPath = decoded.substring(sep + 2);
  const jarName = fullJarPath.split("/").filter(Boolean).slice()
    .pop();
  const selfContained = `jar-fs:/${encodeURIComponent(jarName)}/${encodeURIComponent(internalPath)}?jarPath=${encodeURIComponent(fullJarPath)}`;
  const parts = jarName === jarName ? [] : [];
  const pathParts = selfContained.split("/").filter(Boolean).slice(-1);
  const internalAttempt = pathParts.length ? pathParts.slice(1).join("/") : "";
  console.log("---", jarUri);
  console.log("registry decoded:", decoded);
  console.log("self-contained jarPath query:", decodeURIComponent(selfContained.split("jarPath=")[1]));
  console.log("current path slice attempt:", internalAttempt);
}
JS

Repository: scalameta/metals-vscode

Length of output: 4582


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package info for vscode dependency =="
node - <<'JS'
const fs = requirefs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
console.log(JSON.stringify({dependencies: pkg.dependencies && pkg.dependencies.vscode, devDependencies: pkg.devDependencies && pkg.devDependencies.vscode}));
JS

echo "== deterministic encode/decode probe with error recovery =="
node - <<'JS'
const cases = [
  "jar:file:///path/to/my%20jar.jar!/a%20b%25c.scala",
  "jar:file:///C:/Users/Foo%20Bar/docs%252Fthing.jar!/src%252FFile.scala",
  "jar:file:///path/to/file%23name.jar!/path%23name.scala",
  "jar:file:///path/to/file%3Fname.jar!/query%3Fname.scala",
];
for (const jarUri of cases) {
  let decoded;
  try {
    decoded = decodeURIComponent(jarUri);
  } catch (e) {
    console.log("--- malformed parse", jarUri);
    console.log("decode error:", e.name + ": " + e.message);
    continue;
  }
  const sep = decoded.indexOf("!/");
  const fullJarPath = decoded.substring(0, sep);
  const internalPath = decoded.substring(sep + 2);
  const jarName = fullJarPath.split("/").filter(Boolean).slice().pop();
  const selfContained = `jar-fs:/${encodeURIComponent(jarName)}/${encodeURIComponent(internalPath)}?jarPath=${encodeURIComponent(fullJarPath)}`;
  const parsedQuery = new URLSearchParams(selfContained.split("?")[1] || "");
  const parsedJarPath = parsedQuery.get("jarPath");
  const pathOnly = selfContained
    .split("?")[0]
    .split("/")
    .filter(Boolean);
  const slash = pathOnly.slice(0, 1).join("/");
  const sliceInternal = pathOnly.slice(1).join("/");
  console.log("---", jarUri);
  console.log("decoded:", decoded);
  console.log("registry stored decoded:", decoded);
  console.log("self-contained original:", selfContained);
  console.log("self-contained query jarPath:", parsedJarPath);
  console.log("path-only slash:", slash);
  console.log("path-only slice/internal:", slash + "/" + sliceInternal);
}
JS

Repository: scalameta/metals-vscode

Length of output: 2222


Store and return the raw serialized jar URI during the jarjar-fs round trip.

translateJarToJarFs decodes uri.toString() and stores that decoded form in jarFsToJarUri; translateJarFsToJar then returns either that decoded registry value or a reconstructed string that normalizes percent-encoded #, ?, or path characters. Keep the original Uri in the registry and build the reverse jar-fs URI with Uri so each path/query component serializes correctly. Add round-trip coverage for encoded JAR/archive paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/jarFileSystemProvider.ts` around lines 42 - 55, Update
translateJarToJarFs and translateJarFsToJar to preserve the original serialized
jar Uri in jarFsToJarUri instead of storing uri.toString() after decoding.
Construct the reverse jar-fs value through the Uri API so encoded archive paths
and query components serialize correctly, and add round-trip coverage for
encoded JAR paths.

Comment on lines +130 to +144
const entries = await this.getJarEntries(components.jarPath);
const entry = entries.get(components.internalPath);

if (!entry) {
if (components.internalPath === "" || components.internalPath === "/") {
const stat: FileStat = {
type: FileType.Directory,
ctime: 0,
mtime: 0,
size: 0,
};
this.statCache.set(key, stat);
return stat;
}
throw FileSystemError.FileNotFound(uri);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Index implicit parent directories.

Many ZIP/JAR files contain com/example/Foo.scala without explicit com/ and com/example/ entries. readDirectory() can discover these paths by prefix, but stat() rejects them because entries.get(components.internalPath) is absent. VS Code can therefore fail to open or navigate package directories.

Add each parent path as a FileType.Directory entry while indexing every archive entry.

Proposed fix
           entries.set(name, {
             name: name.split("/").pop() || name,
             type: isDirectory ? FileType.Directory : FileType.File,
             size: entry.uncompressedSize,
           });
+
+          const segments = name.split("/");
+          for (let i = 1; i < segments.length; i++) {
+            const directoryPath = segments.slice(0, i).join("/");
+            entries.set(directoryPath, {
+              name: segments[i - 1],
+              type: FileType.Directory,
+              size: 0,
+            });
+          }

Also applies to: 370-382

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/jarFileSystemProvider.ts` around lines 130 - 144, Update the archive
indexing logic in getJarEntries so that, for every archive entry, each missing
parent path is added to the entries map as a FileType.Directory entry. Preserve
explicit archive entries and existing stat/readDirectory behavior, allowing
stat() to resolve implicit package directories such as com/ and com/example/.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant