diff --git a/public/data/implementation-status.json b/public/data/implementation-status.json index 4327f4e3..e9dd507e 100644 --- a/public/data/implementation-status.json +++ b/public/data/implementation-status.json @@ -4895,7 +4895,7 @@ ] }, "ItemDisplay": { - "implemented": false, + "implemented": true, "todos": [], "entries": [ "item_display" @@ -5245,8 +5245,10 @@ ] }, "TextDisplay": { - "implemented": false, - "todos": [], + "implemented": true, + "todos": [ + "resolve text component" + ], "entries": [ "text_display" ] @@ -5426,5 +5428,622 @@ "fishing_bobber" ] } + }, + "commands": { + "/advancement": { + "implemented": false, + "todos": [], + "entries": [ + "/advancement" + ] + }, + "/attribute": { + "implemented": false, + "todos": [], + "entries": [ + "/attribute" + ] + }, + "/execute": { + "implemented": true, + "todos": [ + "Add pagination for domains and worlds instead of truncating to the grid capacity.", + "Add items after every vanilla command-slot provider and container inventory is", + "Add predicate and function after their runtime registries are ported.", + "Restore Steel stopwatch conditions with the stopwatch command system.", + "Add bossbar after Steel has a persistent custom-bossbar manager.", + "Add entity after live entity NBT can reload every command-visible field.", + "Add `locate biome` once Steel has an asynchronous closest-biome search.", + "Add `locate poi` once Steel has a point-of-interest manager.", + "Add the vanilla compound-NBT branch once Steel has an SNBT compound" + ], + "entries": [ + "/execute" + ] + }, + "/bossbar": { + "implemented": false, + "todos": [], + "entries": [ + "/bossbar" + ] + }, + "/clear": { + "implemented": true, + "todos": [], + "entries": [ + "/clear" + ] + }, + "/clone": { + "implemented": false, + "todos": [], + "entries": [ + "/clone" + ] + }, + "/damage": { + "implemented": true, + "todos": [], + "entries": [ + "/damage" + ] + }, + "/data": { + "implemented": false, + "todos": [], + "entries": [ + "/data" + ] + }, + "/datapack": { + "implemented": false, + "todos": [], + "entries": [ + "/datapack" + ] + }, + "/debug": { + "implemented": false, + "todos": [], + "entries": [ + "/debug" + ] + }, + "/defaultgamemode": { + "implemented": false, + "todos": [], + "entries": [ + "/defaultgamemode" + ] + }, + "/dialog": { + "implemented": false, + "todos": [], + "entries": [ + "/dialog" + ] + }, + "/difficulty": { + "implemented": true, + "todos": [], + "entries": [ + "/difficulty" + ] + }, + "/effect": { + "implemented": false, + "todos": [], + "entries": [ + "/effect" + ] + }, + "/me": { + "implemented": false, + "todos": [], + "entries": [ + "/me" + ] + }, + "/enchant": { + "implemented": true, + "todos": [], + "entries": [ + "/enchant" + ] + }, + "/experience": { + "implemented": true, + "todos": [], + "entries": [ + "/experience", + "/xp" + ] + }, + "/fill": { + "implemented": false, + "todos": [], + "entries": [ + "/fill" + ] + }, + "/fillbiome": { + "implemented": false, + "todos": [], + "entries": [ + "/fillbiome" + ] + }, + "/forceload": { + "implemented": false, + "todos": [], + "entries": [ + "/forceload" + ] + }, + "/function": { + "implemented": false, + "todos": [], + "entries": [ + "/function" + ] + }, + "/gamemode": { + "implemented": true, + "todos": [], + "entries": [ + "/gamemode" + ] + }, + "/gamerule": { + "implemented": true, + "todos": [], + "entries": [ + "/gamerule" + ] + }, + "/give": { + "implemented": true, + "todos": [], + "entries": [ + "/give" + ] + }, + "/help": { + "implemented": false, + "todos": [], + "entries": [ + "/help" + ] + }, + "/item": { + "implemented": false, + "todos": [], + "entries": [ + "/item" + ] + }, + "/kick": { + "implemented": false, + "todos": [], + "entries": [ + "/kick" + ] + }, + "/kill": { + "implemented": true, + "todos": [], + "entries": [ + "/kill" + ] + }, + "/list": { + "implemented": true, + "todos": [], + "entries": [ + "/list" + ] + }, + "/locate": { + "implemented": true, + "todos": [ + "Add `locate biome` once Steel has an asynchronous closest-biome search.", + "Add `locate poi` once Steel has a point-of-interest manager." + ], + "entries": [ + "/locate" + ] + }, + "/loot": { + "implemented": false, + "todos": [], + "entries": [ + "/loot" + ] + }, + "/msg": { + "implemented": false, + "todos": [], + "entries": [ + "/msg", + "/tell", + "/w" + ] + }, + "/swing": { + "implemented": false, + "todos": [], + "entries": [ + "/swing" + ] + }, + "/particle": { + "implemented": false, + "todos": [], + "entries": [ + "/particle" + ] + }, + "/place": { + "implemented": false, + "todos": [], + "entries": [ + "/place" + ] + }, + "/playsound": { + "implemented": false, + "todos": [], + "entries": [ + "/playsound" + ] + }, + "/random": { + "implemented": false, + "todos": [], + "entries": [ + "/random" + ] + }, + "/reload": { + "implemented": false, + "todos": [], + "entries": [ + "/reload" + ] + }, + "/recipe": { + "implemented": false, + "todos": [], + "entries": [ + "/recipe" + ] + }, + "/fetchprofile": { + "implemented": false, + "todos": [], + "entries": [ + "/fetchprofile" + ] + }, + "/return": { + "implemented": false, + "todos": [], + "entries": [ + "/return" + ] + }, + "/ride": { + "implemented": false, + "todos": [], + "entries": [ + "/ride" + ] + }, + "/rotate": { + "implemented": false, + "todos": [], + "entries": [ + "/rotate" + ] + }, + "/say": { + "implemented": false, + "todos": [], + "entries": [ + "/say" + ] + }, + "/schedule": { + "implemented": false, + "todos": [], + "entries": [ + "/schedule" + ] + }, + "/scoreboard": { + "implemented": false, + "todos": [], + "entries": [ + "/scoreboard" + ] + }, + "/seed": { + "implemented": true, + "todos": [], + "entries": [ + "/seed" + ] + }, + "/version": { + "implemented": false, + "todos": [], + "entries": [ + "/version" + ] + }, + "/setblock": { + "implemented": true, + "todos": [], + "entries": [ + "/setblock" + ] + }, + "/spawnpoint": { + "implemented": false, + "todos": [], + "entries": [ + "/spawnpoint" + ] + }, + "/setworldspawn": { + "implemented": true, + "todos": [], + "entries": [ + "/setworldspawn" + ] + }, + "/spectate": { + "implemented": false, + "todos": [], + "entries": [ + "/spectate" + ] + }, + "/spreadplayers": { + "implemented": false, + "todos": [], + "entries": [ + "/spreadplayers" + ] + }, + "/stopsound": { + "implemented": false, + "todos": [], + "entries": [ + "/stopsound" + ] + }, + "/stopwatch": { + "implemented": false, + "todos": [], + "entries": [ + "/stopwatch" + ] + }, + "/summon": { + "implemented": true, + "todos": [ + "Add the vanilla compound-NBT branch once Steel has an SNBT compound" + ], + "entries": [ + "/summon" + ] + }, + "/tag": { + "implemented": false, + "todos": [], + "entries": [ + "/tag" + ] + }, + "/team": { + "implemented": false, + "todos": [], + "entries": [ + "/team" + ] + }, + "/teammsg": { + "implemented": false, + "todos": [], + "entries": [ + "/teammsg", + "/tm" + ] + }, + "/teleport": { + "implemented": true, + "todos": [], + "entries": [ + "/teleport", + "/tp" + ] + }, + "/tellraw": { + "implemented": true, + "todos": [], + "entries": [ + "/tellraw" + ] + }, + "/test": { + "implemented": false, + "todos": [], + "entries": [ + "/test" + ] + }, + "/tick": { + "implemented": true, + "todos": [], + "entries": [ + "/tick" + ] + }, + "/time": { + "implemented": true, + "todos": [], + "entries": [ + "/time" + ] + }, + "/title": { + "implemented": false, + "todos": [], + "entries": [ + "/title" + ] + }, + "/trigger": { + "implemented": false, + "todos": [], + "entries": [ + "/trigger" + ] + }, + "/waypoint": { + "implemented": false, + "todos": [], + "entries": [ + "/waypoint" + ] + }, + "/weather": { + "implemented": true, + "todos": [], + "entries": [ + "/weather" + ] + }, + "/worldborder": { + "implemented": false, + "todos": [], + "entries": [ + "/worldborder" + ] + }, + "/debugconfig": { + "implemented": false, + "todos": [], + "entries": [ + "/debugconfig" + ] + }, + "/ban-ip": { + "implemented": false, + "todos": [], + "entries": [ + "/ban-ip" + ] + }, + "/banlist": { + "implemented": false, + "todos": [], + "entries": [ + "/banlist" + ] + }, + "/ban": { + "implemented": false, + "todos": [], + "entries": [ + "/ban" + ] + }, + "/deop": { + "implemented": false, + "todos": [], + "entries": [ + "/deop" + ] + }, + "/op": { + "implemented": false, + "todos": [], + "entries": [ + "/op" + ] + }, + "/pardon": { + "implemented": false, + "todos": [], + "entries": [ + "/pardon" + ] + }, + "/pardon-ip": { + "implemented": false, + "todos": [], + "entries": [ + "/pardon-ip" + ] + }, + "/perf": { + "implemented": false, + "todos": [], + "entries": [ + "/perf" + ] + }, + "/save-all": { + "implemented": false, + "todos": [], + "entries": [ + "/save-all" + ] + }, + "/save-off": { + "implemented": false, + "todos": [], + "entries": [ + "/save-off" + ] + }, + "/save-on": { + "implemented": false, + "todos": [], + "entries": [ + "/save-on" + ] + }, + "/setidletimeout": { + "implemented": true, + "todos": [], + "entries": [ + "/setidletimeout" + ] + }, + "/stop": { + "implemented": true, + "todos": [], + "entries": [ + "/stop" + ] + }, + "/transfer": { + "implemented": false, + "todos": [], + "entries": [ + "/transfer" + ] + }, + "/whitelist": { + "implemented": false, + "todos": [], + "entries": [ + "/whitelist" + ] + } } } \ No newline at end of file diff --git a/scripts/commands.json b/scripts/commands.json new file mode 100644 index 00000000..95718b39 --- /dev/null +++ b/scripts/commands.json @@ -0,0 +1,368 @@ +{ + "commands": [ + { + "name": "/advancement", + "class": "/advancement" + }, + { + "name": "/attribute", + "class": "/attribute" + }, + { + "name": "/execute", + "class": "/execute" + }, + { + "name": "/bossbar", + "class": "/bossbar" + }, + { + "name": "/clear", + "class": "/clear" + }, + { + "name": "/clone", + "class": "/clone" + }, + { + "name": "/damage", + "class": "/damage" + }, + { + "name": "/data", + "class": "/data" + }, + { + "name": "/datapack", + "class": "/datapack" + }, + { + "name": "/debug", + "class": "/debug" + }, + { + "name": "/defaultgamemode", + "class": "/defaultgamemode" + }, + { + "name": "/dialog", + "class": "/dialog" + }, + { + "name": "/difficulty", + "class": "/difficulty" + }, + { + "name": "/effect", + "class": "/effect" + }, + { + "name": "/me", + "class": "/me" + }, + { + "name": "/enchant", + "class": "/enchant" + }, + { + "name": "/experience", + "class": "/experience" + }, + { + "name": "/xp", + "class": "/experience" + }, + { + "name": "/fill", + "class": "/fill" + }, + { + "name": "/fillbiome", + "class": "/fillbiome" + }, + { + "name": "/forceload", + "class": "/forceload" + }, + { + "name": "/function", + "class": "/function" + }, + { + "name": "/gamemode", + "class": "/gamemode" + }, + { + "name": "/gamerule", + "class": "/gamerule" + }, + { + "name": "/give", + "class": "/give" + }, + { + "name": "/help", + "class": "/help" + }, + { + "name": "/item", + "class": "/item" + }, + { + "name": "/kick", + "class": "/kick" + }, + { + "name": "/kill", + "class": "/kill" + }, + { + "name": "/list", + "class": "/list" + }, + { + "name": "/locate", + "class": "/locate" + }, + { + "name": "/loot", + "class": "/loot" + }, + { + "name": "/msg", + "class": "/msg" + }, + { + "name": "/tell", + "class": "/msg" + }, + { + "name": "/w", + "class": "/msg" + }, + { + "name": "/swing", + "class": "/swing" + }, + { + "name": "/particle", + "class": "/particle" + }, + { + "name": "/place", + "class": "/place" + }, + { + "name": "/playsound", + "class": "/playsound" + }, + { + "name": "/random", + "class": "/random" + }, + { + "name": "/reload", + "class": "/reload" + }, + { + "name": "/recipe", + "class": "/recipe" + }, + { + "name": "/fetchprofile", + "class": "/fetchprofile" + }, + { + "name": "/return", + "class": "/return" + }, + { + "name": "/ride", + "class": "/ride" + }, + { + "name": "/rotate", + "class": "/rotate" + }, + { + "name": "/say", + "class": "/say" + }, + { + "name": "/schedule", + "class": "/schedule" + }, + { + "name": "/scoreboard", + "class": "/scoreboard" + }, + { + "name": "/seed", + "class": "/seed" + }, + { + "name": "/version", + "class": "/version" + }, + { + "name": "/setblock", + "class": "/setblock" + }, + { + "name": "/spawnpoint", + "class": "/spawnpoint" + }, + { + "name": "/setworldspawn", + "class": "/setworldspawn" + }, + { + "name": "/spectate", + "class": "/spectate" + }, + { + "name": "/spreadplayers", + "class": "/spreadplayers" + }, + { + "name": "/stopsound", + "class": "/stopsound" + }, + { + "name": "/stopwatch", + "class": "/stopwatch" + }, + { + "name": "/summon", + "class": "/summon" + }, + { + "name": "/tag", + "class": "/tag" + }, + { + "name": "/team", + "class": "/team" + }, + { + "name": "/teammsg", + "class": "/teammsg" + }, + { + "name": "/tm", + "class": "/teammsg" + }, + { + "name": "/teleport", + "class": "/teleport" + }, + { + "name": "/tp", + "class": "/teleport" + }, + { + "name": "/tellraw", + "class": "/tellraw" + }, + { + "name": "/test", + "class": "/test" + }, + { + "name": "/tick", + "class": "/tick" + }, + { + "name": "/time", + "class": "/time" + }, + { + "name": "/title", + "class": "/title" + }, + { + "name": "/trigger", + "class": "/trigger" + }, + { + "name": "/waypoint", + "class": "/waypoint" + }, + { + "name": "/weather", + "class": "/weather" + }, + { + "name": "/worldborder", + "class": "/worldborder" + }, + { + "name": "/jfr", + "class": "/jfr" + }, + { + "name": "/debugconfig", + "class": "/debugconfig" + }, + { + "name": "/ban-ip", + "class": "/ban-ip" + }, + { + "name": "/banlist", + "class": "/banlist" + }, + { + "name": "/ban", + "class": "/ban" + }, + { + "name": "/deop", + "class": "/deop" + }, + { + "name": "/op", + "class": "/op" + }, + { + "name": "/pardon", + "class": "/pardon" + }, + { + "name": "/pardon-ip", + "class": "/pardon-ip" + }, + { + "name": "/perf", + "class": "/perf" + }, + { + "name": "/save-all", + "class": "/save-all" + }, + { + "name": "/save-off", + "class": "/save-off" + }, + { + "name": "/save-on", + "class": "/save-on" + }, + { + "name": "/setidletimeout", + "class": "/setidletimeout" + }, + { + "name": "/stop", + "class": "/stop" + }, + { + "name": "/transfer", + "class": "/transfer" + }, + { + "name": "/whitelist", + "class": "/whitelist" + } + ] +} \ No newline at end of file diff --git a/scripts/sync-implementation.ts b/scripts/sync-implementation.ts index 8d929466..053c2d39 100644 --- a/scripts/sync-implementation.ts +++ b/scripts/sync-implementation.ts @@ -11,7 +11,7 @@ import { execFileSync } from "node:child_process"; import { readdir, readFile, mkdir, writeFile } from "node:fs/promises"; -import { dirname, join, resolve } from "node:path"; +import path, { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const STEEL_PATH = process.argv[2]; @@ -23,7 +23,9 @@ if (!STEEL_PATH) { const steelRoot = resolve(STEEL_PATH); const behaviorDir = join(steelRoot, "steel-core/src/behavior"); const entityDir = join(steelRoot, "steel-core/src/entity/entities"); +const commandsDir = join(steelRoot, "steel-core/src/command/builtins"); const classesJsonPath = join(steelRoot, "steel-core/build/classes.json"); +const commandsJsonPath = path.join(__dirname, 'commands.json') // --- Scan .rs files for annotated structs --- @@ -45,6 +47,56 @@ interface ClassInfo { todos: string[]; } +/** A single implementation, like a struct or command. */ +type Implementation = { name: string; pos: number } + +/** A todo at a line number with some content. */ +type Todo = { pos: number; text: string } + +function extractTodos(lines: string[]): Todo[] { + // Extract TODO/FIXME comments with their text and position + const todoEntries: Todo[] = []; + const todoLinePattern = /\b(TODO|FIXME):?\s*(.*)/; + for (let i = 0; i < lines.length; i++) { + const lineMatch = lines[i].match(todoLinePattern); + if (!lineMatch) continue; + + // Get the initial TODO text + let text = lineMatch[2].trim(); + + // Collect continuation lines (comment lines that follow without a gap) + for (let j = i + 1; j < lines.length; j++) { + const cont = lines[j].match(/^\s*\/\/\s{2,}(.*)/); + if (!cont) break; + text += " " + cont[1].trim(); + } + + // Calculate byte offset for attribution + const bytePos = lines.slice(0, i).reduce((sum, l) => sum + l.length + 1, 0); + todoEntries.push({ pos: bytePos, text }); + } + + return todoEntries; +} + +function extractTodoList(lines: string[], implementations: Implementation[]): Map { + const todoEntries = extractTodos(lines); + + const todoLists = new Map(); + for (const impl of implementations) todoLists.set(impl.name, []); + + for (const todo of todoEntries) { + let closest: string | null = null; + for (const impl of implementations) { + if (impl.pos <= todo.pos) closest = impl.name; + } + if (!closest) closest = implementations[0].name; + todoLists.get(closest)!.push(todo.text); + } + + return todoLists; +} + /** * Scans Rust source files for `#[block_behavior]` or `#[item_behavior]` annotations. * Returns a map of class name -> { todos } where todos is the count of TODO/FIXME @@ -75,40 +127,7 @@ async function scanImplementedClasses(dir: string, annotation: string): Promise< } if (structs.length === 0) continue; - // Extract TODO/FIXME comments with their text and position - const todoEntries: { pos: number; text: string }[] = []; - const todoLinePattern = /\b(TODO|FIXME):?\s*(.*)/; - for (let i = 0; i < lines.length; i++) { - const lineMatch = lines[i].match(todoLinePattern); - if (!lineMatch) continue; - - // Get the initial TODO text - let text = lineMatch[2].trim(); - - // Collect continuation lines (comment lines that follow without a gap) - for (let j = i + 1; j < lines.length; j++) { - const cont = lines[j].match(/^\s*\/\/\s{2,}(.*)/); - if (!cont) break; - text += " " + cont[1].trim(); - } - - // Calculate byte offset for attribution - const bytePos = lines.slice(0, i).reduce((sum, l) => sum + l.length + 1, 0); - todoEntries.push({ pos: bytePos, text }); - } - - // Attribute each TODO to the nearest preceding struct - const todoLists = new Map(); - for (const struct of structs) todoLists.set(struct.name, []); - - for (const todo of todoEntries) { - let closest: string | null = null; - for (const struct of structs) { - if (struct.pos <= todo.pos) closest = struct.name; - } - if (!closest) closest = structs[0].name; - todoLists.get(closest)!.push(todo.text); - } + let todoLists = extractTodoList(lines, structs); for (const struct of structs) { classes.set(struct.name, { todos: todoLists.get(struct.name) ?? [] }); @@ -118,6 +137,57 @@ async function scanImplementedClasses(dir: string, annotation: string): Promise< return classes; } +/** + * Scans Rust source files for `CommandRegistration::new` calls. + * Returns a map of class name -> { todos } where todos is the count of TODO/FIXME + * comments attributed to that struct (based on proximity in the source file). + */ +async function scanCommandFiles(dir: string): Promise> { + const structPattern = new RegExp( + `CommandRegistration\\s*::\\s*new\\s*\\(\\s*Identifier\\s*::\\s*vanilla_static\\s*\\(\\s*"(\\w+)"\\s*\\)\\s*,.*\\|_\\|\\s*command\\s*\\(\\s*\\)\\s*\\)` + ); + const commandImplementations = new Map(); + + + const files = await collectRsFiles(dir); + for (const file of files) { + const content = await readFile(file, "utf-8"); + const lines = content.split("\n"); + + let match; + const commands: { name: string; pos: number }[] = []; + while ((match = structPattern.exec(content)) !== null) { + let command = match[1] as string; + if (command === undefined || command === "execute") continue; + commands.push({name: command, pos: match.index}); + break; + } + + if (commands.length === 0) continue; + + let todoLists = extractTodoList(lines, commands); + + for (const command of commands) { + commandImplementations.set("/" + command.name, { todos: todoLists.get(command.name) ?? [] }); + } + } + + // Hardcoded check for the /execute command. + const executeFiles = await collectRsFiles(dir + "/execute"); + for (const file of executeFiles) { + let todos = []; + for (const file of files) { + const content = await readFile(file, "utf-8"); + const lines = content.split("\n"); + + todos.push(...extractTodos(lines)); + } + commandImplementations.set("/execute", { todos: todos.map(t => t.text) ?? [] }); + } + + return commandImplementations; +} + // --- Parse classes.json --- interface ClassEntry { @@ -130,18 +200,21 @@ interface ClassesJson { blocks: ClassEntry[]; items: ClassEntry[]; entities: ClassEntry[]; + commands: ClassEntry[]; } // --- Main --- -const [classesRaw, implementedBlockClasses, implementedItemClasses, implementedEntityClasses] = await Promise.all([ +const [classesRaw, commandsRaw, implementedBlockClasses, implementedItemClasses, implementedEntityClasses, implementedCommands] = await Promise.all([ readFile(classesJsonPath, "utf-8").then((raw) => JSON.parse(raw) as ClassesJson), + readFile(commandsJsonPath, "utf-8").then((raw) => JSON.parse(raw) as ClassesJson), scanImplementedClasses(join(behaviorDir, "blocks"), "block_behavior"), scanImplementedClasses(join(behaviorDir, "items"), "item_behavior"), scanImplementedClasses(entityDir, "entity_behavior"), + scanCommandFiles(commandsDir), ]); -const BLACKLISTED_CLASSES = new Set(["AirBlock", "Block", "Item"]); +const BLACKLISTED_CLASSES = new Set(["AirBlock", "Block", "Item", "/jfr"]); function groupByClass( entries: ClassEntry[], @@ -166,6 +239,7 @@ function groupByClass( const blocks = groupByClass(classesRaw.blocks, implementedBlockClasses); const items = groupByClass(classesRaw.items, implementedItemClasses); const entities = groupByClass(classesRaw.entities, implementedEntityClasses); +const commands = groupByClass(commandsRaw.commands, implementedCommands); function steelGit(...args: string[]): string | null { try { @@ -195,7 +269,7 @@ const meta = { steel_committed_at: steelGit("show", "-s", "--format=%cI", "HEAD"), }; -const output = { meta, blocks, items, entities }; +const output = { meta, blocks, items, entities, commands }; const outDir = join(dirname(fileURLToPath(import.meta.url)), "../public/data"); await mkdir(outDir, { recursive: true }); @@ -215,9 +289,11 @@ function summarize(groups: Record; items: Record; entities: Record; + commands: Record; } interface GHIssues { @@ -51,7 +52,7 @@ interface GHIssues { pull_request: Record | null; } -type Tab = "blocks" | "items" | "entities"; +type Tab = "blocks" | "items" | "entities" | "commands"; type StatusFilter = "all" | "complete" | "partial" | "unimplemented"; type ProgressMetric = "surface" | "classes"; @@ -64,8 +65,8 @@ async function iterIssues(pages: number, data: ImplementationData) { .then((json: GHIssues[]) => { Object.values(json).forEach((pr) => { - const iter = (group: Record) => Object.keys(group).forEach((name) => { - const regex = new RegExp(`\\b${name.toLowerCase()}\\b`); + const iter = (group: Record, use_slash_regex: boolean) => Object.keys(group).forEach((name) => { + const regex = use_slash_regex ? new RegExp(`\\B${name.toLowerCase()}\\b`) : new RegExp(`\\b${name.toLowerCase()}\\b`); if (regex.test(pr.title.toLowerCase()) || (pr.body_text && regex.test(pr.body_text.toLowerCase()))) { if (!pr.pull_request) { if (group[name].issues) { @@ -83,9 +84,10 @@ async function iterIssues(pages: number, data: ImplementationData) { } }); - iter(data.items); - iter(data.entities); - iter(data.blocks); + iter(data.items, false); + iter(data.entities, false); + iter(data.blocks, false); + iter(data.commands, true); }); }); } @@ -299,6 +301,15 @@ export default function ImplementationTracker() { ), }, + { + value: "commands", + content: ( + <> + + Commands + + ), + }, ]} /> @@ -307,7 +318,7 @@ export default function ImplementationTracker() { setSearch(e.target.value)} className="tracker-control w-full pl-9 pr-3 py-2 text-sm bg-white dark:bg-white/5 border border-teal-200/40 dark:border-white/10 rounded-2xl text-teal-950 dark:text-white placeholder:text-teal-400 dark:placeholder:text-white/35 focus:outline-none focus:border-emerald-500/60 dark:focus:border-emerald-400/50 transition-all"