Sprint 6 Steps 188-190: command palette, wizards, vuln db
This commit is contained in:
@@ -535,4 +535,7 @@ Sprint 5 in progress. Step 141 (Emacs daemon with user config) done. Next: Step
|
|||||||
| 2026-02-10 | Codex | Step 185: Contextual feature hints with persistence + dismiss/disable. 2/2 tests pass (step185_test, step185_integration_test). |
|
| 2026-02-10 | Codex | Step 185: Contextual feature hints with persistence + dismiss/disable. 2/2 tests pass (step185_test, step185_integration_test). |
|
||||||
| 2026-02-10 | Codex | Step 186: Keyboard shortcut reference panel with filter and live bindings. 2/2 tests pass (step186_test, step186_integration_test). |
|
| 2026-02-10 | Codex | Step 186: Keyboard shortcut reference panel with filter and live bindings. 2/2 tests pass (step186_test, step186_integration_test). |
|
||||||
| 2026-02-10 | Codex | Step 187: In-editor help panel window wired from menu with Markdown renderer reuse. 2/2 tests pass (step187_test, step187_integration_test). |
|
| 2026-02-10 | Codex | Step 187: In-editor help panel window wired from menu with Markdown renderer reuse. 2/2 tests pass (step187_test, step187_integration_test). |
|
||||||
|
| 2026-02-10 | Codex | Step 188: Command palette upgrade (command/file modes, categories, recent section, context-aware filtering, inline params, fuzzy highlights). 1/1 tests pass (step188_test). |
|
||||||
|
| 2026-02-10 | Codex | Step 189: Guided workflow wizards (annotate file, cross-language project, connect agent) with shared wizard framework. 1/1 tests pass (step189_test). |
|
||||||
|
| 2026-02-10 | Codex | Step 190: Vulnerability database (OSV parsing, cache+TTL, offline mode, query/range matching, background refresh hook). 1/1 tests pass (step190_test). |
|
||||||
| 2026-02-10 | Codex | Step 166: Extract main.cpp into panel headers marked complete (already implemented). step166_test passes; file_limits_test 4/4 passes. |
|
| 2026-02-10 | Codex | Step 166: Extract main.cpp into panel headers marked complete (already implemented). step166_test passes; file_limits_test 4/4 passes. |
|
||||||
|
|||||||
@@ -1100,6 +1100,16 @@ add_executable(step187_integration_test tests/step187_integration_test.cpp)
|
|||||||
target_include_directories(step187_integration_test PRIVATE src)
|
target_include_directories(step187_integration_test PRIVATE src)
|
||||||
target_link_libraries(step187_integration_test PRIVATE nlohmann_json::nlohmann_json)
|
target_link_libraries(step187_integration_test PRIVATE nlohmann_json::nlohmann_json)
|
||||||
|
|
||||||
|
add_executable(step188_test tests/step188_test.cpp)
|
||||||
|
target_include_directories(step188_test PRIVATE src)
|
||||||
|
|
||||||
|
add_executable(step189_test tests/step189_test.cpp)
|
||||||
|
target_include_directories(step189_test PRIVATE src)
|
||||||
|
|
||||||
|
add_executable(step190_test tests/step190_test.cpp)
|
||||||
|
target_include_directories(step190_test PRIVATE src)
|
||||||
|
target_link_libraries(step190_test PRIVATE nlohmann_json::nlohmann_json)
|
||||||
|
|
||||||
find_package(SDL2 CONFIG REQUIRED)
|
find_package(SDL2 CONFIG REQUIRED)
|
||||||
find_package(OpenGL REQUIRED)
|
find_package(OpenGL REQUIRED)
|
||||||
find_package(glad CONFIG REQUIRED)
|
find_package(glad CONFIG REQUIRED)
|
||||||
|
|||||||
@@ -8,18 +8,45 @@ struct CommandEntry {
|
|||||||
std::string id;
|
std::string id;
|
||||||
std::string label;
|
std::string label;
|
||||||
std::string shortcut;
|
std::string shortcut;
|
||||||
|
std::string category;
|
||||||
|
std::string inputHint;
|
||||||
|
int contextMask = 0;
|
||||||
|
bool inlineInput = false;
|
||||||
int lastUsed = 0;
|
int lastUsed = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct CommandMatch {
|
||||||
|
CommandEntry entry;
|
||||||
|
int score = 0;
|
||||||
|
std::vector<int> matchIndices;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum CommandContext : int {
|
||||||
|
CommandContext_Any = 0,
|
||||||
|
CommandContext_Editor = 1 << 0,
|
||||||
|
CommandContext_Search = 1 << 1,
|
||||||
|
CommandContext_Settings = 1 << 2,
|
||||||
|
CommandContext_Help = 1 << 3,
|
||||||
|
CommandContext_Terminal = 1 << 4
|
||||||
|
};
|
||||||
|
|
||||||
class CommandPalette {
|
class CommandPalette {
|
||||||
public:
|
public:
|
||||||
void registerCommand(const std::string& id,
|
void registerCommand(const std::string& id,
|
||||||
const std::string& label,
|
const std::string& label,
|
||||||
const std::string& shortcut) {
|
const std::string& shortcut,
|
||||||
|
const std::string& category,
|
||||||
|
int contextMask,
|
||||||
|
const std::string& inputHint,
|
||||||
|
bool inlineInput) {
|
||||||
auto& entry = commands_[id];
|
auto& entry = commands_[id];
|
||||||
entry.id = id;
|
entry.id = id;
|
||||||
entry.label = label;
|
entry.label = label;
|
||||||
entry.shortcut = shortcut;
|
entry.shortcut = shortcut;
|
||||||
|
entry.category = category;
|
||||||
|
entry.contextMask = contextMask;
|
||||||
|
entry.inputHint = inputHint;
|
||||||
|
entry.inlineInput = inlineInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
void markUsed(const std::string& id) {
|
void markUsed(const std::string& id) {
|
||||||
@@ -28,26 +55,48 @@ public:
|
|||||||
it->second.lastUsed = ++useCounter_;
|
it->second.lastUsed = ++useCounter_;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<CommandEntry> search(const std::string& query) const {
|
std::vector<CommandMatch> search(const std::string& query,
|
||||||
std::vector<CommandEntry> results;
|
int contextMask,
|
||||||
|
bool strictContext) const {
|
||||||
|
std::vector<CommandMatch> results;
|
||||||
for (const auto& [_, cmd] : commands_) {
|
for (const auto& [_, cmd] : commands_) {
|
||||||
|
const bool contextOk = matchesContext(cmd, contextMask);
|
||||||
|
if (strictContext && !contextOk) continue;
|
||||||
|
CommandMatch match;
|
||||||
|
match.entry = cmd;
|
||||||
if (query.empty()) {
|
if (query.empty()) {
|
||||||
results.push_back(cmd);
|
match.score = cmd.lastUsed > 0 ? 50 + cmd.lastUsed : 1;
|
||||||
} else {
|
} else {
|
||||||
int score = fuzzyScore(cmd.label, query);
|
int score = 0;
|
||||||
if (score > 0) {
|
if (!fuzzyMatch(cmd.label, query, score, match.matchIndices)) continue;
|
||||||
CommandEntry ranked = cmd;
|
match.score = score;
|
||||||
ranked.lastUsed = cmd.lastUsed + score * 1000;
|
if (contextOk) match.score += 25;
|
||||||
results.push_back(ranked);
|
else match.score -= 10;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
results.push_back(match);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::sort(results.begin(), results.end(), [](const CommandEntry& a, const CommandEntry& b) {
|
std::sort(results.begin(), results.end(), [](const CommandMatch& a, const CommandMatch& b) {
|
||||||
|
if (a.score != b.score) return a.score > b.score;
|
||||||
|
if (a.entry.lastUsed != b.entry.lastUsed) return a.entry.lastUsed > b.entry.lastUsed;
|
||||||
|
return a.entry.label < b.entry.label;
|
||||||
|
});
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<CommandEntry> recent(int maxCount, int contextMask) const {
|
||||||
|
std::vector<CommandEntry> entries;
|
||||||
|
for (const auto& [_, cmd] : commands_) {
|
||||||
|
if (cmd.lastUsed <= 0) continue;
|
||||||
|
if (!matchesContext(cmd, contextMask)) continue;
|
||||||
|
entries.push_back(cmd);
|
||||||
|
}
|
||||||
|
std::sort(entries.begin(), entries.end(), [](const CommandEntry& a, const CommandEntry& b) {
|
||||||
if (a.lastUsed != b.lastUsed) return a.lastUsed > b.lastUsed;
|
if (a.lastUsed != b.lastUsed) return a.lastUsed > b.lastUsed;
|
||||||
return a.label < b.label;
|
return a.label < b.label;
|
||||||
});
|
});
|
||||||
return results;
|
if ((int)entries.size() > maxCount) entries.resize(maxCount);
|
||||||
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<CommandEntry> all() const {
|
std::vector<CommandEntry> all() const {
|
||||||
@@ -56,11 +105,18 @@ public:
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
static bool fuzzyMatch(const std::string& text,
|
||||||
static int fuzzyScore(const std::string& text, const std::string& query) {
|
const std::string& query,
|
||||||
if (query.empty()) return 1;
|
int& outScore,
|
||||||
int score = 0;
|
std::vector<int>& outIndices) {
|
||||||
|
outScore = 0;
|
||||||
|
outIndices.clear();
|
||||||
|
if (query.empty()) {
|
||||||
|
outScore = 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
size_t pos = 0;
|
size_t pos = 0;
|
||||||
|
int score = 0;
|
||||||
for (char qc : query) {
|
for (char qc : query) {
|
||||||
char q = toLower(qc);
|
char q = toLower(qc);
|
||||||
bool found = false;
|
bool found = false;
|
||||||
@@ -68,22 +124,30 @@ private:
|
|||||||
char t = toLower(text[pos]);
|
char t = toLower(text[pos]);
|
||||||
if (t == q) {
|
if (t == q) {
|
||||||
score += 10;
|
score += 10;
|
||||||
|
outIndices.push_back((int)pos);
|
||||||
found = true;
|
found = true;
|
||||||
++pos;
|
++pos;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
++pos;
|
++pos;
|
||||||
}
|
}
|
||||||
if (!found) return 0;
|
if (!found) return false;
|
||||||
}
|
}
|
||||||
return score;
|
outScore = score;
|
||||||
|
return score > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
static char toLower(char c) {
|
static char toLower(char c) {
|
||||||
if (c >= 'A' && c <= 'Z') return char(c - 'A' + 'a');
|
if (c >= 'A' && c <= 'Z') return char(c - 'A' + 'a');
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool matchesContext(const CommandEntry& entry, int contextMask) {
|
||||||
|
if (entry.contextMask == CommandContext_Any) return true;
|
||||||
|
return (entry.contextMask & contextMask) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
std::unordered_map<std::string, CommandEntry> commands_;
|
std::unordered_map<std::string, CommandEntry> commands_;
|
||||||
int useCounter_ = 0;
|
int useCounter_ = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -230,10 +230,12 @@ struct EditorState {
|
|||||||
char refactorNameB[128] = {};
|
char refactorNameB[128] = {};
|
||||||
std::string refactorError;
|
std::string refactorError;
|
||||||
CommandPalette commandPalette;
|
CommandPalette commandPalette;
|
||||||
std::unordered_map<std::string, std::function<void()>> commandHandlers;
|
std::unordered_map<std::string, std::function<void(const std::string&)>> commandHandlers;
|
||||||
bool showCommandPalette = false;
|
bool showCommandPalette = false;
|
||||||
char commandQuery[128] = {};
|
char commandQuery[128] = {};
|
||||||
int commandSelected = 0;
|
int commandSelected = 0;
|
||||||
|
char commandInlineValue[64] = {};
|
||||||
|
std::string commandInlineCommandId;
|
||||||
struct DiffState {
|
struct DiffState {
|
||||||
bool active = false;
|
bool active = false;
|
||||||
bool preview = false;
|
bool preview = false;
|
||||||
@@ -808,6 +810,7 @@ struct EditorState {
|
|||||||
loadFeatureHints(featureHints, workspaceRoot);
|
loadFeatureHints(featureHints, workspaceRoot);
|
||||||
loadSettingsFromDisk();
|
loadSettingsFromDisk();
|
||||||
registerCommands();
|
registerCommands();
|
||||||
|
library.vulnDb.startBackgroundRefresh();
|
||||||
events.subscribe(UIEventType::BufferSwitched, [this](const UIEvent&) {
|
events.subscribe(UIEventType::BufferSwitched, [this](const UIEvent&) {
|
||||||
if (!active()) return;
|
if (!active()) return;
|
||||||
library.primitives.setRoot(activeAST());
|
library.primitives.setRoot(activeAST());
|
||||||
@@ -937,7 +940,7 @@ struct EditorState {
|
|||||||
const std::string& event) {
|
const std::string& event) {
|
||||||
logAgentEvent("Agent " + s.sessionId + " " + event);
|
logAgentEvent("Agent " + s.sessionId + " " + event);
|
||||||
if (event == "connected") {
|
if (event == "connected") {
|
||||||
agent.roles[s.sessionId] = AgentRole::Linter;
|
agent.roles[s.sessionId] = agent.defaultRole;
|
||||||
} else if (event == "disconnected") {
|
} else if (event == "disconnected") {
|
||||||
agent.roles.erase(s.sessionId);
|
agent.roles.erase(s.sessionId);
|
||||||
}
|
}
|
||||||
@@ -1579,8 +1582,21 @@ struct EditorState {
|
|||||||
void registerCommand(const std::string& id,
|
void registerCommand(const std::string& id,
|
||||||
const std::string& label,
|
const std::string& label,
|
||||||
const std::string& shortcut,
|
const std::string& shortcut,
|
||||||
|
const std::string& category,
|
||||||
|
int contextMask,
|
||||||
std::function<void()> fn) {
|
std::function<void()> fn) {
|
||||||
commandPalette.registerCommand(id, label, shortcut);
|
commandPalette.registerCommand(id, label, shortcut, category, contextMask, "", false);
|
||||||
|
commandHandlers[id] = [fn](const std::string&) { fn(); };
|
||||||
|
}
|
||||||
|
|
||||||
|
void registerCommand(const std::string& id,
|
||||||
|
const std::string& label,
|
||||||
|
const std::string& shortcut,
|
||||||
|
const std::string& category,
|
||||||
|
int contextMask,
|
||||||
|
const std::string& inputHint,
|
||||||
|
std::function<void(const std::string&)> fn) {
|
||||||
|
commandPalette.registerCommand(id, label, shortcut, category, contextMask, inputHint, true);
|
||||||
commandHandlers[id] = std::move(fn);
|
commandHandlers[id] = std::move(fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1590,12 +1606,14 @@ struct EditorState {
|
|||||||
|
|
||||||
registerCommand("file.new", "File: New File",
|
registerCommand("file.new", "File: New File",
|
||||||
keys.getBinding("file.new").toString(),
|
keys.getBinding("file.new").toString(),
|
||||||
|
"File", CommandContext_Editor,
|
||||||
[this]() {
|
[this]() {
|
||||||
std::string lang = active() ? active()->language : "python";
|
std::string lang = active() ? active()->language : "python";
|
||||||
createBuffer(makeUntitledName(), "", lang);
|
createBuffer(makeUntitledName(), "", lang);
|
||||||
});
|
});
|
||||||
registerCommand("file.open", "File: Open...",
|
registerCommand("file.open", "File: Open...",
|
||||||
keys.getBinding("file.open").toString(),
|
keys.getBinding("file.open").toString(),
|
||||||
|
"File", CommandContext_Editor,
|
||||||
[this]() {
|
[this]() {
|
||||||
std::string path = FileDialog::openFile(
|
std::string path = FileDialog::openFile(
|
||||||
{"Open File", {"*.py","*.cpp","*.h","*.el","*.js","*.ts","*.java","*.rs","*.go"},
|
{"Open File", {"*.py","*.cpp","*.h","*.el","*.js","*.ts","*.java","*.rs","*.go"},
|
||||||
@@ -1604,9 +1622,11 @@ struct EditorState {
|
|||||||
});
|
});
|
||||||
registerCommand("file.save", "File: Save",
|
registerCommand("file.save", "File: Save",
|
||||||
keys.getBinding("file.save").toString(),
|
keys.getBinding("file.save").toString(),
|
||||||
|
"File", CommandContext_Editor,
|
||||||
[this]() { doSave(); });
|
[this]() { doSave(); });
|
||||||
registerCommand("project.open", "Project: Open...",
|
registerCommand("project.open", "Project: Open...",
|
||||||
"",
|
"",
|
||||||
|
"Project", CommandContext_Editor,
|
||||||
[this]() {
|
[this]() {
|
||||||
std::string path = FileDialog::openFile(
|
std::string path = FileDialog::openFile(
|
||||||
{"Open Project", {"*.whetstone"}, std::string()});
|
{"Open Project", {"*.whetstone"}, std::string()});
|
||||||
@@ -1619,6 +1639,7 @@ struct EditorState {
|
|||||||
});
|
});
|
||||||
registerCommand("project.save", "Project: Save...",
|
registerCommand("project.save", "Project: Save...",
|
||||||
"",
|
"",
|
||||||
|
"Project", CommandContext_Editor,
|
||||||
[this]() {
|
[this]() {
|
||||||
std::string path = FileDialog::saveFile(
|
std::string path = FileDialog::saveFile(
|
||||||
{"Save Project", {"*.whetstone"}, std::string()});
|
{"Save Project", {"*.whetstone"}, std::string()});
|
||||||
@@ -1631,52 +1652,82 @@ struct EditorState {
|
|||||||
});
|
});
|
||||||
registerCommand("edit.undo", "Edit: Undo",
|
registerCommand("edit.undo", "Edit: Undo",
|
||||||
keys.getBinding("edit.undo").toString(),
|
keys.getBinding("edit.undo").toString(),
|
||||||
|
"Edit", CommandContext_Editor,
|
||||||
[this]() { doUndo(); });
|
[this]() { doUndo(); });
|
||||||
registerCommand("edit.redo", "Edit: Redo",
|
registerCommand("edit.redo", "Edit: Redo",
|
||||||
keys.getBinding("edit.redo").toString(),
|
keys.getBinding("edit.redo").toString(),
|
||||||
|
"Edit", CommandContext_Editor,
|
||||||
[this]() { doRedo(); });
|
[this]() { doRedo(); });
|
||||||
registerCommand("search.find", "Search: Find/Replace",
|
registerCommand("search.find", "Search: Find/Replace",
|
||||||
keys.getBinding("search.find").toString(),
|
keys.getBinding("search.find").toString(),
|
||||||
|
"Search", CommandContext_Editor,
|
||||||
[this]() {
|
[this]() {
|
||||||
search.showFind = true;
|
search.showFind = true;
|
||||||
search.showReplace = false;
|
search.showReplace = false;
|
||||||
});
|
});
|
||||||
registerCommand("search.replace", "Search: Replace",
|
registerCommand("search.replace", "Search: Replace",
|
||||||
keys.getBinding("search.replace").toString(),
|
keys.getBinding("search.replace").toString(),
|
||||||
|
"Search", CommandContext_Editor,
|
||||||
[this]() {
|
[this]() {
|
||||||
search.showFind = true;
|
search.showFind = true;
|
||||||
search.showReplace = true;
|
search.showReplace = true;
|
||||||
});
|
});
|
||||||
registerCommand("search.findNext", "Search: Find Next",
|
registerCommand("search.findNext", "Search: Find Next",
|
||||||
keys.getBinding("search.findNext").toString(),
|
keys.getBinding("search.findNext").toString(),
|
||||||
|
"Search", CommandContext_Editor,
|
||||||
[this]() { doFindNext(false); });
|
[this]() { doFindNext(false); });
|
||||||
registerCommand("search.findPrev", "Search: Find Previous",
|
registerCommand("search.findPrev", "Search: Find Previous",
|
||||||
keys.getBinding("search.findPrev").toString(),
|
keys.getBinding("search.findPrev").toString(),
|
||||||
|
"Search", CommandContext_Editor,
|
||||||
[this]() { doFindNext(true); });
|
[this]() { doFindNext(true); });
|
||||||
registerCommand("search.findInFiles", "Search: Find in Files",
|
registerCommand("search.findInFiles", "Search: Find in Files",
|
||||||
keys.getBinding("search.findInFiles").toString(),
|
keys.getBinding("search.findInFiles").toString(),
|
||||||
|
"Search", CommandContext_Search,
|
||||||
[this]() { search.showProjectSearch = !search.showProjectSearch; });
|
[this]() { search.showProjectSearch = !search.showProjectSearch; });
|
||||||
registerCommand("nav.goToLine", "Navigate: Go to Line",
|
registerCommand("nav.goToLine", "Navigate: Go to Line",
|
||||||
keys.getBinding("nav.goToLine").toString(),
|
keys.getBinding("nav.goToLine").toString(),
|
||||||
[this]() {
|
"Navigate", CommandContext_Editor, "line[:col]",
|
||||||
|
[this](const std::string& arg) {
|
||||||
|
if (!arg.empty()) {
|
||||||
|
int line = 0;
|
||||||
|
int col = 0;
|
||||||
|
if (parseLineColInput(arg, line, col)) {
|
||||||
|
if (active()) {
|
||||||
|
int totalLines = countLines(active()->editBuf);
|
||||||
|
if (totalLines > 0) line = std::max(1, std::min(line, totalLines));
|
||||||
|
col = std::max(1, col);
|
||||||
|
jumpTo(active(), line - 1, col - 1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
notify(NotificationLevel::Warning, "Go to Line: invalid input.");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
search.showGoToLine = true;
|
search.showGoToLine = true;
|
||||||
search.goToLineBuf[0] = '\0';
|
search.goToLineBuf[0] = '\0';
|
||||||
search.goToLineError = false;
|
search.goToLineError = false;
|
||||||
});
|
});
|
||||||
registerCommand("view.whitespace", "View: Toggle Whitespace", "",
|
registerCommand("view.whitespace", "View: Toggle Whitespace", "",
|
||||||
|
"View", CommandContext_Editor,
|
||||||
[this]() { ui.showWhitespace = !ui.showWhitespace; });
|
[this]() { ui.showWhitespace = !ui.showWhitespace; });
|
||||||
registerCommand("view.minimap", "View: Toggle Minimap", "",
|
registerCommand("view.minimap", "View: Toggle Minimap", "",
|
||||||
|
"View", CommandContext_Editor,
|
||||||
[this]() { ui.showMinimap = !ui.showMinimap; });
|
[this]() { ui.showMinimap = !ui.showMinimap; });
|
||||||
registerCommand("view.annotations", "View: Toggle Annotations", "",
|
registerCommand("view.annotations", "View: Toggle Annotations", "",
|
||||||
|
"View", CommandContext_Editor,
|
||||||
[this]() { ui.showAnnotations = !ui.showAnnotations; });
|
[this]() { ui.showAnnotations = !ui.showAnnotations; });
|
||||||
registerCommand("view.outline", "View: Toggle Outline", "",
|
registerCommand("view.outline", "View: Toggle Outline", "",
|
||||||
|
"View", CommandContext_Editor,
|
||||||
[this]() { ui.showOutline = !ui.showOutline; });
|
[this]() { ui.showOutline = !ui.showOutline; });
|
||||||
registerCommand("view.toggleTerminal", "View: Toggle Terminal",
|
registerCommand("view.toggleTerminal", "View: Toggle Terminal",
|
||||||
keys.getBinding("view.toggleTerminal").toString(),
|
keys.getBinding("view.toggleTerminal").toString(),
|
||||||
|
"View", CommandContext_Terminal,
|
||||||
[this]() { build.showTerminalPanel = !build.showTerminalPanel; });
|
[this]() { build.showTerminalPanel = !build.showTerminalPanel; });
|
||||||
registerCommand("view.lsp", "View: LSP Servers...", "",
|
registerCommand("view.lsp", "View: LSP Servers...", "",
|
||||||
|
"View", CommandContext_Settings,
|
||||||
[this]() { ui.showLspSettings = !ui.showLspSettings; });
|
[this]() { ui.showLspSettings = !ui.showLspSettings; });
|
||||||
registerCommand("mode.toggle", "Mode: Toggle Text/Structured", "",
|
registerCommand("mode.toggle", "Mode: Toggle Text/Structured", "",
|
||||||
|
"Mode", CommandContext_Editor,
|
||||||
[this]() {
|
[this]() {
|
||||||
if (!active()) return;
|
if (!active()) return;
|
||||||
active()->bufferMode = active()->bufferMode == BufferManager::BufferMode::Text ?
|
active()->bufferMode = active()->bufferMode == BufferManager::BufferMode::Text ?
|
||||||
@@ -1691,6 +1742,7 @@ struct EditorState {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
registerCommand("refactor.rename", "Refactor: Rename Variable", "",
|
registerCommand("refactor.rename", "Refactor: Rename Variable", "",
|
||||||
|
"Refactor", CommandContext_Editor,
|
||||||
[this]() {
|
[this]() {
|
||||||
refactorAction = 1;
|
refactorAction = 1;
|
||||||
showRefactorPopup = true;
|
showRefactorPopup = true;
|
||||||
@@ -1698,6 +1750,7 @@ struct EditorState {
|
|||||||
refactorNameB[0] = '\0';
|
refactorNameB[0] = '\0';
|
||||||
});
|
});
|
||||||
registerCommand("refactor.extract", "Refactor: Extract Function", "",
|
registerCommand("refactor.extract", "Refactor: Extract Function", "",
|
||||||
|
"Refactor", CommandContext_Editor,
|
||||||
[this]() {
|
[this]() {
|
||||||
refactorAction = 2;
|
refactorAction = 2;
|
||||||
showRefactorPopup = true;
|
showRefactorPopup = true;
|
||||||
@@ -1705,22 +1758,34 @@ struct EditorState {
|
|||||||
refactorNameB[0] = '\0';
|
refactorNameB[0] = '\0';
|
||||||
});
|
});
|
||||||
registerCommand("refactor.inline", "Refactor: Inline Variable", "",
|
registerCommand("refactor.inline", "Refactor: Inline Variable", "",
|
||||||
|
"Refactor", CommandContext_Editor,
|
||||||
[this]() {
|
[this]() {
|
||||||
refactorAction = 3;
|
refactorAction = 3;
|
||||||
showRefactorPopup = true;
|
showRefactorPopup = true;
|
||||||
refactorNameA[0] = '\0';
|
refactorNameA[0] = '\0';
|
||||||
refactorNameB[0] = '\0';
|
refactorNameB[0] = '\0';
|
||||||
});
|
});
|
||||||
|
registerCommand("wizard.annotate", "Wizard: Annotate File", "",
|
||||||
|
"Workflow", CommandContext_Editor,
|
||||||
|
[this]() { ui.showAnnotateWizard = true; });
|
||||||
|
registerCommand("wizard.project", "Wizard: Cross-Language Project", "",
|
||||||
|
"Workflow", CommandContext_Editor,
|
||||||
|
[this]() { ui.showProjectWizard = true; });
|
||||||
|
registerCommand("wizard.agent", "Wizard: Connect Agent", "",
|
||||||
|
"Workflow", CommandContext_Editor,
|
||||||
|
[this]() { ui.showAgentWizard = true; });
|
||||||
registerCommand("build.run", "Build: Run", keys.getBinding("build.run").toString(),
|
registerCommand("build.run", "Build: Run", keys.getBinding("build.run").toString(),
|
||||||
|
"Build", CommandContext_Terminal,
|
||||||
[this]() { runActiveFile(false); });
|
[this]() { runActiveFile(false); });
|
||||||
registerCommand("build.build", "Build: Build", keys.getBinding("build.build").toString(),
|
registerCommand("build.build", "Build: Build", keys.getBinding("build.build").toString(),
|
||||||
|
"Build", CommandContext_Terminal,
|
||||||
[this]() { runActiveFile(true); });
|
[this]() { runActiveFile(true); });
|
||||||
}
|
}
|
||||||
|
|
||||||
void executeCommand(const std::string& id) {
|
void executeCommand(const std::string& id, const std::string& arg = {}) {
|
||||||
auto it = commandHandlers.find(id);
|
auto it = commandHandlers.find(id);
|
||||||
if (it == commandHandlers.end()) return;
|
if (it == commandHandlers.end()) return;
|
||||||
it->second();
|
it->second(arg);
|
||||||
commandPalette.markUsed(id);
|
commandPalette.markUsed(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
395
editor/src/VulnerabilityDatabase.h
Normal file
395
editor/src/VulnerabilityDatabase.h
Normal file
@@ -0,0 +1,395 @@
|
|||||||
|
#pragma once
|
||||||
|
// Step 190: Vulnerability knowledge base (OSV)
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <functional>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <mutex>
|
||||||
|
#include <chrono>
|
||||||
|
#include <thread>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
struct VulnerabilityRecord {
|
||||||
|
std::string ecosystem;
|
||||||
|
std::string package;
|
||||||
|
std::string cveId;
|
||||||
|
std::string summary;
|
||||||
|
std::string severity;
|
||||||
|
std::vector<std::string> affectedVersions;
|
||||||
|
std::vector<std::string> fixedVersions;
|
||||||
|
std::vector<std::string> references;
|
||||||
|
struct VersionRange {
|
||||||
|
std::string introduced;
|
||||||
|
std::string fixed;
|
||||||
|
};
|
||||||
|
std::vector<VersionRange> ranges;
|
||||||
|
};
|
||||||
|
|
||||||
|
class VulnerabilityDatabase {
|
||||||
|
public:
|
||||||
|
using Fetcher = std::function<std::string(const std::string& ecosystem,
|
||||||
|
const std::string& package)>;
|
||||||
|
|
||||||
|
void setCacheDirectory(const std::string& path) {
|
||||||
|
std::scoped_lock lock(mu_);
|
||||||
|
cacheDir_ = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setTTLHours(int hours) {
|
||||||
|
std::scoped_lock lock(mu_);
|
||||||
|
ttlHours_ = std::max(1, hours);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setOfflineMode(bool offline) {
|
||||||
|
std::scoped_lock lock(mu_);
|
||||||
|
offlineMode_ = offline;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setFetcher(Fetcher fetcher) {
|
||||||
|
std::scoped_lock lock(mu_);
|
||||||
|
fetcher_ = std::move(fetcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
void trackPackage(const std::string& ecosystem,
|
||||||
|
const std::string& package) {
|
||||||
|
std::scoped_lock lock(mu_);
|
||||||
|
tracked_.push_back(makeKey(ecosystem, package));
|
||||||
|
}
|
||||||
|
|
||||||
|
void startBackgroundRefresh() {
|
||||||
|
std::thread([this]() { refreshTrackedPackages(); }).detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<VulnerabilityRecord> query(const std::string& ecosystem,
|
||||||
|
const std::string& package,
|
||||||
|
const std::string& version) {
|
||||||
|
ensureLoaded(ecosystem, package);
|
||||||
|
const std::string key = makeKey(ecosystem, package);
|
||||||
|
std::scoped_lock lock(mu_);
|
||||||
|
auto it = cache_.find(key);
|
||||||
|
if (it == cache_.end()) return {};
|
||||||
|
if (version.empty()) return it->second;
|
||||||
|
|
||||||
|
std::vector<VulnerabilityRecord> out;
|
||||||
|
for (const auto& record : it->second) {
|
||||||
|
if (isAffected(record, version)) out.push_back(record);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::string cacheDir_ = defaultCacheDir();
|
||||||
|
int ttlHours_ = 24;
|
||||||
|
bool offlineMode_ = false;
|
||||||
|
Fetcher fetcher_;
|
||||||
|
std::vector<std::string> tracked_;
|
||||||
|
std::unordered_map<std::string, std::vector<VulnerabilityRecord>> cache_;
|
||||||
|
std::mutex mu_;
|
||||||
|
|
||||||
|
static std::string defaultCacheDir() {
|
||||||
|
const char* home = std::getenv("USERPROFILE");
|
||||||
|
if (!home) home = std::getenv("HOME");
|
||||||
|
std::filesystem::path base = home ? home : ".";
|
||||||
|
return (base / ".whetstone" / "vuln_cache").string();
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string makeKey(const std::string& ecosystem,
|
||||||
|
const std::string& package) {
|
||||||
|
return ecosystem + ":" + package;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string sanitizeFileName(const std::string& name) {
|
||||||
|
std::string out;
|
||||||
|
out.reserve(name.size());
|
||||||
|
for (char c : name) {
|
||||||
|
if (std::isalnum(static_cast<unsigned char>(c)) || c == '_' || c == '-') {
|
||||||
|
out.push_back(c);
|
||||||
|
} else if (c == ':' || c == '/' || c == '\\') {
|
||||||
|
out.push_back('_');
|
||||||
|
} else {
|
||||||
|
out.push_back('_');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path cachePathFor(const std::string& ecosystem,
|
||||||
|
const std::string& package) const {
|
||||||
|
std::filesystem::path dir(cacheDir_);
|
||||||
|
std::string file = sanitizeFileName(makeKey(ecosystem, package)) + ".json";
|
||||||
|
return dir / file;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool cacheIsStale(const std::filesystem::path& path) const {
|
||||||
|
if (!std::filesystem::exists(path)) return true;
|
||||||
|
auto last = std::filesystem::last_write_time(path);
|
||||||
|
auto now = std::filesystem::file_time_type::clock::now();
|
||||||
|
auto age = now - last;
|
||||||
|
auto hours = std::chrono::duration_cast<std::chrono::hours>(age).count();
|
||||||
|
return hours >= ttlHours_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ensureLoaded(const std::string& ecosystem,
|
||||||
|
const std::string& package) {
|
||||||
|
const std::string key = makeKey(ecosystem, package);
|
||||||
|
{
|
||||||
|
std::scoped_lock lock(mu_);
|
||||||
|
if (cache_.find(key) != cache_.end()) return;
|
||||||
|
}
|
||||||
|
refreshPackage(ecosystem, package, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void refreshTrackedPackages() {
|
||||||
|
std::vector<std::string> tracked;
|
||||||
|
{
|
||||||
|
std::scoped_lock lock(mu_);
|
||||||
|
tracked = tracked_;
|
||||||
|
}
|
||||||
|
for (const auto& key : tracked) {
|
||||||
|
auto pos = key.find(':');
|
||||||
|
if (pos == std::string::npos) continue;
|
||||||
|
refreshPackage(key.substr(0, pos), key.substr(pos + 1), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void refreshPackage(const std::string& ecosystem,
|
||||||
|
const std::string& package,
|
||||||
|
bool allowFetch) {
|
||||||
|
std::filesystem::path path = cachePathFor(ecosystem, package);
|
||||||
|
if (!allowFetch && !cacheIsStale(path)) {
|
||||||
|
loadCache(path, ecosystem, package);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (allowFetch && !offlineMode_ && fetcher_) {
|
||||||
|
std::string payload = fetcher_(ecosystem, package);
|
||||||
|
if (!payload.empty()) {
|
||||||
|
std::filesystem::create_directories(path.parent_path());
|
||||||
|
std::ofstream out(path);
|
||||||
|
if (out.is_open()) {
|
||||||
|
out << payload;
|
||||||
|
}
|
||||||
|
loadCache(path, ecosystem, package);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadCache(path, ecosystem, package);
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadCache(const std::filesystem::path& path,
|
||||||
|
const std::string& ecosystem,
|
||||||
|
const std::string& package) {
|
||||||
|
std::ifstream in(path);
|
||||||
|
if (!in.is_open()) return;
|
||||||
|
nlohmann::json doc;
|
||||||
|
try {
|
||||||
|
in >> doc;
|
||||||
|
} catch (...) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto records = parseOSV(doc);
|
||||||
|
std::scoped_lock lock(mu_);
|
||||||
|
cache_[makeKey(ecosystem, package)] = std::move(records);
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<VulnerabilityRecord> parseOSV(const nlohmann::json& doc) {
|
||||||
|
std::vector<VulnerabilityRecord> records;
|
||||||
|
if (!doc.is_object()) return records;
|
||||||
|
nlohmann::json vulns;
|
||||||
|
if (doc.contains("vulnerabilities")) {
|
||||||
|
vulns = doc.at("vulnerabilities");
|
||||||
|
} else if (doc.contains("id")) {
|
||||||
|
vulns = nlohmann::json::array({doc});
|
||||||
|
} else {
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& vuln : vulns) {
|
||||||
|
if (!vuln.is_object()) continue;
|
||||||
|
VulnerabilityRecord record;
|
||||||
|
record.summary = vuln.value("summary", "");
|
||||||
|
record.cveId = pickCVE(vuln);
|
||||||
|
record.severity = deriveSeverity(vuln);
|
||||||
|
record.references = parseReferences(vuln);
|
||||||
|
|
||||||
|
if (!vuln.contains("affected") || !vuln["affected"].is_array()) continue;
|
||||||
|
for (const auto& affected : vuln["affected"]) {
|
||||||
|
if (!affected.is_object()) continue;
|
||||||
|
if (!affected.contains("package")) continue;
|
||||||
|
const auto& pkg = affected["package"];
|
||||||
|
record.ecosystem = pkg.value("ecosystem", "");
|
||||||
|
record.package = pkg.value("name", "");
|
||||||
|
record.affectedVersions = parseVersions(affected);
|
||||||
|
record.fixedVersions = parseFixedVersions(affected);
|
||||||
|
record.ranges = parseRanges(affected);
|
||||||
|
records.push_back(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<std::string> parseReferences(const nlohmann::json& vuln) {
|
||||||
|
std::vector<std::string> refs;
|
||||||
|
if (!vuln.contains("references")) return refs;
|
||||||
|
for (const auto& ref : vuln["references"]) {
|
||||||
|
if (!ref.is_object()) continue;
|
||||||
|
std::string url = ref.value("url", "");
|
||||||
|
if (!url.empty()) refs.push_back(url);
|
||||||
|
}
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string pickCVE(const nlohmann::json& vuln) {
|
||||||
|
if (vuln.contains("aliases")) {
|
||||||
|
for (const auto& alias : vuln["aliases"]) {
|
||||||
|
if (alias.is_string()) {
|
||||||
|
std::string val = alias.get<std::string>();
|
||||||
|
if (val.rfind("CVE-", 0) == 0) return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vuln.value("id", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string deriveSeverity(const nlohmann::json& vuln) {
|
||||||
|
if (vuln.contains("severity") && vuln["severity"].is_array()) {
|
||||||
|
for (const auto& sev : vuln["severity"]) {
|
||||||
|
std::string score = sev.value("score", "");
|
||||||
|
double val = 0.0;
|
||||||
|
try {
|
||||||
|
val = std::stod(score);
|
||||||
|
} catch (...) {
|
||||||
|
val = 0.0;
|
||||||
|
}
|
||||||
|
if (val >= 9.0) return "Critical";
|
||||||
|
if (val >= 7.0) return "High";
|
||||||
|
if (val >= 4.0) return "Medium";
|
||||||
|
if (val > 0.0) return "Low";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (vuln.contains("database_specific")) {
|
||||||
|
const auto& db = vuln["database_specific"];
|
||||||
|
if (db.contains("severity")) return db["severity"].get<std::string>();
|
||||||
|
}
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<std::string> parseVersions(const nlohmann::json& affected) {
|
||||||
|
std::vector<std::string> versions;
|
||||||
|
if (affected.contains("versions") && affected["versions"].is_array()) {
|
||||||
|
for (const auto& v : affected["versions"]) {
|
||||||
|
if (v.is_string()) versions.push_back(v.get<std::string>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return versions;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<std::string> parseFixedVersions(const nlohmann::json& affected) {
|
||||||
|
std::vector<std::string> versions;
|
||||||
|
if (!affected.contains("ranges") || !affected["ranges"].is_array()) return versions;
|
||||||
|
for (const auto& range : affected["ranges"]) {
|
||||||
|
if (!range.contains("events")) continue;
|
||||||
|
for (const auto& event : range["events"]) {
|
||||||
|
if (event.contains("fixed")) {
|
||||||
|
versions.push_back(event["fixed"].get<std::string>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return versions;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::vector<VulnerabilityRecord::VersionRange>
|
||||||
|
parseRanges(const nlohmann::json& affected) {
|
||||||
|
std::vector<VulnerabilityRecord::VersionRange> ranges;
|
||||||
|
if (!affected.contains("ranges") || !affected["ranges"].is_array()) return ranges;
|
||||||
|
for (const auto& range : affected["ranges"]) {
|
||||||
|
if (!range.contains("events")) continue;
|
||||||
|
VulnerabilityRecord::VersionRange current;
|
||||||
|
bool hasOpen = false;
|
||||||
|
for (const auto& event : range["events"]) {
|
||||||
|
if (event.contains("introduced")) {
|
||||||
|
current.introduced = event["introduced"].get<std::string>();
|
||||||
|
current.fixed.clear();
|
||||||
|
hasOpen = true;
|
||||||
|
} else if (event.contains("fixed")) {
|
||||||
|
if (!hasOpen) current.introduced = "0";
|
||||||
|
current.fixed = event["fixed"].get<std::string>();
|
||||||
|
ranges.push_back(current);
|
||||||
|
hasOpen = false;
|
||||||
|
} else if (event.contains("last_affected")) {
|
||||||
|
if (!hasOpen) current.introduced = "0";
|
||||||
|
current.fixed = event["last_affected"].get<std::string>();
|
||||||
|
ranges.push_back(current);
|
||||||
|
hasOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasOpen) {
|
||||||
|
ranges.push_back(current);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ranges;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool isAffected(const VulnerabilityRecord& record,
|
||||||
|
const std::string& version) {
|
||||||
|
for (const auto& v : record.affectedVersions) {
|
||||||
|
if (v == version) return true;
|
||||||
|
}
|
||||||
|
for (const auto& range : record.ranges) {
|
||||||
|
if (versionInRange(version, range.introduced, range.fixed)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int compareVersions(const std::string& a, const std::string& b) {
|
||||||
|
auto split = [](const std::string& v) {
|
||||||
|
std::vector<std::string> parts;
|
||||||
|
std::string cur;
|
||||||
|
for (char c : v) {
|
||||||
|
if (std::isalnum(static_cast<unsigned char>(c)) || c == '.') {
|
||||||
|
cur.push_back(c);
|
||||||
|
} else if (!cur.empty()) {
|
||||||
|
parts.push_back(cur);
|
||||||
|
cur.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!cur.empty()) parts.push_back(cur);
|
||||||
|
return parts;
|
||||||
|
};
|
||||||
|
auto pa = split(a);
|
||||||
|
auto pb = split(b);
|
||||||
|
size_t count = std::max(pa.size(), pb.size());
|
||||||
|
for (size_t i = 0; i < count; ++i) {
|
||||||
|
std::string sa = i < pa.size() ? pa[i] : "0";
|
||||||
|
std::string sb = i < pb.size() ? pb[i] : "0";
|
||||||
|
if (isNumber(sa) && isNumber(sb)) {
|
||||||
|
long long ia = std::stoll(sa);
|
||||||
|
long long ib = std::stoll(sb);
|
||||||
|
if (ia < ib) return -1;
|
||||||
|
if (ia > ib) return 1;
|
||||||
|
} else {
|
||||||
|
if (sa < sb) return -1;
|
||||||
|
if (sa > sb) return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool isNumber(const std::string& s) {
|
||||||
|
if (s.empty()) return false;
|
||||||
|
return std::all_of(s.begin(), s.end(),
|
||||||
|
[](unsigned char c) { return std::isdigit(c); });
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool versionInRange(const std::string& version,
|
||||||
|
const std::string& introduced,
|
||||||
|
const std::string& fixed) {
|
||||||
|
if (!introduced.empty() && compareVersions(version, introduced) < 0) return false;
|
||||||
|
if (!fixed.empty() && compareVersions(version, fixed) >= 0) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
67
editor/src/WizardFramework.h
Normal file
67
editor/src/WizardFramework.h
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "imgui.h"
|
||||||
|
|
||||||
|
enum class WizardNavAction {
|
||||||
|
None,
|
||||||
|
Back,
|
||||||
|
Next,
|
||||||
|
Finish,
|
||||||
|
Cancel
|
||||||
|
};
|
||||||
|
|
||||||
|
struct WizardFrame {
|
||||||
|
bool open = false;
|
||||||
|
int step = 0;
|
||||||
|
int stepCount = 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
static bool beginWizardWindow(const char* title,
|
||||||
|
WizardFrame& frame,
|
||||||
|
const ImVec2& size = ImVec2(560, 420)) {
|
||||||
|
if (!frame.open) return false;
|
||||||
|
ImGui::SetNextWindowSize(size, ImGuiCond_FirstUseEver);
|
||||||
|
if (!ImGui::Begin(title, &frame.open, ImGuiWindowFlags_NoResize)) {
|
||||||
|
ImGui::End();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int total = frame.stepCount > 0 ? frame.stepCount : 1;
|
||||||
|
int step = std::max(0, std::min(frame.step, total - 1));
|
||||||
|
frame.step = step;
|
||||||
|
ImGui::TextDisabled("Step %d of %d", step + 1, total);
|
||||||
|
ImGui::Separator();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static WizardNavAction renderWizardFooter(WizardFrame& frame,
|
||||||
|
const char* finishLabel = "Finish") {
|
||||||
|
WizardNavAction action = WizardNavAction::None;
|
||||||
|
ImGui::Separator();
|
||||||
|
if (ImGui::Button("Cancel")) {
|
||||||
|
frame.open = false;
|
||||||
|
action = WizardNavAction::Cancel;
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
ImGui::Dummy(ImVec2(8.0f, 0.0f));
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (frame.step > 0) {
|
||||||
|
if (ImGui::Button("Back")) {
|
||||||
|
frame.step = std::max(0, frame.step - 1);
|
||||||
|
action = WizardNavAction::Back;
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
}
|
||||||
|
bool isLast = frame.step >= frame.stepCount - 1;
|
||||||
|
if (!isLast) {
|
||||||
|
if (ImGui::Button("Next")) {
|
||||||
|
frame.step = std::min(frame.step + 1, frame.stepCount - 1);
|
||||||
|
action = WizardNavAction::Next;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (ImGui::Button(finishLabel)) {
|
||||||
|
frame.open = false;
|
||||||
|
action = WizardNavAction::Finish;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return action;
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
#include "CompletionUtils.h"
|
#include "CompletionUtils.h"
|
||||||
#include "ThemeEngine.h"
|
#include "ThemeEngine.h"
|
||||||
#include "FirstRunWizard.h"
|
#include "FirstRunWizard.h"
|
||||||
|
#include "panels/WizardPanels.h"
|
||||||
#include "FeatureHints.h"
|
#include "FeatureHints.h"
|
||||||
#include "ShortcutReference.h"
|
#include "ShortcutReference.h"
|
||||||
|
|
||||||
@@ -381,6 +382,18 @@ int main(int, char**) {
|
|||||||
wizard.open = state.ui.showFirstRunWizard;
|
wizard.open = state.ui.showFirstRunWizard;
|
||||||
renderFirstRunWizard(state, wizard);
|
renderFirstRunWizard(state, wizard);
|
||||||
state.ui.showFirstRunWizard = wizard.open;
|
state.ui.showFirstRunWizard = wizard.open;
|
||||||
|
static AnnotateFileWizardState annotateWizard;
|
||||||
|
annotateWizard.frame.open = state.ui.showAnnotateWizard;
|
||||||
|
renderAnnotateFileWizard(state, annotateWizard);
|
||||||
|
state.ui.showAnnotateWizard = annotateWizard.frame.open;
|
||||||
|
static ProjectWizardState projectWizard;
|
||||||
|
projectWizard.frame.open = state.ui.showProjectWizard;
|
||||||
|
renderProjectWizard(state, projectWizard);
|
||||||
|
state.ui.showProjectWizard = projectWizard.frame.open;
|
||||||
|
static ConnectAgentWizardState agentWizard;
|
||||||
|
agentWizard.frame.open = state.ui.showAgentWizard;
|
||||||
|
renderConnectAgentWizard(state, agentWizard);
|
||||||
|
state.ui.showAgentWizard = agentWizard.frame.open;
|
||||||
renderFeatureHintBar(state.featureHints, state.workspaceRoot);
|
renderFeatureHintBar(state.featureHints, state.workspaceRoot);
|
||||||
static ShortcutReferenceState shortcutPanel;
|
static ShortcutReferenceState shortcutPanel;
|
||||||
renderShortcutReference(state, shortcutPanel);
|
renderShortcutReference(state, shortcutPanel);
|
||||||
|
|||||||
@@ -1,6 +1,56 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "../EditorState.h"
|
#include "../EditorState.h"
|
||||||
#include "../EditorUtils.h"
|
#include "../EditorUtils.h"
|
||||||
|
#include <filesystem>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
|
static int buildCommandContextMask(const EditorState& state) {
|
||||||
|
int mask = CommandContext_Editor;
|
||||||
|
if (state.search.showProjectSearch || state.search.showFind) mask |= CommandContext_Search;
|
||||||
|
if (state.ui.showSettingsPanel || state.ui.showLspSettings) mask |= CommandContext_Settings;
|
||||||
|
if (state.ui.showHelpPanel || state.ui.showShortcutReference) mask |= CommandContext_Help;
|
||||||
|
if (state.build.showTerminalPanel) mask |= CommandContext_Terminal;
|
||||||
|
return mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void collectFilePaths(const FileNode& node, std::vector<std::string>& out) {
|
||||||
|
if (!node.isDir) {
|
||||||
|
out.push_back(node.path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const auto& child : node.children) {
|
||||||
|
collectFilePaths(child, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string makeRelativePath(const std::string& path, const std::string& root) {
|
||||||
|
if (root.empty()) return path;
|
||||||
|
std::error_code ec;
|
||||||
|
auto rel = std::filesystem::relative(path, root, ec);
|
||||||
|
if (ec) return path;
|
||||||
|
return rel.generic_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void drawHighlightedText(ImDrawList* drawList,
|
||||||
|
const std::string& text,
|
||||||
|
const std::vector<int>& indices,
|
||||||
|
ImVec2 pos,
|
||||||
|
ImU32 normalColor,
|
||||||
|
ImU32 highlightColor) {
|
||||||
|
std::vector<unsigned char> highlight(text.size(), 0);
|
||||||
|
for (int idx : indices) {
|
||||||
|
if (idx >= 0 && idx < (int)highlight.size()) highlight[idx] = 1;
|
||||||
|
}
|
||||||
|
ImFont* font = ImGui::GetFont();
|
||||||
|
float fontSize = ImGui::GetFontSize();
|
||||||
|
char buf[2] = {0, 0};
|
||||||
|
for (size_t i = 0; i < text.size(); ++i) {
|
||||||
|
buf[0] = text[i];
|
||||||
|
drawList->AddText(font, fontSize, pos,
|
||||||
|
highlight[i] ? highlightColor : normalColor, buf);
|
||||||
|
pos.x += ImGui::CalcTextSize(buf).x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void renderGoToLineDialog(EditorState& state) {
|
static void renderGoToLineDialog(EditorState& state) {
|
||||||
if (state.search.showGoToLine) {
|
if (state.search.showGoToLine) {
|
||||||
@@ -177,42 +227,301 @@ static void renderCommandPalette(EditorState& state) {
|
|||||||
if (!state.showCommandPalette) return;
|
if (!state.showCommandPalette) return;
|
||||||
ImGui::Begin("Command Palette", &state.showCommandPalette,
|
ImGui::Begin("Command Palette", &state.showCommandPalette,
|
||||||
ImGuiWindowFlags_AlwaysAutoResize);
|
ImGuiWindowFlags_AlwaysAutoResize);
|
||||||
ImGui::SetNextItemWidth(420.0f);
|
ImGui::SetNextItemWidth(520.0f);
|
||||||
if (ImGui::IsWindowAppearing()) {
|
if (ImGui::IsWindowAppearing()) {
|
||||||
ImGui::SetKeyboardFocusHere();
|
ImGui::SetKeyboardFocusHere();
|
||||||
}
|
}
|
||||||
if (ImGui::InputText("##cmdQuery", state.commandQuery, sizeof(state.commandQuery))) {
|
const bool hintCommandMode = (state.commandQuery[0] == '>');
|
||||||
|
const char* queryHint = hintCommandMode
|
||||||
|
? "> Type a command"
|
||||||
|
: "Type to search files (prefix '>' for commands)";
|
||||||
|
bool queryChanged = ImGui::InputTextWithHint("##cmdQuery",
|
||||||
|
queryHint,
|
||||||
|
state.commandQuery,
|
||||||
|
sizeof(state.commandQuery),
|
||||||
|
ImGuiInputTextFlags_EnterReturnsTrue);
|
||||||
|
if (queryChanged) {
|
||||||
state.commandSelected = 0;
|
state.commandSelected = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto results = state.commandPalette.search(state.commandQuery);
|
const bool dismiss = ImGui::IsKeyPressed(ImGuiKey_Escape);
|
||||||
int maxIndex = std::max(0, (int)results.size() - 1);
|
if (dismiss) state.showCommandPalette = false;
|
||||||
if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) {
|
|
||||||
state.commandSelected = std::min(state.commandSelected + 1, maxIndex);
|
|
||||||
}
|
|
||||||
if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) {
|
|
||||||
state.commandSelected = std::max(state.commandSelected - 1, 0);
|
|
||||||
}
|
|
||||||
bool accept = ImGui::IsKeyPressed(ImGuiKey_Enter);
|
|
||||||
bool dismiss = ImGui::IsKeyPressed(ImGuiKey_Escape);
|
|
||||||
|
|
||||||
if (dismiss) {
|
std::string rawQuery = state.commandQuery;
|
||||||
state.showCommandPalette = false;
|
bool commandMode = false;
|
||||||
|
std::string query = rawQuery;
|
||||||
|
if (!query.empty() && query[0] == '>') {
|
||||||
|
commandMode = true;
|
||||||
|
query.erase(0, 1);
|
||||||
|
if (!query.empty() && query[0] == ' ') query.erase(0, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < (int)results.size(); ++i) {
|
if (commandMode) {
|
||||||
const auto& cmd = results[i];
|
const int contextMask = buildCommandContextMask(state);
|
||||||
bool selected = (i == state.commandSelected);
|
const bool strictContext = query.empty();
|
||||||
std::string label = cmd.label;
|
auto results = state.commandPalette.search(query, contextMask, strictContext);
|
||||||
if (!cmd.shortcut.empty()) label += " [" + cmd.shortcut + "]";
|
std::vector<CommandEntry> recent =
|
||||||
if (ImGui::Selectable(label.c_str(), selected)) {
|
query.empty() ? state.commandPalette.recent(5, contextMask) : std::vector<CommandEntry>();
|
||||||
state.commandSelected = i;
|
std::vector<CommandMatch> recentMatches;
|
||||||
accept = true;
|
recentMatches.reserve(recent.size());
|
||||||
|
std::unordered_set<std::string> recentIds;
|
||||||
|
for (const auto& entry : recent) {
|
||||||
|
recentIds.insert(entry.id);
|
||||||
|
CommandMatch match;
|
||||||
|
match.entry = entry;
|
||||||
|
recentMatches.push_back(std::move(match));
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CategoryGroup {
|
||||||
|
std::string label;
|
||||||
|
std::vector<CommandMatch> items;
|
||||||
|
int bestScore = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::unordered_map<std::string, CategoryGroup> groupsByName;
|
||||||
|
for (const auto& match : results) {
|
||||||
|
if (query.empty() && recentIds.count(match.entry.id)) continue;
|
||||||
|
std::string category = match.entry.category.empty() ? "Other" : match.entry.category;
|
||||||
|
auto& group = groupsByName[category];
|
||||||
|
group.label = category;
|
||||||
|
group.items.push_back(match);
|
||||||
|
group.bestScore = std::max(group.bestScore, match.score);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<CategoryGroup> groups;
|
||||||
|
groups.reserve(groupsByName.size());
|
||||||
|
for (auto& [_, group] : groupsByName) {
|
||||||
|
std::sort(group.items.begin(), group.items.end(),
|
||||||
|
[](const CommandMatch& a, const CommandMatch& b) {
|
||||||
|
if (a.score != b.score) return a.score > b.score;
|
||||||
|
return a.entry.label < b.entry.label;
|
||||||
|
});
|
||||||
|
groups.push_back(std::move(group));
|
||||||
|
}
|
||||||
|
std::sort(groups.begin(), groups.end(),
|
||||||
|
[](const CategoryGroup& a, const CategoryGroup& b) {
|
||||||
|
if (a.bestScore != b.bestScore) return a.bestScore > b.bestScore;
|
||||||
|
return a.label < b.label;
|
||||||
|
});
|
||||||
|
|
||||||
|
int totalItems = (int)recentMatches.size();
|
||||||
|
for (const auto& group : groups) totalItems += (int)group.items.size();
|
||||||
|
int maxIndex = std::max(0, totalItems - 1);
|
||||||
|
state.commandSelected = std::min(state.commandSelected, maxIndex);
|
||||||
|
if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) {
|
||||||
|
state.commandSelected = std::min(state.commandSelected + 1, maxIndex);
|
||||||
|
}
|
||||||
|
if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) {
|
||||||
|
state.commandSelected = std::max(state.commandSelected - 1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImU32 normalColor = ImGui::GetColorU32(ImGuiCol_Text);
|
||||||
|
const ImU32 disabledColor = ImGui::GetColorU32(ImGuiCol_TextDisabled);
|
||||||
|
const ImU32 highlightColor = ImGui::GetColorU32(ImGuiCol_PlotHistogram);
|
||||||
|
int displayIndex = 0;
|
||||||
|
const CommandMatch* selectedMatch = nullptr;
|
||||||
|
bool accept = ImGui::IsKeyPressed(ImGuiKey_Enter);
|
||||||
|
|
||||||
|
if (!recentMatches.empty()) {
|
||||||
|
ImGui::TextDisabled("Recent");
|
||||||
|
ImGui::Separator();
|
||||||
|
for (const auto& match : recentMatches) {
|
||||||
|
bool selected = (displayIndex == state.commandSelected);
|
||||||
|
std::string rowId = "##cmd_recent_" + std::to_string(displayIndex);
|
||||||
|
if (ImGui::Selectable(rowId.c_str(), selected,
|
||||||
|
ImGuiSelectableFlags_SpanAvailWidth,
|
||||||
|
ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing()))) {
|
||||||
|
state.commandSelected = displayIndex;
|
||||||
|
accept = true;
|
||||||
|
}
|
||||||
|
ImVec2 min = ImGui::GetItemRectMin();
|
||||||
|
ImVec2 max = ImGui::GetItemRectMax();
|
||||||
|
ImVec2 textPos(min.x + ImGui::GetStyle().FramePadding.x,
|
||||||
|
min.y + ImGui::GetStyle().FramePadding.y);
|
||||||
|
drawHighlightedText(ImGui::GetWindowDrawList(),
|
||||||
|
match.entry.label,
|
||||||
|
match.matchIndices,
|
||||||
|
textPos,
|
||||||
|
normalColor,
|
||||||
|
highlightColor);
|
||||||
|
if (!match.entry.shortcut.empty()) {
|
||||||
|
ImVec2 size = ImGui::CalcTextSize(match.entry.shortcut.c_str());
|
||||||
|
ImGui::GetWindowDrawList()->AddText(
|
||||||
|
ImVec2(max.x - ImGui::GetStyle().FramePadding.x - size.x, textPos.y),
|
||||||
|
disabledColor,
|
||||||
|
match.entry.shortcut.c_str());
|
||||||
|
}
|
||||||
|
if (selected) selectedMatch = &match;
|
||||||
|
++displayIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& group : groups) {
|
||||||
|
if (group.items.empty()) continue;
|
||||||
|
if (!recentMatches.empty() || !group.label.empty()) {
|
||||||
|
if (displayIndex > 0) ImGui::Separator();
|
||||||
|
ImGui::TextDisabled("%s", group.label.c_str());
|
||||||
|
}
|
||||||
|
for (const auto& match : group.items) {
|
||||||
|
bool selected = (displayIndex == state.commandSelected);
|
||||||
|
std::string rowId = "##cmd_" + std::to_string(displayIndex);
|
||||||
|
if (ImGui::Selectable(rowId.c_str(), selected,
|
||||||
|
ImGuiSelectableFlags_SpanAvailWidth,
|
||||||
|
ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing()))) {
|
||||||
|
state.commandSelected = displayIndex;
|
||||||
|
accept = true;
|
||||||
|
}
|
||||||
|
ImVec2 min = ImGui::GetItemRectMin();
|
||||||
|
ImVec2 max = ImGui::GetItemRectMax();
|
||||||
|
ImVec2 textPos(min.x + ImGui::GetStyle().FramePadding.x,
|
||||||
|
min.y + ImGui::GetStyle().FramePadding.y);
|
||||||
|
drawHighlightedText(ImGui::GetWindowDrawList(),
|
||||||
|
match.entry.label,
|
||||||
|
match.matchIndices,
|
||||||
|
textPos,
|
||||||
|
normalColor,
|
||||||
|
highlightColor);
|
||||||
|
if (!match.entry.shortcut.empty()) {
|
||||||
|
ImVec2 size = ImGui::CalcTextSize(match.entry.shortcut.c_str());
|
||||||
|
ImGui::GetWindowDrawList()->AddText(
|
||||||
|
ImVec2(max.x - ImGui::GetStyle().FramePadding.x - size.x, textPos.y),
|
||||||
|
disabledColor,
|
||||||
|
match.entry.shortcut.c_str());
|
||||||
|
}
|
||||||
|
if (selected) selectedMatch = &match;
|
||||||
|
++displayIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalItems == 0) {
|
||||||
|
ImGui::TextDisabled("No matching commands.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedMatch && selectedMatch->entry.inlineInput) {
|
||||||
|
if (selectedMatch->entry.id != state.commandInlineCommandId) {
|
||||||
|
state.commandInlineCommandId = selectedMatch->entry.id;
|
||||||
|
state.commandInlineValue[0] = '\0';
|
||||||
|
}
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::TextDisabled("Input");
|
||||||
|
ImGui::SetNextItemWidth(300.0f);
|
||||||
|
bool inlineSubmit = ImGui::InputTextWithHint("##cmdInlineInput",
|
||||||
|
selectedMatch->entry.inputHint.c_str(),
|
||||||
|
state.commandInlineValue,
|
||||||
|
sizeof(state.commandInlineValue),
|
||||||
|
ImGuiInputTextFlags_EnterReturnsTrue);
|
||||||
|
if (inlineSubmit) {
|
||||||
|
state.executeCommand(selectedMatch->entry.id,
|
||||||
|
std::string(state.commandInlineValue));
|
||||||
|
state.showCommandPalette = false;
|
||||||
|
} else if (accept) {
|
||||||
|
ImGui::SetKeyboardFocusHere(-1);
|
||||||
|
}
|
||||||
|
} else if (accept && totalItems > 0 && state.commandSelected >= 0 &&
|
||||||
|
state.commandSelected < totalItems && selectedMatch) {
|
||||||
|
state.executeCommand(selectedMatch->entry.id);
|
||||||
|
state.showCommandPalette = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
struct FileMatch {
|
||||||
|
std::string path;
|
||||||
|
std::string display;
|
||||||
|
int score = 0;
|
||||||
|
std::vector<int> matchIndices;
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::string cachedRoot;
|
||||||
|
static std::vector<std::string> cachedFiles;
|
||||||
|
if (state.workspaceRoot.empty()) {
|
||||||
|
cachedFiles.clear();
|
||||||
|
cachedRoot.clear();
|
||||||
|
} else if (cachedRoot != state.workspaceRoot || state.fileTreeDirty) {
|
||||||
|
state.refreshFileTree();
|
||||||
|
cachedFiles.clear();
|
||||||
|
if (!state.fileTreeRoot.path.empty()) {
|
||||||
|
collectFilePaths(state.fileTreeRoot, cachedFiles);
|
||||||
|
}
|
||||||
|
cachedRoot = state.workspaceRoot;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<FileMatch> fileMatches;
|
||||||
|
if (!query.empty()) {
|
||||||
|
fileMatches.reserve(cachedFiles.size());
|
||||||
|
for (const auto& path : cachedFiles) {
|
||||||
|
FileMatch match;
|
||||||
|
match.path = path;
|
||||||
|
match.display = makeRelativePath(path, state.workspaceRoot);
|
||||||
|
int score = 0;
|
||||||
|
if (!CommandPalette::fuzzyMatch(match.display, query, score, match.matchIndices)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match.score = score;
|
||||||
|
fileMatches.push_back(std::move(match));
|
||||||
|
}
|
||||||
|
std::sort(fileMatches.begin(), fileMatches.end(),
|
||||||
|
[](const FileMatch& a, const FileMatch& b) {
|
||||||
|
if (a.score != b.score) return a.score > b.score;
|
||||||
|
return a.display < b.display;
|
||||||
|
});
|
||||||
|
if (fileMatches.size() > 60) fileMatches.resize(60);
|
||||||
|
}
|
||||||
|
|
||||||
|
int maxIndex = std::max(0, (int)fileMatches.size() - 1);
|
||||||
|
state.commandSelected = std::min(state.commandSelected, maxIndex);
|
||||||
|
if (ImGui::IsKeyPressed(ImGuiKey_DownArrow)) {
|
||||||
|
state.commandSelected = std::min(state.commandSelected + 1, maxIndex);
|
||||||
|
}
|
||||||
|
if (ImGui::IsKeyPressed(ImGuiKey_UpArrow)) {
|
||||||
|
state.commandSelected = std::max(state.commandSelected - 1, 0);
|
||||||
|
}
|
||||||
|
bool accept = ImGui::IsKeyPressed(ImGuiKey_Enter);
|
||||||
|
|
||||||
|
const ImU32 normalColor = ImGui::GetColorU32(ImGuiCol_Text);
|
||||||
|
const ImU32 disabledColor = ImGui::GetColorU32(ImGuiCol_TextDisabled);
|
||||||
|
const ImU32 highlightColor = ImGui::GetColorU32(ImGuiCol_PlotHistogram);
|
||||||
|
|
||||||
|
if (state.workspaceRoot.empty()) {
|
||||||
|
ImGui::TextDisabled("Open a folder to search files.");
|
||||||
|
} else if (query.empty()) {
|
||||||
|
ImGui::TextDisabled("Start typing to search files.");
|
||||||
|
} else if (fileMatches.empty()) {
|
||||||
|
ImGui::TextDisabled("No matching files.");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < (int)fileMatches.size(); ++i) {
|
||||||
|
const auto& match = fileMatches[i];
|
||||||
|
bool selected = (i == state.commandSelected);
|
||||||
|
std::string rowId = "##file_" + std::to_string(i);
|
||||||
|
if (ImGui::Selectable(rowId.c_str(), selected,
|
||||||
|
ImGuiSelectableFlags_SpanAvailWidth,
|
||||||
|
ImVec2(0.0f, ImGui::GetTextLineHeightWithSpacing()))) {
|
||||||
|
state.commandSelected = i;
|
||||||
|
accept = true;
|
||||||
|
}
|
||||||
|
ImVec2 min = ImGui::GetItemRectMin();
|
||||||
|
ImVec2 max = ImGui::GetItemRectMax();
|
||||||
|
ImVec2 textPos(min.x + ImGui::GetStyle().FramePadding.x,
|
||||||
|
min.y + ImGui::GetStyle().FramePadding.y);
|
||||||
|
drawHighlightedText(ImGui::GetWindowDrawList(),
|
||||||
|
match.display,
|
||||||
|
match.matchIndices,
|
||||||
|
textPos,
|
||||||
|
normalColor,
|
||||||
|
highlightColor);
|
||||||
|
std::string folder = std::filesystem::path(match.path).parent_path().string();
|
||||||
|
if (!folder.empty()) {
|
||||||
|
ImVec2 size = ImGui::CalcTextSize(folder.c_str());
|
||||||
|
ImGui::GetWindowDrawList()->AddText(
|
||||||
|
ImVec2(max.x - ImGui::GetStyle().FramePadding.x - size.x, textPos.y),
|
||||||
|
disabledColor,
|
||||||
|
folder.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accept && !fileMatches.empty()) {
|
||||||
|
state.doOpen(fileMatches[state.commandSelected].path);
|
||||||
|
state.showCommandPalette = false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (accept && !results.empty()) {
|
|
||||||
state.executeCommand(results[state.commandSelected].id);
|
|
||||||
state.showCommandPalette = false;
|
|
||||||
}
|
}
|
||||||
ImGui::End();
|
ImGui::End();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,6 +200,21 @@ static void renderMenuBar(EditorState& state) {
|
|||||||
if (!canRefactor) ImGui::EndDisabled();
|
if (!canRefactor) ImGui::EndDisabled();
|
||||||
ImGui::EndMenu();
|
ImGui::EndMenu();
|
||||||
}
|
}
|
||||||
|
if (ImGui::BeginMenu("Workflow")) {
|
||||||
|
bool hasAst = state.activeAST() != nullptr;
|
||||||
|
if (!hasAst) ImGui::BeginDisabled();
|
||||||
|
if (ImGui::MenuItem("Annotate File Wizard")) {
|
||||||
|
state.ui.showAnnotateWizard = true;
|
||||||
|
}
|
||||||
|
if (ImGui::MenuItem("Cross-Language Project Wizard")) {
|
||||||
|
state.ui.showProjectWizard = true;
|
||||||
|
}
|
||||||
|
if (!hasAst) ImGui::EndDisabled();
|
||||||
|
if (ImGui::MenuItem("Connect Agent Wizard")) {
|
||||||
|
state.ui.showAgentWizard = true;
|
||||||
|
}
|
||||||
|
ImGui::EndMenu();
|
||||||
|
}
|
||||||
if (ImGui::BeginMenu("Keybindings")) {
|
if (ImGui::BeginMenu("Keybindings")) {
|
||||||
for (auto p : KeybindingManager::availableProfiles()) {
|
for (auto p : KeybindingManager::availableProfiles()) {
|
||||||
if (ImGui::MenuItem(KeybindingManager::profileName(p), nullptr,
|
if (ImGui::MenuItem(KeybindingManager::profileName(p), nullptr,
|
||||||
|
|||||||
364
editor/src/panels/WizardPanels.h
Normal file
364
editor/src/panels/WizardPanels.h
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "../EditorState.h"
|
||||||
|
#include "../EditorUtils.h"
|
||||||
|
#include "../MemoryStrategyInference.h"
|
||||||
|
#include "../ASTMutationAPI.h"
|
||||||
|
#include "../CrossLanguageProjector.h"
|
||||||
|
#include "../WizardFramework.h"
|
||||||
|
#include "../AgentPermissionPolicy.h"
|
||||||
|
#include "../ast/Function.h"
|
||||||
|
#include "../ast/Annotation.h"
|
||||||
|
#include <atomic>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct AnnotateFileWizardState {
|
||||||
|
WizardFrame frame;
|
||||||
|
bool prepared = false;
|
||||||
|
int index = 0;
|
||||||
|
int accepted = 0;
|
||||||
|
int skipped = 0;
|
||||||
|
std::string targetPath;
|
||||||
|
std::vector<MemoryStrategyInference::Suggestion> suggestions;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ProjectWizardState {
|
||||||
|
WizardFrame frame;
|
||||||
|
int targetIndex = 0;
|
||||||
|
std::string preview;
|
||||||
|
int sourceAnnoCount = 0;
|
||||||
|
int projectedAnnoCount = 0;
|
||||||
|
bool annotationsPreserved = false;
|
||||||
|
bool previewReady = false;
|
||||||
|
std::string targetLanguage;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ConnectAgentWizardState {
|
||||||
|
WizardFrame frame;
|
||||||
|
int portInput = 8765;
|
||||||
|
int roleIndex = 0;
|
||||||
|
bool tested = false;
|
||||||
|
bool testOk = false;
|
||||||
|
std::string testMessage;
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::string wizardId(const char* prefix) {
|
||||||
|
static std::atomic<int> counter{0};
|
||||||
|
return std::string(prefix) + "_wiz_" + std::to_string(counter.fetch_add(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
static ASTNode* wizardFindNodeById(ASTNode* node, const std::string& nodeId) {
|
||||||
|
if (!node) return nullptr;
|
||||||
|
if (node->id == nodeId) return node;
|
||||||
|
for (auto* child : node->allChildren()) {
|
||||||
|
if (auto* found = wizardFindNodeById(child, nodeId)) return found;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool wizardHasMemoryAnnotation(const ASTNode* node) {
|
||||||
|
for (auto* anno : node->getChildren("annotations")) {
|
||||||
|
const auto& ct = anno->conceptType;
|
||||||
|
if (ct == "DeallocateAnnotation" || ct == "LifetimeAnnotation" ||
|
||||||
|
ct == "ReclaimAnnotation" || ct == "OwnerAnnotation" ||
|
||||||
|
ct == "AllocateAnnotation") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ASTNode* wizardCreateAnnotation(const MemoryStrategyInference::Suggestion& s) {
|
||||||
|
if (s.annotationType == "DeallocateAnnotation") {
|
||||||
|
return new DeallocateAnnotation(wizardId("anno"), s.strategy);
|
||||||
|
}
|
||||||
|
if (s.annotationType == "LifetimeAnnotation") {
|
||||||
|
return new LifetimeAnnotation(wizardId("anno"), s.strategy);
|
||||||
|
}
|
||||||
|
if (s.annotationType == "ReclaimAnnotation") {
|
||||||
|
return new ReclaimAnnotation(wizardId("anno"), s.strategy);
|
||||||
|
}
|
||||||
|
if (s.annotationType == "OwnerAnnotation") {
|
||||||
|
return new OwnerAnnotation(wizardId("anno"), s.strategy);
|
||||||
|
}
|
||||||
|
if (s.annotationType == "AllocateAnnotation") {
|
||||||
|
return new AllocateAnnotation(wizardId("anno"), s.strategy);
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void prepareAnnotateWizard(EditorState& state, AnnotateFileWizardState& wizard) {
|
||||||
|
wizard.suggestions.clear();
|
||||||
|
wizard.index = 0;
|
||||||
|
wizard.accepted = 0;
|
||||||
|
wizard.skipped = 0;
|
||||||
|
wizard.prepared = true;
|
||||||
|
wizard.targetPath = state.active() ? state.active()->path : "";
|
||||||
|
|
||||||
|
if (!state.isStructured()) return;
|
||||||
|
Module* ast = state.activeAST();
|
||||||
|
if (!ast) return;
|
||||||
|
|
||||||
|
MemoryStrategyInference infer;
|
||||||
|
auto suggestions = infer.inferAnnotations(ast);
|
||||||
|
for (const auto& s : suggestions) {
|
||||||
|
ASTNode* node = wizardFindNodeById(ast, s.nodeId);
|
||||||
|
if (!node || node->conceptType != "Function") continue;
|
||||||
|
if (wizardHasMemoryAnnotation(node)) continue;
|
||||||
|
wizard.suggestions.push_back(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void renderAnnotateFileWizard(EditorState& state, AnnotateFileWizardState& wizard) {
|
||||||
|
if (!wizard.frame.open) return;
|
||||||
|
wizard.frame.stepCount = 3;
|
||||||
|
if (!beginWizardWindow("Annotate File Wizard", wizard.frame)) return;
|
||||||
|
|
||||||
|
if (!wizard.prepared ||
|
||||||
|
(state.active() && wizard.targetPath != state.active()->path)) {
|
||||||
|
prepareAnnotateWizard(state, wizard);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wizard.frame.step == 0) {
|
||||||
|
ImGui::TextUnformatted("Review suggested memory annotations.");
|
||||||
|
ImGui::Separator();
|
||||||
|
if (!state.isStructured() || !state.activeAST()) {
|
||||||
|
ImGui::TextDisabled("Open a structured buffer to continue.");
|
||||||
|
} else {
|
||||||
|
ImGui::Text("Found %d function(s) with suggestions.",
|
||||||
|
(int)wizard.suggestions.size());
|
||||||
|
ImGui::TextDisabled("You can accept or skip each suggestion.");
|
||||||
|
}
|
||||||
|
} else if (wizard.frame.step == 1) {
|
||||||
|
if (!state.isStructured() || !state.activeAST()) {
|
||||||
|
ImGui::TextDisabled("Open a structured buffer to continue.");
|
||||||
|
} else if (wizard.suggestions.empty()) {
|
||||||
|
ImGui::TextDisabled("No suggestions to review.");
|
||||||
|
} else {
|
||||||
|
if (wizard.index >= (int)wizard.suggestions.size()) {
|
||||||
|
wizard.frame.step = 2;
|
||||||
|
} else {
|
||||||
|
const auto& s = wizard.suggestions[wizard.index];
|
||||||
|
ASTNode* node = wizardFindNodeById(state.activeAST(), s.nodeId);
|
||||||
|
std::string fnName = "(unknown)";
|
||||||
|
if (node && node->conceptType == "Function") {
|
||||||
|
fnName = static_cast<Function*>(node)->name;
|
||||||
|
}
|
||||||
|
ImGui::Text("Function: %s", fnName.c_str());
|
||||||
|
ImGui::Text("Suggestion: %s (%s)",
|
||||||
|
s.annotationType.c_str(),
|
||||||
|
s.strategy.c_str());
|
||||||
|
ImGui::TextWrapped("Reason: %s", s.reason.c_str());
|
||||||
|
ImGui::Text("Confidence: %.0f%%", s.confidence * 100.0);
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
if (ImGui::Button("Accept")) {
|
||||||
|
MemoryStrategyInference infer;
|
||||||
|
ASTNode* ast = state.mutationAST();
|
||||||
|
ASTMutationAPI mut;
|
||||||
|
if (ast) mut.setRoot(ast);
|
||||||
|
ASTNode* anno = wizardCreateAnnotation(s);
|
||||||
|
if (!ast || !anno) {
|
||||||
|
state.notify(NotificationLevel::Error,
|
||||||
|
"Failed to apply annotation.");
|
||||||
|
} else {
|
||||||
|
auto res = mut.insertNode(s.nodeId, "annotations", anno);
|
||||||
|
if (!res.success) {
|
||||||
|
delete anno;
|
||||||
|
state.notify(NotificationLevel::Error,
|
||||||
|
"Annotation insert failed: " + res.error);
|
||||||
|
} else {
|
||||||
|
if (!res.warning.empty()) {
|
||||||
|
state.notify(NotificationLevel::Warning, res.warning);
|
||||||
|
}
|
||||||
|
infer.recordFeedback(s, true);
|
||||||
|
wizard.accepted += 1;
|
||||||
|
state.applyOrchestratorToActive();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wizard.index += 1;
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Skip")) {
|
||||||
|
MemoryStrategyInference infer;
|
||||||
|
infer.recordFeedback(s, false);
|
||||||
|
wizard.skipped += 1;
|
||||||
|
wizard.index += 1;
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Skip All")) {
|
||||||
|
wizard.skipped += (int)wizard.suggestions.size() - wizard.index;
|
||||||
|
wizard.index = (int)wizard.suggestions.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (wizard.frame.step == 2) {
|
||||||
|
ImGui::TextUnformatted("Summary");
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::Text("Accepted: %d", wizard.accepted);
|
||||||
|
ImGui::Text("Skipped: %d", wizard.skipped);
|
||||||
|
}
|
||||||
|
|
||||||
|
WizardNavAction nav = renderWizardFooter(wizard.frame, "Done");
|
||||||
|
if (nav == WizardNavAction::Finish || nav == WizardNavAction::Cancel) {
|
||||||
|
wizard.prepared = false;
|
||||||
|
}
|
||||||
|
ImGui::End();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void buildProjectionPreview(EditorState& state,
|
||||||
|
ProjectWizardState& wizard,
|
||||||
|
const std::string& targetLanguage) {
|
||||||
|
wizard.preview.clear();
|
||||||
|
wizard.previewReady = false;
|
||||||
|
wizard.sourceAnnoCount = 0;
|
||||||
|
wizard.projectedAnnoCount = 0;
|
||||||
|
wizard.annotationsPreserved = false;
|
||||||
|
wizard.targetLanguage = targetLanguage;
|
||||||
|
|
||||||
|
if (!state.isStructured()) return;
|
||||||
|
Module* ast = state.activeAST();
|
||||||
|
if (!ast) return;
|
||||||
|
|
||||||
|
CrossLanguageProjector projector;
|
||||||
|
auto projected = projector.project(ast, targetLanguage);
|
||||||
|
if (!projected) {
|
||||||
|
wizard.preview = "Projection failed.";
|
||||||
|
wizard.previewReady = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wizard.sourceAnnoCount = countAnnotationNodes(ast);
|
||||||
|
wizard.projectedAnnoCount = countAnnotationNodes(projected.get());
|
||||||
|
wizard.annotationsPreserved = projector.annotationsPreserved(ast, projected.get());
|
||||||
|
|
||||||
|
std::string text = generateForLanguage(projected.get(), targetLanguage);
|
||||||
|
int lines = 0;
|
||||||
|
std::string snippet;
|
||||||
|
snippet.reserve(text.size());
|
||||||
|
for (char c : text) {
|
||||||
|
snippet.push_back(c);
|
||||||
|
if (c == '\n') {
|
||||||
|
lines += 1;
|
||||||
|
if (lines >= 18) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wizard.preview = snippet;
|
||||||
|
wizard.previewReady = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void renderProjectWizard(EditorState& state, ProjectWizardState& wizard) {
|
||||||
|
if (!wizard.frame.open) return;
|
||||||
|
wizard.frame.stepCount = 3;
|
||||||
|
if (!beginWizardWindow("Cross-Language Project Wizard", wizard.frame)) return;
|
||||||
|
|
||||||
|
const char* languages[] = {"python", "cpp", "elisp", "javascript",
|
||||||
|
"typescript", "java", "rust", "go"};
|
||||||
|
int languageCount = IM_ARRAYSIZE(languages);
|
||||||
|
std::string activeLang = state.active() ? state.active()->language : "python";
|
||||||
|
|
||||||
|
if (wizard.frame.step == 0) {
|
||||||
|
ImGui::Text("Source language: %s", activeLang.c_str());
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::TextUnformatted("Choose target language");
|
||||||
|
for (int i = 0; i < languageCount; ++i) {
|
||||||
|
if (ImGui::RadioButton(languages[i], wizard.targetIndex == i)) {
|
||||||
|
wizard.targetIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!state.isStructured() || !state.activeAST()) {
|
||||||
|
ImGui::TextDisabled("Structured AST required for projection.");
|
||||||
|
}
|
||||||
|
} else if (wizard.frame.step == 1) {
|
||||||
|
if (!wizard.previewReady) {
|
||||||
|
buildProjectionPreview(state, wizard, languages[wizard.targetIndex]);
|
||||||
|
}
|
||||||
|
ImGui::Text("Target: %s", languages[wizard.targetIndex]);
|
||||||
|
ImGui::Text("Annotations: %d -> %d (%s)",
|
||||||
|
wizard.sourceAnnoCount,
|
||||||
|
wizard.projectedAnnoCount,
|
||||||
|
wizard.annotationsPreserved ? "preserved" : "lossy");
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::TextUnformatted("Preview");
|
||||||
|
ImGui::BeginChild("##projectPreview", ImVec2(0, 200), true);
|
||||||
|
ImGui::TextUnformatted(wizard.preview.c_str());
|
||||||
|
ImGui::EndChild();
|
||||||
|
} else if (wizard.frame.step == 2) {
|
||||||
|
ImGui::TextUnformatted("Confirm projection");
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::TextWrapped("This will open a new tab with the projected code.");
|
||||||
|
ImGui::Text("Target language: %s", languages[wizard.targetIndex]);
|
||||||
|
}
|
||||||
|
|
||||||
|
WizardNavAction nav = renderWizardFooter(wizard.frame, "Project");
|
||||||
|
if (nav == WizardNavAction::Next && wizard.frame.step == 1) {
|
||||||
|
wizard.previewReady = false;
|
||||||
|
}
|
||||||
|
if (nav == WizardNavAction::Finish) {
|
||||||
|
if (state.isStructured() && state.activeAST()) {
|
||||||
|
state.projectToLanguage(languages[wizard.targetIndex]);
|
||||||
|
} else {
|
||||||
|
state.notify(NotificationLevel::Warning,
|
||||||
|
"Projection requires a structured buffer.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nav == WizardNavAction::Cancel) {
|
||||||
|
wizard.previewReady = false;
|
||||||
|
}
|
||||||
|
ImGui::End();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void renderConnectAgentWizard(EditorState& state, ConnectAgentWizardState& wizard) {
|
||||||
|
if (!wizard.frame.open) return;
|
||||||
|
wizard.frame.stepCount = 3;
|
||||||
|
if (!beginWizardWindow("Connect Agent Wizard", wizard.frame)) return;
|
||||||
|
|
||||||
|
if (wizard.frame.step == 0) {
|
||||||
|
if (wizard.portInput <= 0) wizard.portInput = state.agent.port;
|
||||||
|
ImGui::TextUnformatted("Agent server endpoint");
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::InputInt("Port", &wizard.portInput);
|
||||||
|
wizard.portInput = std::max(1, std::min(wizard.portInput, 65535));
|
||||||
|
ImGui::Text("ws://localhost:%d", wizard.portInput);
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::TextUnformatted("Default permissions");
|
||||||
|
const char* roleLabels[] = {"Linter", "Refactor", "Generator"};
|
||||||
|
if (wizard.roleIndex < 0 || wizard.roleIndex > 2) {
|
||||||
|
wizard.roleIndex = (int)state.agent.defaultRole;
|
||||||
|
}
|
||||||
|
if (ImGui::Combo("Role", &wizard.roleIndex, roleLabels, 3)) {
|
||||||
|
state.agent.defaultRole = static_cast<AgentRole>(wizard.roleIndex);
|
||||||
|
}
|
||||||
|
if (ImGui::Button("Apply & Restart Server")) {
|
||||||
|
state.shutdownAgentServer();
|
||||||
|
state.agent.port = wizard.portInput;
|
||||||
|
state.initAgentServer();
|
||||||
|
wizard.tested = false;
|
||||||
|
}
|
||||||
|
} else if (wizard.frame.step == 1) {
|
||||||
|
ImGui::TextUnformatted("Test connection");
|
||||||
|
ImGui::Separator();
|
||||||
|
if (ImGui::Button("Test Now")) {
|
||||||
|
bool running = state.agent.server && state.agent.server->isRunning();
|
||||||
|
wizard.tested = true;
|
||||||
|
wizard.testOk = running;
|
||||||
|
wizard.testMessage = running
|
||||||
|
? "Server is running and accepting connections."
|
||||||
|
: "Server not running. Try restarting.";
|
||||||
|
}
|
||||||
|
if (wizard.tested) {
|
||||||
|
ImGui::TextColored(wizard.testOk ? ImVec4(0.2f, 0.8f, 0.3f, 1.0f)
|
||||||
|
: ImVec4(0.9f, 0.4f, 0.4f, 1.0f),
|
||||||
|
"%s", wizard.testMessage.c_str());
|
||||||
|
}
|
||||||
|
} else if (wizard.frame.step == 2) {
|
||||||
|
ImGui::TextUnformatted("API capabilities");
|
||||||
|
ImGui::Separator();
|
||||||
|
ImGui::BulletText("getAST, findNodesByType, findNodesByAnnotation");
|
||||||
|
ImGui::BulletText("applyMutation, applyAnnotationSuggestion");
|
||||||
|
ImGui::BulletText("generateCode, getAnnotationSuggestions");
|
||||||
|
ImGui::BulletText("setAgentRole, workflow recording APIs");
|
||||||
|
}
|
||||||
|
|
||||||
|
renderWizardFooter(wizard.frame, "Done");
|
||||||
|
ImGui::End();
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ struct AgentState {
|
|||||||
std::unique_ptr<WebSocketAgentServer> server;
|
std::unique_ptr<WebSocketAgentServer> server;
|
||||||
MockWebSocketTransport* transport = nullptr;
|
MockWebSocketTransport* transport = nullptr;
|
||||||
int port = 8765;
|
int port = 8765;
|
||||||
|
AgentRole defaultRole = AgentRole::Linter;
|
||||||
std::vector<std::string> log;
|
std::vector<std::string> log;
|
||||||
std::map<std::string, AgentRole> roles;
|
std::map<std::string, AgentRole> roles;
|
||||||
WorkflowRecorder workflowRecorder;
|
WorkflowRecorder workflowRecorder;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "CompositionPanel.h"
|
#include "CompositionPanel.h"
|
||||||
#include "LibraryIndexer.h"
|
#include "LibraryIndexer.h"
|
||||||
#include "PrimitivesRegistry.h"
|
#include "PrimitivesRegistry.h"
|
||||||
|
#include "VulnerabilityDatabase.h"
|
||||||
|
|
||||||
struct LibraryState {
|
struct LibraryState {
|
||||||
bool showDependencyPanel = true;
|
bool showDependencyPanel = true;
|
||||||
@@ -24,4 +25,5 @@ struct LibraryState {
|
|||||||
std::vector<LibraryIndexRequest> libraryIndexRequests;
|
std::vector<LibraryIndexRequest> libraryIndexRequests;
|
||||||
LibraryIndexData libraryIndex;
|
LibraryIndexData libraryIndex;
|
||||||
PrimitivesRegistry primitives;
|
PrimitivesRegistry primitives;
|
||||||
|
VulnerabilityDatabase vulnDb;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ struct UIFlags {
|
|||||||
bool showFirstRunWizard = false;
|
bool showFirstRunWizard = false;
|
||||||
bool showShortcutReference = false;
|
bool showShortcutReference = false;
|
||||||
bool showHelpPanel = false;
|
bool showHelpPanel = false;
|
||||||
|
bool showAnnotateWizard = false;
|
||||||
|
bool showProjectWizard = false;
|
||||||
|
bool showAgentWizard = false;
|
||||||
int bottomTab = 0; // 0=Output,1=AST,2=Highlighted
|
int bottomTab = 0; // 0=Output,1=AST,2=Highlighted
|
||||||
LayoutPreset layoutPreset = LayoutPreset::VSCode;
|
LayoutPreset layoutPreset = LayoutPreset::VSCode;
|
||||||
};
|
};
|
||||||
|
|||||||
34
editor/tests/step188_test.cpp
Normal file
34
editor/tests/step188_test.cpp
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// Step 188: Command palette enhancements.
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
#include <fstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
static std::string readFile(const std::string& path) {
|
||||||
|
std::ifstream f(path);
|
||||||
|
if (!f.is_open()) return {};
|
||||||
|
return std::string((std::istreambuf_iterator<char>(f)),
|
||||||
|
std::istreambuf_iterator<char>());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void assertContains(const std::string& text, const std::string& needle) {
|
||||||
|
assert(text.find(needle) != std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
const std::string palette = readFile("src/CommandPalette.h");
|
||||||
|
const std::string dialogs = readFile("src/panels/DialogPanels.h");
|
||||||
|
|
||||||
|
assertContains(palette, "CommandContext");
|
||||||
|
assertContains(palette, "CommandMatch");
|
||||||
|
assertContains(palette, "recent(");
|
||||||
|
assertContains(palette, "inlineInput");
|
||||||
|
assertContains(palette, "fuzzyMatch");
|
||||||
|
|
||||||
|
assertContains(dialogs, "InputTextWithHint");
|
||||||
|
assertContains(dialogs, "prefix '>'");
|
||||||
|
assertContains(dialogs, "commandMode");
|
||||||
|
|
||||||
|
printf("step188_test: all assertions passed\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
30
editor/tests/step189_test.cpp
Normal file
30
editor/tests/step189_test.cpp
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// Step 189: Guided workflow wizard scaffolding.
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
#include <fstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
static std::string readFile(const std::string& path) {
|
||||||
|
std::ifstream f(path);
|
||||||
|
if (!f.is_open()) return {};
|
||||||
|
return std::string((std::istreambuf_iterator<char>(f)),
|
||||||
|
std::istreambuf_iterator<char>());
|
||||||
|
}
|
||||||
|
|
||||||
|
static void assertContains(const std::string& text, const std::string& needle) {
|
||||||
|
assert(text.find(needle) != std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
const std::string framework = readFile("src/WizardFramework.h");
|
||||||
|
const std::string panels = readFile("src/panels/WizardPanels.h");
|
||||||
|
|
||||||
|
assertContains(framework, "WizardFrame");
|
||||||
|
assertContains(framework, "renderWizardFooter");
|
||||||
|
assertContains(panels, "Annotate File Wizard");
|
||||||
|
assertContains(panels, "Cross-Language Project Wizard");
|
||||||
|
assertContains(panels, "Connect Agent Wizard");
|
||||||
|
|
||||||
|
printf("step189_test: all assertions passed\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
53
editor/tests/step190_test.cpp
Normal file
53
editor/tests/step190_test.cpp
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// Step 190: Vulnerability database parsing/querying.
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
#include <fstream>
|
||||||
|
#include <filesystem>
|
||||||
|
#include "VulnerabilityDatabase.h"
|
||||||
|
|
||||||
|
static void writeFile(const std::string& path, const std::string& contents) {
|
||||||
|
std::ofstream out(path);
|
||||||
|
out << contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
std::filesystem::path cacheDir = "tmp_vuln_cache";
|
||||||
|
std::filesystem::create_directories(cacheDir);
|
||||||
|
|
||||||
|
const std::string json = R"({
|
||||||
|
"vulnerabilities": [{
|
||||||
|
"id": "CVE-2024-0001",
|
||||||
|
"summary": "Test vuln",
|
||||||
|
"severity": [{"type": "CVSS_V3", "score": "7.5"}],
|
||||||
|
"aliases": ["CVE-2024-0001"],
|
||||||
|
"affected": [{
|
||||||
|
"package": {"ecosystem": "PyPI", "name": "requests"},
|
||||||
|
"ranges": [{
|
||||||
|
"type": "SEMVER",
|
||||||
|
"events": [
|
||||||
|
{"introduced": "1.0.0"},
|
||||||
|
{"fixed": "2.0.0"}
|
||||||
|
]
|
||||||
|
}],
|
||||||
|
"versions": ["1.5.0"]
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
})";
|
||||||
|
|
||||||
|
const std::string fileName = "PyPI_requests.json";
|
||||||
|
writeFile((cacheDir / fileName).string(), json);
|
||||||
|
|
||||||
|
VulnerabilityDatabase db;
|
||||||
|
db.setCacheDirectory(cacheDir.string());
|
||||||
|
|
||||||
|
auto hit = db.query("PyPI", "requests", "1.5.0");
|
||||||
|
assert(hit.size() == 1);
|
||||||
|
assert(hit[0].cveId == "CVE-2024-0001");
|
||||||
|
assert(hit[0].severity == "High");
|
||||||
|
|
||||||
|
auto miss = db.query("PyPI", "requests", "2.1.0");
|
||||||
|
assert(miss.empty());
|
||||||
|
|
||||||
|
printf("step190_test: all assertions passed\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -284,7 +284,7 @@ without being annoying.
|
|||||||
- "Open in Browser" button for full online docs
|
- "Open in Browser" button for full online docs
|
||||||
*New:* `HelpPanel.h`, `MarkdownRenderer.h`
|
*New:* `HelpPanel.h`, `MarkdownRenderer.h`
|
||||||
|
|
||||||
- [ ] **Step 188: Enhanced command palette**
|
- [x] **Step 188: Enhanced command palette**
|
||||||
Upgrade the existing command palette:
|
Upgrade the existing command palette:
|
||||||
- Commands grouped by category with section headers
|
- Commands grouped by category with section headers
|
||||||
- "Recently Used" section pinned at top (last 5 commands)
|
- "Recently Used" section pinned at top (last 5 commands)
|
||||||
@@ -294,7 +294,7 @@ without being annoying.
|
|||||||
- Fuzzy matching highlights matched characters
|
- Fuzzy matching highlights matched characters
|
||||||
*Modifies:* `CommandPalette.h`
|
*Modifies:* `CommandPalette.h`
|
||||||
|
|
||||||
- [ ] **Step 189: Guided workflow wizards**
|
- [x] **Step 189: Guided workflow wizards**
|
||||||
Multi-step wizards for complex operations:
|
Multi-step wizards for complex operations:
|
||||||
- **"Annotate File"** wizard: iterates through unannotated functions,
|
- **"Annotate File"** wizard: iterates through unannotated functions,
|
||||||
shows inference suggestions, user confirms/skips each. Summary at end.
|
shows inference suggestions, user confirms/skips each. Summary at end.
|
||||||
@@ -312,7 +312,7 @@ without being annoying.
|
|||||||
Integrate the security vulnerability awareness and semantic annotation
|
Integrate the security vulnerability awareness and semantic annotation
|
||||||
feature requests with polished UX.
|
feature requests with polished UX.
|
||||||
|
|
||||||
- [ ] **Step 190: Vulnerability knowledge base**
|
- [x] **Step 190: Vulnerability knowledge base**
|
||||||
`VulnerabilityDatabase.h`:
|
`VulnerabilityDatabase.h`:
|
||||||
- Data model: `VulnerabilityRecord` (ecosystem, package, affected_versions,
|
- Data model: `VulnerabilityRecord` (ecosystem, package, affected_versions,
|
||||||
severity, CVE ID, summary, references, fix_versions)
|
severity, CVE ID, summary, references, fix_versions)
|
||||||
|
|||||||
Reference in New Issue
Block a user