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
10 changes: 6 additions & 4 deletions scripts/e2e-session-persistence.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,7 @@ test('e2e: formats the installed extension version from either extension namespa
assert.equal(getExtensionVersionLabel({}), '');
});

test('e2e: default no-workspace source uses an untitled tab that cannot compile', async () => {
test('e2e: fresh no-workspace state has no open file and cannot compile', async () => {
const originalDocument = global.document;
global.document = createFakeDocument();
try {
Expand All @@ -565,9 +565,11 @@ test('e2e: default no-workspace source uses an untitled tab that cannot compile'

resetToNewProject();

assert.deepEqual(getToolbarOpenTabPaths(), ['untitled:default']);
assert.equal(getToolbarActiveTabPath(), 'untitled:default');
assert.equal(global.document.getElementById('tab-bar').children[0].children[0].textContent, 'unsaved file');
assert.deepEqual(getToolbarOpenTabPaths(), []);
assert.equal(getToolbarActiveTabPath(), null);
assert.equal(editorValue, '');
assert.equal(global.document.getElementById('tab-bar').children.length, 0);
assert.equal(global.document.getElementById('status-file').textContent, '');

await assert.rejects(() => assembleCompilePayload({}), /Open a folder or save a file/);
} finally {
Expand Down
37 changes: 36 additions & 1 deletion scripts/e2e-workspace-file-tracking.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -954,14 +954,49 @@ test('e2e: toolbar compilation requires a ready compiler and root-level C/C++ so

test('e2e: New file with no workspace opens the folder picker; cancel leaves state unchanged', async () => {
const ctx = await setupToolbar({ openFolderResult: null });
ctx.toolbar.resetToNewProject(); // no workspace, single main.cpp tab
ctx.toolbar.resetToNewProject();

ctx.document.getElementById('btn-new').click();
await tick();

assert.equal(ctx.fsCalls.openFolder, 1, 'folder picker invoked');
assert.equal(inlineInput(ctx.document), null, 'no inline input after cancel');
assert.equal(ctx.fsCalls.create.length, 0, 'no file created');
assert.deepEqual(ctx.toolbar.getOpenTabPaths(), [], 'no synthetic tab created');
});

test('e2e: Open Folder renders README in Explorer without opening it', async () => {
const ctx = await setupToolbar({
openFolderResult: { name: 'project', entries: [{ path: 'README.md', kind: 'file' }] },
});
ctx.toolbar.resetToNewProject();

ctx.document.getElementById('btn-open').click();
await tick();

assert.deepEqual(renderedTreePaths(ctx.document), ['README.md']);
assert.deepEqual(ctx.toolbar.getOpenTabPaths(), []);
assert.equal(ctx.toolbar.getActiveTabPath(), null);
assert.deepEqual(ctx.editorCalls.setValue.slice(-1), ['']);
});

test('e2e: saving from the empty state creates and opens a workspace file', async () => {
const originalPrompt = global.prompt;
global.prompt = () => 'main.cpp';
try {
const ctx = await setupToolbar({ openFolderResult: { name: 'project', entries: [] } });
ctx.toolbar.resetToNewProject();

ctx.document.getElementById('btn-save').click();
await tick();
await tick();

assert.deepEqual(ctx.fsCalls.create.map((file) => file.path), ['main.cpp']);
assert.deepEqual(ctx.toolbar.getOpenTabPaths(), ['main.cpp']);
assert.equal(ctx.toolbar.getActiveTabPath(), 'main.cpp');
} finally {
global.prompt = originalPrompt;
}
});

test('e2e: New file with a workspace shows an inline Explorer naming input', async () => {
Expand Down
2 changes: 0 additions & 2 deletions src/ui/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import {
getOpenTabsSnapshot,
restoreWorkspace,
resetToNewProject,
restoreNoWorkspaceSource,
assembleCompilePayload,
applyWorkspaceSnapshot,
} from './toolbar.js';
Expand Down Expand Up @@ -111,7 +110,6 @@ window.addEventListener('DOMContentLoaded', async () => {
getActiveTabPath,
getOpenTabsSnapshot,
restoreWorkspace,
restoreNoWorkspaceSource,
confirmReload: promptReloadPreviousProject,
startNewProject: resetToNewProject,
setExplorerLoading: (loading) => toolbarController?.setExplorerLoading(loading),
Expand Down
11 changes: 1 addition & 10 deletions src/ui/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,6 @@ self.MonacoEnvironment = {
},
};

// ── Default C++ starter source ────────────────────────────────────────────────
export const DEFAULT_SOURCE = `#include <iostream>

int main() {
std::cout << "Hello, World!" << std::endl;
return 0;
}
`;

// ── Internal state ────────────────────────────────────────────────────────────
let _editor = null;

Expand Down Expand Up @@ -78,7 +69,7 @@ export function createEditor(container) {
});

_editor = monaco.editor.create(container, {
value: DEFAULT_SOURCE,
value: '',
language: 'cpp',
theme: 'browser-cpp-dark',
fontSize: 14,
Expand Down
3 changes: 1 addition & 2 deletions src/ui/session-persistence.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,7 @@ export function createSessionPersistence({
if (typeof fsAPI.resetWorkspace === 'function') fsAPI.resetWorkspace();
}

// Abandon the saved session and load the default new-project state
// (no workspace, a `main.cpp` tab with editorAPI.DEFAULT_SOURCE).
// Abandon the saved session and return to the empty no-workspace state.
async function abandonForNewProject() {
await clearPersistedSession();
await startNewProject();
Expand Down
66 changes: 23 additions & 43 deletions src/ui/toolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,6 @@ const _openTabs = new Map();
let _activeTabPath = null;
/** When true, programmatic setValue calls do not trigger markDirty(true). */
let _loadingFile = false;
// Internal in-memory document identifier; never a workspace-relative path.
const UNSAVED_TAB_PATH = 'untitled:default';
const UNSAVED_TAB_LABEL = 'unsaved file';

// ── Session persistence callback ──────────────────────────────────────────────
/** Optional callback supplied by app.js to persist the session after state changes. */
let _persistSession = null;
Expand Down Expand Up @@ -613,14 +609,23 @@ async function reloadOverwrittenTabs(changedPaths) {
}

/**
* Load the default new-project state (no workspace, a single unsaved tab with
* `editorAPI.DEFAULT_SOURCE`). Unlike {@link actionNew} this skips the
* Load the empty new-project state. Unlike {@link actionNew} this skips the
* unsaved-changes confirmation so it can drive the relaunch "Start new project"
* path, where the prior session is being intentionally abandoned.
*/
export function resetToNewProject() {
clearTransientProjectState();
restoreNoWorkspaceSource(_editorAPI.DEFAULT_SOURCE ?? '');
closeAllTabs();
_fsAPI.newFile();
clearWorkspaceMode();
_terminalAPI.resetTerminalSession?.(null);
_fileName = '';
_loadingFile = true;
_editorAPI.setValue('');
_editorAPI.clearDiagnostics();
_loadingFile = false;
const statusFile = document.getElementById('status-file');
if (statusFile) statusFile.textContent = '';
}

function clearTransientProjectState() {
Expand All @@ -630,16 +635,6 @@ function clearTransientProjectState() {
_editorAPI.clearDiagnostics?.();
}

/** Restore a source-only session into the same no-workspace tab state as a new project. */
export function restoreNoWorkspaceSource(source) {
closeAllTabs();
_fsAPI.newFile();
clearWorkspaceMode();
_terminalAPI.resetTerminalSession?.(null);
openTabForFile(UNSAVED_TAB_PATH, source);
markDirty(false);
}

async function actionSave() {
try {
if (_workspace && _activeTabPath && _fsAPI?.writeWorkspaceFile) {
Expand Down Expand Up @@ -679,19 +674,18 @@ async function saveUntitledDocument() {

setWorkspaceMode(result.snapshot ?? workspace);
applyWorkspaceSnapshot(result.snapshot ?? workspace);
renameActiveTabPath(result.path);
openTabForFile(result.path, _editorAPI.getValue());
markDirty(false);
_persistSession?.();
}

async function actionSaveAs() {
try {
if (!_workspace && _activeTabPath === UNSAVED_TAB_PATH) {
if (!_workspace && !_activeTabPath) {
await saveUntitledDocument();
return;
}
const suggestedName = _activeTabPath === UNSAVED_TAB_PATH ? 'main.cpp' : _fileName;
const name = await _fsAPI.saveFileAs(_editorAPI.getValue(), suggestedName);
const name = await _fsAPI.saveFileAs(_editorAPI.getValue(), _fileName || 'main.cpp');
if (name) {
renameActiveTabPath(name);
markDirty(false);
Expand Down Expand Up @@ -843,7 +837,7 @@ function inferLanguage(path) {
}

function tabDisplayName(path) {
return path === UNSAVED_TAB_PATH ? UNSAVED_TAB_LABEL : workspaceBaseName(path) || path;
return workspaceBaseName(path) || path;
}

/** Returns true if any open tab has unsaved changes. */
Expand Down Expand Up @@ -1114,19 +1108,6 @@ function highlightWorkspaceFile(path) {
if (active) active.classList.add('active');
}

async function openWorkspaceInitialFile(workspace) {
const file = pickInitialWorkspaceFile(workspace.entries);
if (!file) {
// No README.md at root – clear editor but open no tab automatically
_loadingFile = true;
_editorAPI.setValue('');
_editorAPI.clearDiagnostics();
_loadingFile = false;
return;
}
await openWorkspaceFile(file.path);
}

async function openWorkspaceFile(path) {
if (_openTabs.has(path)) {
switchToTab(path);
Expand Down Expand Up @@ -1160,13 +1141,6 @@ async function openWorkspaceFile(path) {
openTabForFile(path, content);
}

function pickInitialWorkspaceFile(entries) {
// Only auto-open README.md if it exists at the workspace root
return entries.find(
(entry) => entry.kind === 'file' && entry.path.toLowerCase() === 'readme.md'
) || null;
}

function showOpenError(err) {
const kind = _workspace ? 'folder' : 'file';
alert(`Could not open ${kind}:\n${err.message}`);
Expand Down Expand Up @@ -1274,7 +1248,13 @@ async function openFolderWorkspace() {
clearTransientProjectState();
closeAllTabs();
setWorkspaceMode(workspace);
await openWorkspaceInitialFile(workspace);
_fileName = '';
_loadingFile = true;
_editorAPI.setValue('');
_editorAPI.clearDiagnostics();
_loadingFile = false;
const statusFile = document.getElementById('status-file');
if (statusFile) statusFile.textContent = '';
renderWorkspaceSidebar(workspace);
_persistSession?.(); // persist immediately so the new workspace survives unload
return true;
Expand Down
Loading