mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-10-24 08:02:27 +00:00
vscode: tweaks to detection logic
This commit is contained in:
1 parent
94e331a43a
commit
b0f1d496e6
5 files changed
+96
-17
No files matched your search
@@ -38,6 +38,13 @@ Strongest first:
|
||||
Plain C or assembly with none of these never brings up a notification,
|
||||
so other projects you open aren't interrupted.
|
||||
|
||||
Hardware registers and function names in a header only declare a
|
||||
library, so a folder of nothing but headers and library sources isn't
|
||||
offered as a project. When a folder holds several programs, its review
|
||||
entry shows the count (**Game Boy · 26 programs**). Accepting it makes
|
||||
the folder a directory of small programs, so **Run** builds whichever
|
||||
program is open.
|
||||
|
||||
Some platforms share everything detection can see. For example, the
|
||||
ColecoVision, MSX and Sega platforms all use `cv.h`. Then the list shows
|
||||
them all and you choose.
|
||||
|
||||
+13
-10
@@ -4,7 +4,8 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { Detection, detectProject, isBuildableSource, isClearWinner, isStrongDetection, toolForDialect } from '../../src/common/detect';
|
||||
import { Detection, detectProject, detectionSummary, describeDetection, describeFinding, isBuildableSource, isClearWinner, isFolderOfPrograms, isStrongDetection, toolForDialect } from '../../src/common/detect';
|
||||
export { detectionSummary, describeDetection, describeFinding, isFolderOfPrograms };
|
||||
import { PLATFORM_PARAMS } from '../../src/worker/platforms';
|
||||
import { getToolForPlatform } from '../../src/common/toolselect';
|
||||
import { isProbablyBinary } from '../../src/common/util';
|
||||
@@ -33,12 +34,6 @@ export function platformName(templates: Templates, id: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
function describe(d: Detection): string {
|
||||
var ev = d.evidence[0];
|
||||
if (!ev) return '';
|
||||
return `${ev.file === '.' ? 'folder' : ev.file}${ev.line ? ':' + ev.line : ''} ${ev.reason}`;
|
||||
}
|
||||
|
||||
async function readText(uri: vscode.Uri): Promise<string | null> {
|
||||
if (isProbablyBinary(uri.path)) return null;
|
||||
var doc = vscode.workspace.textDocuments.find(d => d.uri.toString() === uri.toString());
|
||||
@@ -91,7 +86,7 @@ export async function chooseForFile(templates: Templates, file: vscode.Uri): Pro
|
||||
if (isClearWinner(found)) {
|
||||
platform = found[0].platform;
|
||||
// the user asked, so apply it now and offer a way out
|
||||
vscode.window.showInformationMessage(`Using ${platformName(templates, platform)} (${describe(found[0])}).`, 'Change')
|
||||
vscode.window.showInformationMessage(`Using ${platformName(templates, platform)} (${describeDetection(found[0])}).`, 'Change')
|
||||
.then(async a => {
|
||||
if (a === 'Change') vscode.commands.executeCommand('8bitworkshop.selectPlatform');
|
||||
});
|
||||
@@ -115,7 +110,7 @@ export async function pickPlatformWithEvidence(templates: Templates, found: Dete
|
||||
items.push({ label: 'Detected', kind: vscode.QuickPickItemKind.Separator });
|
||||
for (var d of candidates) {
|
||||
shown.add(d.platform);
|
||||
items.push({ label: platformName(templates, d.platform), description: d.platform, detail: describe(d), id: d.platform });
|
||||
items.push({ label: platformName(templates, d.platform), description: d.platform, detail: describeDetection(d), id: d.platform });
|
||||
}
|
||||
items.push({ label: 'All platforms', kind: vscode.QuickPickItemKind.Separator });
|
||||
}
|
||||
@@ -173,11 +168,19 @@ export async function scanFolder(templates: Templates, folder: vscode.WorkspaceF
|
||||
findings.push({ dir: vscode.Uri.file(dir), detection: found[0] });
|
||||
}
|
||||
}
|
||||
// a folder of programs already covers a nested library folder inside it;
|
||||
// a nested project with its own main file stays
|
||||
findings = findings.filter(f => !findings.some(p =>
|
||||
p !== f && isFolderOfPrograms(p.detection) && !f.detection.mainFile && isInsideDir(p.dir.fsPath, f.dir.fsPath)));
|
||||
return findings;
|
||||
}
|
||||
|
||||
function isInsideDir(parent: string, child: string): boolean {
|
||||
var rel = path.relative(parent, child);
|
||||
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
|
||||
}
|
||||
|
||||
export function dontAskKey(folder: vscode.WorkspaceFolder) {
|
||||
return DONT_ASK + folder.uri.toString();
|
||||
}
|
||||
|
||||
export { describe as describeDetection };
|
||||
@@ -14,7 +14,7 @@ import { Project, findRootDir, isHeaderFile, isInside, isOwnExtension, isSourceF
|
||||
import { CONFIG, ProjectScope } from './projectscope';
|
||||
import { BuildReason, BuildScheduler } from './autobuild';
|
||||
import { PRESET_SCHEME, PresetFileSystem, Templates } from './templates';
|
||||
import { chooseForFile, describeDetection, dontAskKey, platformName, scanFolder } from './detection';
|
||||
import { chooseForFile, detectionSummary, describeFinding, dontAskKey, platformName, scanFolder } from './detection';
|
||||
import { TOOL_META } from '../../src/common/toolmeta';
|
||||
|
||||
let context: vscode.ExtensionContext;
|
||||
@@ -726,7 +726,7 @@ async function detectProjects(asked: boolean) {
|
||||
var f = findings[0], d = f.detection;
|
||||
var where = path.relative(folder.uri.fsPath, f.dir.fsPath);
|
||||
var answer = await vscode.window.showInformationMessage(
|
||||
`This looks like a${/^[aeiou]/i.test(platformName(templates, d.platform)) ? 'n' : ''} ${platformName(templates, d.platform)} project${where ? ' in ' + where : ''} (${describeDetection(d)}).`,
|
||||
`This looks like a${/^[aeiou]/i.test(platformName(templates, d.platform)) ? 'n' : ''} ${platformName(templates, d.platform)} project${where ? ' in ' + where : ''} (${describeFinding(d)}).`,
|
||||
'Use It', 'Choose...', 'Not Now', "Don't Ask");
|
||||
if (answer === 'Use It') await acceptFinding(f.dir.fsPath, d.platform, d.mainFile, d.tool);
|
||||
else if (answer === 'Choose...') {
|
||||
@@ -740,9 +740,9 @@ async function detectProjects(asked: boolean) {
|
||||
if (review !== 'Review') continue;
|
||||
type Item = vscode.QuickPickItem & { f: typeof findings[0] };
|
||||
var items: Item[] = findings.map(f => ({
|
||||
label: path.relative(folder.uri.fsPath, f.dir.fsPath) || folder.name,
|
||||
description: `${platformName(templates, f.detection.platform)}${f.detection.mainFile ? ' · ' + f.detection.mainFile : ''}`,
|
||||
detail: describeDetection(f.detection), picked: true, f,
|
||||
label: path.relative(folder.uri.fsPath, f.dir.fsPath) || '.',
|
||||
description: `${platformName(templates, f.detection.platform)}${detectionSummary(f.detection) ? ' · ' + detectionSummary(f.detection) : ''}`,
|
||||
detail: describeFinding(f.detection), picked: true, f,
|
||||
}));
|
||||
var chosen = await vscode.window.showQuickPick(items, { canPickMany: true, title: 'Use these as 8bitworkshop projects', matchOnDetail: true });
|
||||
for (var c of chosen || []) await acceptFinding(c.f.dir.fsPath, c.f.detection.platform, c.f.detection.mainFile, c.f.detection.tool);
|
||||
|
||||
+46
-1
@@ -50,6 +50,7 @@ export const ROM_PLATFORMS: { [ext: string]: string } = {
|
||||
const SYSTEM_HEADERS: { [header: string]: string[] } = {
|
||||
'nes.h': ['nes'],
|
||||
'neslib.h': ['nes'],
|
||||
'apu.h': ['nes'],
|
||||
'cv.h': ['coleco', 'msx-libcv', 'sms-sg1000-libcv', 'sms-sms-libcv', 'sms-gg-libcv'],
|
||||
'cvu.h': ['coleco', 'msx-libcv', 'sms-sg1000-libcv', 'sms-sms-libcv', 'sms-gg-libcv'],
|
||||
'c64.h': ['c64'],
|
||||
@@ -63,6 +64,7 @@ const SYSTEM_HEADERS: { [header: string]: string[] } = {
|
||||
'pce.h': ['pce'],
|
||||
'gb/gb.h': ['gb'],
|
||||
'gb.h': ['gb'],
|
||||
'gbtext.h': ['gb'],
|
||||
'vcs.h': ['vcs'],
|
||||
'macro.h': ['vcs'],
|
||||
'vectrex.h': ['vectrex'],
|
||||
@@ -88,6 +90,9 @@ const FINGERPRINTS: { re: RegExp, platforms: string[], reason: string }[] = [
|
||||
];
|
||||
|
||||
const HEADER_EXTS = ['.h', '.inc', '.i'];
|
||||
// a register or function name in a header is a declaration, not a use, so
|
||||
// it points at the library rather than at a program written for the platform
|
||||
const HEADER_FINGERPRINT_WEIGHT = 0.15;
|
||||
// extensions so many tools use that they say nothing about the platform
|
||||
const GENERIC_EXTS = ['.c', '.h', '.s', '.asm', '.a', '.inc', '.i', '.bas'];
|
||||
|
||||
@@ -229,7 +234,8 @@ export async function detectProject(input: DetectInput): Promise<Detection[]> {
|
||||
var fm = fp.re.exec(text);
|
||||
if (fm) {
|
||||
var fline = text.substring(0, fm.index).split('\n').length;
|
||||
addAll(fp.platforms, 0.5, { file, line: fline, reason: fp.reason });
|
||||
var fweight = HEADER_EXTS.includes(ext) ? HEADER_FINGERPRINT_WEIGHT : 0.5;
|
||||
addAll(fp.platforms, fweight, { file, line: fline, reason: fp.reason });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -282,6 +288,45 @@ export function isStrongDetection(d: Detection): boolean {
|
||||
return d.score >= 0.5;
|
||||
}
|
||||
|
||||
/** True for a header or include file: a declaration, not a program. */
|
||||
export function isHeaderFile(fn: string): boolean {
|
||||
return HEADER_EXTS.includes(extname(fn));
|
||||
}
|
||||
|
||||
/** The clue to show first: a program source's, unless only a header matches. */
|
||||
export function mainEvidence(d: Detection): DetectEvidence | undefined {
|
||||
return d.evidence.find(e => e.file !== '.' && !isHeaderFile(e.file)) || d.evidence[0];
|
||||
}
|
||||
|
||||
/** "game.c", or "26 programs" for a directory of programs. */
|
||||
export function detectionSummary(d: Detection): string {
|
||||
if (d.mainFile) return d.mainFile;
|
||||
var n = d.mainCandidates?.length || 0;
|
||||
return n > 1 ? `${n} programs` : '';
|
||||
}
|
||||
|
||||
/** True for a directory that holds several programs and no single main file. */
|
||||
export function isFolderOfPrograms(d: Detection): boolean {
|
||||
return !d.mainFile && (d.mainCandidates?.length || 0) > 1;
|
||||
}
|
||||
|
||||
/** A one-line description of the clue behind a detection. */
|
||||
export function describeDetection(d: Detection): string {
|
||||
var ev = mainEvidence(d);
|
||||
if (!ev) return '';
|
||||
return `${ev.file === '.' ? 'folder' : ev.file}${ev.line ? ':' + ev.line : ''} ${ev.reason}`;
|
||||
}
|
||||
|
||||
/** The clue, and for a folder of programs the number of programs it holds. */
|
||||
export function describeFinding(d: Detection): string {
|
||||
var text = describeDetection(d);
|
||||
if (isFolderOfPrograms(d)) {
|
||||
var n = d.mainCandidates!.length;
|
||||
text = `${n} programs${text ? ' — ' + text : ''}`;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* The files that look like programs: buildable sources no other file
|
||||
* includes or links. `mainFile` is set when one stands out; when several
|
||||
|
||||
+25
-1
@@ -2,7 +2,7 @@ import assert from "assert";
|
||||
import { describe, it } from "mocha";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { detectProject, detectDialect, findMainCandidates, headersFromPresets, isClearWinner, parseReadmeBadge, toolForDialect } from "../../src/common/detect";
|
||||
import { detectProject, detectDialect, detectionSummary, describeFinding, findMainCandidates, headersFromPresets, isClearWinner, isFolderOfPrograms, isHeaderFile, mainEvidence, parseReadmeBadge, toolForDialect } from "../../src/common/detect";
|
||||
import { PLATFORM_PARAMS } from "../../src/worker/platforms";
|
||||
|
||||
const PRESETS = 'presets';
|
||||
@@ -72,6 +72,30 @@ describe('detect', () => {
|
||||
assert.deepStrictEqual(findMainCandidates('nes', [...texts.keys()], texts), { candidates: ['a.c', 'b.c'], mainFile: undefined });
|
||||
});
|
||||
|
||||
it('downweights a fingerprint found in a header', async () => {
|
||||
// the GBDK calls are library declarations, not a program using the hardware
|
||||
var header = 'void display_off();\nvoid SHOW_BKG();\nvoid set_bkg_data();\n';
|
||||
var d = await detectProject({ files: ['gb.h'], read: () => header, platforms: PLATFORMS, dirName: 'gb' });
|
||||
assert.ok(d.every(x => x.score < 0.5), JSON.stringify(d));
|
||||
assert.ok(d[0].score > 0, 'the header still counts for something');
|
||||
assert.ok(isHeaderFile('gb/gb.h'));
|
||||
assert.ok(!isHeaderFile('gb/gb.sgb'));
|
||||
assert.strictEqual(mainEvidence(d[0])?.reason, 'calls GBDK functions');
|
||||
});
|
||||
|
||||
it('lists every program in a folder of programs', async () => {
|
||||
var programs = ['chase.c', 'climber.c', 'testphys.c'];
|
||||
var map: { [fn: string]: string } = {};
|
||||
for (var fn of programs) map[fn] = '#include "gb/gb.h"\nvoid main() {}\n';
|
||||
var d = await detectProject({ files: programs, read: (fn) => map[fn] ?? null, platforms: PLATFORMS, headers: { 'gb/gb.h': ['gb'] }, dirName: 'gb' });
|
||||
assert.strictEqual(d[0].platform, 'gb');
|
||||
assert.strictEqual(d[0].mainFile, undefined);
|
||||
assert.deepStrictEqual(d[0].mainCandidates, programs);
|
||||
assert.ok(isFolderOfPrograms(d[0]));
|
||||
assert.strictEqual(detectionSummary(d[0]), '3 programs');
|
||||
assert.strictEqual(describeFinding(d[0]), '3 programs — chase.c:1 includes "gb/gb.h"');
|
||||
});
|
||||
|
||||
// Every preset's true platform is its directory. Track how often the top
|
||||
// guess for a lone preset file is right, and fail if it gets worse.
|
||||
it('guesses the platform of the presets', async function () {
|
||||
|
||||
Reference in new issue
Block a user