mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-10-25 00:02:16 +00:00
vscode: Remove Project command, project deletion detection
This commit is contained in:
1 parent
cdc6687e50
commit
28aaf16b36
7 files changed
+178
-5
No files matched your search
@@ -80,6 +80,8 @@ Click the status bar, then choose one of these:
|
||||
|
||||
- **Change Platform...** to pick another platform.
|
||||
- **Detect Again** to look at the clues again.
|
||||
- **Remove Project...** to clear its settings, so it isn't a project at
|
||||
all. See [Remove a project](projects.md#remove-a-project).
|
||||
|
||||
## From the command line
|
||||
|
||||
|
||||
@@ -74,6 +74,25 @@ folder already has a project, you choose one of these:
|
||||
|
||||
Or click the status bar and choose **Change Main File...**.
|
||||
|
||||
## Remove a project
|
||||
|
||||
Click the status bar and choose **Remove Project...**. It clears the
|
||||
project's settings, so the files stop being built, and detection can
|
||||
offer the project again the next time you open the folder.
|
||||
|
||||
- A top-level project clears the folder's `platform`, `mainFile`, and
|
||||
`tool` settings. Other projects in `8bitworkshop.folders` stay.
|
||||
- A project in a subfolder removes just its `8bitworkshop.folders`
|
||||
entry.
|
||||
- A project that comes from a README badge isn't in settings, so the
|
||||
command offers to open the README instead.
|
||||
|
||||
Editing `.vscode/settings.json` by hand does the same thing: when the
|
||||
last of a project's settings goes away, its errors in **Problems**
|
||||
clear, its language highlighting ends, and a running program for it
|
||||
stops. There's no **Undo**; set a main file or accept a detection prompt
|
||||
to make the project again.
|
||||
|
||||
## Keep a list of programs to run
|
||||
|
||||
To switch between programs with <kbd>F5</kbd>:
|
||||
|
||||
@@ -141,6 +141,11 @@
|
||||
"title": "Detect Projects",
|
||||
"category": "8bitworkshop"
|
||||
},
|
||||
{
|
||||
"command": "8bitworkshop.removeProject",
|
||||
"title": "Remove Project...",
|
||||
"category": "8bitworkshop"
|
||||
},
|
||||
{
|
||||
"command": "8bitworkshop.addLaunchConfiguration",
|
||||
"title": "Add Launch Configuration",
|
||||
@@ -424,6 +429,10 @@
|
||||
"command": "8bitworkshop.copyToWorkspace",
|
||||
"when": "resourceScheme == 8bws-preset"
|
||||
},
|
||||
{
|
||||
"command": "8bitworkshop.removeProject",
|
||||
"when": "8bitworkshop.canRun"
|
||||
},
|
||||
{
|
||||
"command": "8bitworkshop.pause",
|
||||
"when": "8bitworkshop.emuRunning"
|
||||
|
||||
@@ -94,6 +94,7 @@ export function activate(ctx: vscode.ExtensionContext) {
|
||||
command('setMainFile', (uri?: vscode.Uri) => setMainFile(uri));
|
||||
command('changeMainFile', () => changeMainFile());
|
||||
command('selectPlatform', () => changePlatform());
|
||||
command('removeProject', () => removeProject());
|
||||
command('newProject', (platformId?: string) => newProject(platformId));
|
||||
command('openExample', () => openExample());
|
||||
command('copyToWorkspace', (uri?: vscode.Uri) => copyToWorkspace(uri));
|
||||
@@ -135,7 +136,7 @@ export function activate(ctx: vscode.ExtensionContext) {
|
||||
vscode.workspace.onDidChangeTextDocument(e => onChange(e)),
|
||||
vscode.window.onDidChangeActiveTextEditor(() => { updateStatus(); detectForActiveFile(); }),
|
||||
vscode.workspace.onDidOpenTextDocument(doc => assignLanguage(doc)),
|
||||
scope.onDidChange(() => { updateStatus(); vscode.workspace.textDocuments.forEach(assignLanguage); }),
|
||||
scope.onDidChange(() => onProjectsChanged()),
|
||||
vscode.workspace.onDidChangeWorkspaceFolders(() => detectProjects(false)),
|
||||
{ dispose: () => { builds?.dispose(); emu?.dispose(); } },
|
||||
);
|
||||
@@ -370,6 +371,26 @@ function lastTool(t: Target): string | undefined {
|
||||
return scope.lastBuildOf(t.main.fsPath)?.tool;
|
||||
}
|
||||
|
||||
/** Settings changed, in the editor or by us: re-read, and drop what's stale. */
|
||||
function onProjectsChanged() {
|
||||
// a project removed through settings.json shouldn't leave errors behind
|
||||
releaseHeldDiagnostics(false);
|
||||
clearOrphanDiagnostics();
|
||||
// a project that no longer exists can't keep running
|
||||
if (running?.project && !scope.projectFor(running.main)) panel?.dispose();
|
||||
updateStatus();
|
||||
vscode.workspace.textDocuments.forEach(assignLanguage);
|
||||
}
|
||||
|
||||
/** Delete diagnostics for files no project owns any more. */
|
||||
function clearOrphanDiagnostics() {
|
||||
var orphans: vscode.Uri[] = [];
|
||||
diagnostics.forEach(uri => {
|
||||
if (uri.scheme === 'file' && !scope.projectFor(uri)) orphans.push(uri);
|
||||
});
|
||||
for (var uri of orphans) diagnostics.delete(uri);
|
||||
}
|
||||
|
||||
async function projectMenu() {
|
||||
var target = currentTarget();
|
||||
var project = target?.project;
|
||||
@@ -400,6 +421,7 @@ async function projectMenu() {
|
||||
if (project.mainFile && target.main.fsPath !== project.mainFile)
|
||||
items.push({ label: '$(debug-alt) Add Launch Configuration', description: `for ${describeTarget(target)}`, run: () => addLaunchConfiguration() });
|
||||
items.push({ label: '$(search) Detect Again', run: () => detectAgain(project) });
|
||||
items.push({ label: '$(trash) Remove Project...', run: () => removeProject() });
|
||||
}
|
||||
var picked = await vscode.window.showQuickPick(items, { title: '8bitworkshop' });
|
||||
await picked?.run?.();
|
||||
@@ -889,6 +911,35 @@ async function detectAgain(project: Project) {
|
||||
if (choice) await scope.updateProject(project, { platform: choice.platform, tool: choice.tool || null });
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a project's settings, so it stops building and detection can offer
|
||||
* it again. A project that comes from a README badge lives in the README.
|
||||
*/
|
||||
async function removeProject() {
|
||||
var uri = activeUri();
|
||||
var project = currentTarget()?.project || scope.projectFor(uri);
|
||||
if (!project) {
|
||||
vscode.window.showInformationMessage("8bitworkshop: this file isn't part of a project.");
|
||||
return;
|
||||
}
|
||||
if (project.origin === 'readme') {
|
||||
var readme = vscode.Uri.joinPath(vscode.Uri.file(project.folder || project.scope), 'README.md');
|
||||
var open = await vscode.window.showInformationMessage(
|
||||
'This project comes from the 8bitworkshop link in README.md, not from settings.', 'Open README.md');
|
||||
if (open === 'Open README.md') vscode.window.showTextDocument(readme).then(undefined, e => output.appendLine(`readme: ${e}`));
|
||||
return;
|
||||
}
|
||||
var where = project.origin === 'settings' ? 'this folder'
|
||||
: project.folder ? path.relative(project.folder, project.scope) : project.scope;
|
||||
var answer = await vscode.window.showWarningMessage(
|
||||
`Remove the ${platformName(templates, project.platform)} project for ${where}?`,
|
||||
{ modal: true, detail: 'Build and Run stop treating these files as 8bitworkshop. Detection may offer the project again when the folder reopens.' },
|
||||
'Remove Project');
|
||||
if (answer !== 'Remove Project') return;
|
||||
if (await scope.removeProject(project)) vscode.window.showInformationMessage('8bitworkshop: project removed.');
|
||||
else vscode.window.showWarningMessage("8bitworkshop: couldn't remove this project from settings.");
|
||||
}
|
||||
|
||||
////// templates
|
||||
|
||||
async function newProject(platformId?: string) {
|
||||
|
||||
@@ -141,6 +141,23 @@ export function listProjects(ctx: ProjectContext): Project[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* A folder's settings with project `p` removed: the settings and folders to
|
||||
* persist. Undefined when `p` isn't stored in this folder -- it comes from a
|
||||
* README badge, a window with no folder, or another folder. A top-level
|
||||
* project clears the folder's settings; a subproject drops its `folders` entry.
|
||||
*/
|
||||
export function withoutProject(folder: FolderInfo, p: Project): { settings: ProjectSettings, folders: { [dir: string]: ProjectSettings } } | undefined {
|
||||
if (p.folder !== folder.path) return undefined;
|
||||
if (p.origin === 'settings') return { settings: {}, folders: folder.folders || {} };
|
||||
if (p.origin === 'folders' && p.folderKey) {
|
||||
var folders = { ...(folder.folders || {}) };
|
||||
delete folders[p.folderKey];
|
||||
return { settings: folder.settings, folders };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The project that owns `file`, or undefined if it isn't ours. In order: the
|
||||
* most specific 8bitworkshop.folders entry, the folder's settings, its README
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { parseReadmeBadge } from '../../src/common/detect';
|
||||
import { FolderInfo, Project, ProjectContext, ProjectSettings, RunTargetChoice, isInside, listProjects, projectFor, resolveRunTarget } from './projectinfo';
|
||||
import { FolderInfo, Project, ProjectContext, ProjectSettings, RunTargetChoice, isInside, listProjects, projectFor, resolveRunTarget, withoutProject } from './projectinfo';
|
||||
|
||||
export const CONFIG = '8bitworkshop';
|
||||
const RECENT_BUILDS = 6;
|
||||
@@ -70,8 +70,8 @@ export class ProjectScope implements vscode.Disposable {
|
||||
this.emitter.fire();
|
||||
}
|
||||
|
||||
context(): ProjectContext {
|
||||
var folders: FolderInfo[] = (vscode.workspace.workspaceFolders || [])
|
||||
private folderInfos(): FolderInfo[] {
|
||||
return (vscode.workspace.workspaceFolders || [])
|
||||
.filter(f => f.uri.scheme === 'file')
|
||||
.map(f => {
|
||||
var cfg = vscode.workspace.getConfiguration(CONFIG, f.uri);
|
||||
@@ -86,6 +86,15 @@ export class ProjectScope implements vscode.Disposable {
|
||||
readme: this.readmes.get(f.uri.fsPath),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
context(): ProjectContext {
|
||||
var folders = this.folderInfos();
|
||||
// a build record outlives a project edited out of settings.json; drop it,
|
||||
// so removing the settings really removes the project
|
||||
var known = new Set(listProjects({ folders }).map(p => p.scope));
|
||||
for (var w of this.windowProjects) known.add(w.scope);
|
||||
this.builds = this.builds.filter(b => known.has(b.project.scope));
|
||||
return { folders, window: this.windowProjects, builds: this.builds };
|
||||
}
|
||||
|
||||
@@ -187,6 +196,41 @@ export class ProjectScope implements vscode.Disposable {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forget a project: clear its settings, so detection can offer it again.
|
||||
* A project from a README badge or with no folder can't be cleared here.
|
||||
* Returns true if the project is gone.
|
||||
*/
|
||||
async removeProject(p: Project): Promise<boolean> {
|
||||
if (p.origin === 'window') {
|
||||
this.windowProjects = this.windowProjects.filter(w => !(w.origin === 'window' && w.scope === p.scope));
|
||||
this.memoryTargets.delete(p.scope);
|
||||
this.emitter.fire();
|
||||
return true;
|
||||
}
|
||||
var folder = this.folderInfos().find(f => f.path === p.folder);
|
||||
if (!folder) return false;
|
||||
var next = withoutProject(folder, p);
|
||||
if (!next) return false;
|
||||
var wsFolder = (vscode.workspace.workspaceFolders || []).find(f => f.uri.fsPath === p.folder);
|
||||
if (!wsFolder) return false;
|
||||
var cfg = vscode.workspace.getConfiguration(CONFIG, wsFolder.uri);
|
||||
var target = (vscode.workspace.workspaceFolders || []).length > 1 ? vscode.ConfigurationTarget.WorkspaceFolder : vscode.ConfigurationTarget.Workspace;
|
||||
await cfg.update('platform', next.settings.platform || undefined, target);
|
||||
await cfg.update('mainFile', next.settings.mainFile || undefined, target);
|
||||
await cfg.update('tool', next.settings.tool || undefined, target);
|
||||
await cfg.update('folders', Object.keys(next.folders).length ? next.folders : undefined, target);
|
||||
await this.forgetTarget(p);
|
||||
this.emitter.fire();
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Drop the run target stored for a project that no longer exists. */
|
||||
private async forgetTarget(p: Project) {
|
||||
if (p.origin === 'window' || !p.folder) this.memoryTargets.delete(p.scope);
|
||||
else await this.state.update(this.targetKey(p), undefined);
|
||||
}
|
||||
|
||||
/** Change one setting of an existing project, where it came from. */
|
||||
async updateProject(p: Project, change: { platform?: string, mainFile?: string | null, tool?: string | null }) {
|
||||
if (p.origin === 'window') {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as assert from 'assert';
|
||||
import * as path from 'path';
|
||||
import { FolderInfo, Project, asmLanguageFor, isOwnExtension, projectFor, resolveRunTarget } from '../src/projectinfo';
|
||||
import { FolderInfo, Project, asmLanguageFor, isOwnExtension, projectFor, resolveRunTarget, withoutProject } from '../src/projectinfo';
|
||||
import { BuildReason, BuildScheduler, Clock } from '../src/autobuild';
|
||||
|
||||
const WS = path.resolve('/work/game');
|
||||
@@ -83,6 +83,37 @@ describe('projectFor', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('withoutProject', function () {
|
||||
var folder: FolderInfo = {
|
||||
path: WS,
|
||||
settings: { platform: 'nes', mainFile: 'src/main.c', tool: 'cc65' },
|
||||
folders: {
|
||||
'tests': { platform: 'nes' },
|
||||
'tests/c64': { platform: 'c64', mainFile: 'demo.c', tool: 'cc65' },
|
||||
},
|
||||
};
|
||||
var project = (over: Partial<Project>): Project => ({ scope: WS, root: WS, platform: 'nes', origin: 'settings', folder: WS, ...over });
|
||||
|
||||
it('clears a top-level project but keeps subproject entries', function () {
|
||||
var next = withoutProject(folder, project({ origin: 'settings' }));
|
||||
assert.deepEqual(next.settings, {});
|
||||
assert.deepEqual(next.folders, folder.folders);
|
||||
});
|
||||
|
||||
it('drops only a subproject entry, keeping the rest and the settings', function () {
|
||||
var next = withoutProject(folder, project({ scope: at('tests/c64'), origin: 'folders', folderKey: 'tests/c64' }));
|
||||
assert.deepEqual(next.settings, folder.settings);
|
||||
assert.deepEqual(Object.keys(next.folders), ['tests']);
|
||||
assert.deepEqual(next.folders.tests, { platform: 'nes' });
|
||||
});
|
||||
|
||||
it('leaves a project it does not store (README, window, another folder)', function () {
|
||||
assert.equal(withoutProject(folder, project({ origin: 'readme' })), undefined);
|
||||
assert.equal(withoutProject(folder, project({ origin: 'window', folder: undefined })), undefined);
|
||||
assert.equal(withoutProject(folder, project({ origin: 'settings', folder: '/elsewhere' })), undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveRunTarget', function () {
|
||||
var project: Project = { scope: WS, root: WS, platform: 'nes', mainFile: at('game.c'), origin: 'settings' };
|
||||
var noDeps = () => false;
|
||||
|
||||
Reference in new issue
Block a user