Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"name": "browser.cpp",
"short_name": "browser.cpp",
"description": "In-browser C++20 IDE powered by Monaco Editor and WASM Clang",
"version": "0.4.1",
"version": "0.4.2",
"minimum_chrome_version": "105",
"icons": {
"16": "icons/icon16.png",
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "browser.cpp",
"version": "0.4.1",
"version": "0.4.2",
"description": "In-browser C++20 IDE with WASM Clang toolchain",
"private": true,
"scripts": {
Expand Down
92 changes: 90 additions & 2 deletions scripts/e2e-workspace-file-tracking.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -622,6 +622,7 @@ class FakeElement {
this.children = [];
this.listeners = new Map();
this.dataset = {};
this.attributes = new Map();
this.style = {};
this._className = '';
this.classList = {
Expand Down Expand Up @@ -693,8 +694,9 @@ class FakeElement {
setAttribute(name, value) {
if (name === 'id') { this.id = value; return; }
if (name.startsWith('data-')) this.dataset[name.slice(5)] = value;
this.attributes.set(name, String(value));
}
getAttribute() { return null; }
getAttribute(name) { return this.attributes.get(name) ?? null; }

querySelectorAll(selector) {
if (selector !== 'li') return [];
Expand Down Expand Up @@ -844,6 +846,12 @@ function renderedTreePaths(document) {
.filter(Boolean) ?? [];
}

function renderedTreeItem(document, path) {
return document.getElementById('file-tree')
?.querySelectorAll('li')
.find((item) => item.dataset.path === path) ?? null;
}

function shortcutEvent(key, { metaKey = false, ctrlKey = false, shiftKey = false } = {}) {
let prevented = false;
return {
Expand Down Expand Up @@ -926,6 +934,85 @@ test('e2e: submitting a nested path creates parent directories and opens the fil
assert.deepEqual(ctx.fsCalls.create.map((c) => c.path), ['src/lib/util.hpp']);
assert.ok(ctx.toolbar.getOpenTabPaths().includes('src/lib/util.hpp'));
assert.ok(ctx.terminalCalls.refresh.length >= 1, 'terminal workspace refreshed');
assert.deepEqual(renderedTreePaths(ctx.document), ['src', 'src/lib', 'src/lib/util.hpp']);
assert.equal(renderedTreeItem(ctx.document, 'src').getAttribute('aria-expanded'), 'true');
assert.equal(renderedTreeItem(ctx.document, 'src/lib').getAttribute('aria-expanded'), 'true');
});

test('e2e: restored nested directories start collapsed until the user expands each level', async () => {
const ctx = await setupToolbar();
await ctx.toolbar.restoreWorkspace({
name: 'p',
entries: [
{ path: 'src', kind: 'directory' },
{ path: 'src/lib', kind: 'directory' },
{ path: 'src/lib/util.hpp', kind: 'file' },
],
}, [], null);

assert.deepEqual(renderedTreePaths(ctx.document), ['src']);
assert.equal(renderedTreeItem(ctx.document, 'src').getAttribute('aria-expanded'), 'false');

renderedTreeItem(ctx.document, 'src').click();
assert.deepEqual(renderedTreePaths(ctx.document), ['src', 'src/lib']);
assert.equal(renderedTreeItem(ctx.document, 'src/lib').getAttribute('aria-expanded'), 'false');

renderedTreeItem(ctx.document, 'src/lib').click();
assert.deepEqual(renderedTreePaths(ctx.document), ['src', 'src/lib', 'src/lib/util.hpp']);
});

test('e2e: refresh keeps expanded directories but prunes directories that no longer exist', async () => {
const ctx = await setupToolbar();
const initial = {
name: 'p',
entries: [
{ path: 'src', kind: 'directory' },
{ path: 'src/lib', kind: 'directory' },
{ path: 'src/lib/util.hpp', kind: 'file' },
],
};
await ctx.toolbar.restoreWorkspace(initial, [], null);

renderedTreeItem(ctx.document, 'src').click();
ctx.toolbar.applyWorkspaceSnapshot({
name: 'p',
entries: [...initial.entries, { path: 'src/lib/deep.hpp', kind: 'file' }],
});

assert.deepEqual(renderedTreePaths(ctx.document), ['src', 'src/lib']);
assert.equal(renderedTreeItem(ctx.document, 'src').getAttribute('aria-expanded'), 'true');
assert.equal(renderedTreeItem(ctx.document, 'src/lib').getAttribute('aria-expanded'), 'false');

ctx.toolbar.applyWorkspaceSnapshot({ name: 'p', entries: [] });
ctx.toolbar.applyWorkspaceSnapshot(initial);
assert.equal(renderedTreeItem(ctx.document, 'src').getAttribute('aria-expanded'), 'false');
});

test('e2e: external disk refresh does not expand newly added nested directories', async () => {
const ctx = await setupToolbar({
refreshWorkspaceResult: {
snapshot: {
name: 'p',
entries: [
{ path: 'generated', kind: 'directory' },
{ path: 'generated/output.txt', kind: 'file' },
],
},
added: [
{ path: 'generated', kind: 'directory' },
{ path: 'generated/output.txt', kind: 'file' },
],
removed: [],
changed: [],
},
});
await ctx.toolbar.restoreWorkspace({ name: 'p', entries: [] }, [], null);

ctx.document.dispatch('visibilitychange');
await tick();

assert.deepEqual(renderedTreePaths(ctx.document), ['generated']);
assert.equal(renderedTreeItem(ctx.document, 'generated').getAttribute('aria-expanded'), 'false');
});

test('e2e: Escape cancels inline creation and removes the row cleanly', async () => {
Expand Down Expand Up @@ -1224,7 +1311,8 @@ test('e2e: runtime-created files appear in the refreshed workspace snapshot', as
const latestWorkspace = ctx.terminalCalls.refresh.at(-1);
assert.ok(latestWorkspace.entries.some((entry) => entry.path === 'generated' && entry.kind === 'directory'));
assert.ok(latestWorkspace.entries.some((entry) => entry.path === createdPath && entry.kind === 'file'));
assert.deepEqual(renderedTreePaths(ctx.document), ['generated', createdPath]);
assert.deepEqual(renderedTreePaths(ctx.document), ['generated']);
assert.equal(renderedTreeItem(ctx.document, 'generated').getAttribute('aria-expanded'), 'false');
});

test('e2e: runtime file writes warn when no writable folder workspace is open', async () => {
Expand Down
31 changes: 16 additions & 15 deletions src/ui/toolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ async function handleWorkerMessage(data) {
}
}
if (snapshot) {
applyWorkspaceSnapshot(snapshot, changedPaths);
applyWorkspaceSnapshot(snapshot);
await reloadOverwrittenTabs(changedPaths);
}
}
Expand Down Expand Up @@ -445,7 +445,8 @@ async function submitInlineFileCreation(rawName, errorEl, onReject) {
}

endInlineFileCreation();
applyWorkspaceSnapshot(result.snapshot, [result.path]);
applyWorkspaceSnapshot(result.snapshot);
revealWorkspacePath(result.path);
openTabForFile(result.path, '');
markDirty(false);
highlightWorkspaceFile(result.path);
Expand All @@ -467,26 +468,27 @@ function inlineCreateErrorMessage(error) {

/**
* Adopt a refreshed workspace snapshot after an incremental mutation: update the
* in-memory workspace, refresh the terminal index (preserving cwd), expand the
* ancestor directories of any newly created paths so they are visible, and
* re-render the Explorer.
* in-memory workspace, refresh the terminal index (preserving cwd), preserve
* the user's directory expansion choices, and re-render the Explorer.
*
* @param {object} snapshot – workspace snapshot from filesystem.js
* @param {string[]} [revealPaths] – paths whose ancestor dirs should be expanded
*/
export function applyWorkspaceSnapshot(snapshot, revealPaths = []) {
export function applyWorkspaceSnapshot(snapshot) {
if (!snapshot) return;
_workspace = snapshot;
_terminalAPI.refreshWorkspace?.(snapshot);
pruneExpandedWorkspaceDirectories(snapshot);
for (const path of revealPaths) {
for (const dir of directoriesForPath(path)) {
_expandedWorkspaceDirectories.add(dir);
}
}
renderWorkspaceSidebar(snapshot);
}

/** Reveal a path created through the Explorer's explicit New File action. */
function revealWorkspacePath(path) {
for (const dir of directoriesForPath(path)) {
_expandedWorkspaceDirectories.add(dir);
}
renderWorkspaceSidebar(_workspace);
}

function pruneExpandedWorkspaceDirectories(snapshot) {
const existingDirs = new Set(
(snapshot.entries || [])
Expand All @@ -511,8 +513,7 @@ async function syncWorkspaceFromDisk(reason) {
if (!result?.snapshot) return null;
if (!hasWorkspaceDiff(result)) return result;

const revealPaths = (result.added || []).map((entry) => normalizeOverlayPath(entry.path));
applyWorkspaceSnapshot(result.snapshot, revealPaths);
applyWorkspaceSnapshot(result.snapshot);

const changedPaths = (result.changed || []).map((entry) => normalizeOverlayPath(entry.path));
if (changedPaths.length) {
Expand Down Expand Up @@ -548,7 +549,7 @@ function hasWorkspaceDiff(result) {
async function persistWorkspaceFile(path, bytes) {
try {
const snapshot = await _fsAPI.writeWorkspaceFile(path, bytes);
if (snapshot) applyWorkspaceSnapshot(snapshot, [normalizeOverlayPath(path)]);
if (snapshot) applyWorkspaceSnapshot(snapshot);
await syncWorkspaceFromDisk('compile-result');
} catch (err) {
console.warn('[browser.cpp] Failed to persist workspace file:', path, err);
Expand Down
Loading