Step 133: import statement generation

This commit is contained in:
Bill
2026-02-09 17:10:03 -07:00
parent dcc963f625
commit 2c8202a413
8 changed files with 512 additions and 2 deletions

View File

@@ -382,7 +382,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi
## What's Next
Sprint 5 in progress. Step 132 (library symbol browser) done. Next: Step 133 (import statement generation).
Sprint 5 in progress. Step 133 (import statement generation) done. Next: Step 134 (available primitives registry).
---
@@ -478,3 +478,4 @@ Sprint 5 in progress. Step 132 (library symbol browser) done. Next: Step 133 (im
| 2026-02-09 | Codex | Step 130: LSP workspace symbol + completion indexing for dependencies with ExternalModule population and library index polling. 7/7 tests pass. |
| 2026-02-09 | Codex | Step 131: Stub-based library indexing (pyi/d.ts/headers/lib.rs) with workspace scan fallback. 8/8 tests pass. |
| 2026-02-09 | Codex | Step 132: Library symbol browser panel with filtering, doc detail display, and insert template helpers. 5/5 tests pass. |
| 2026-02-09 | Codex | Step 133: Import statement generation + unused import warnings across Python/JS/Rust/Go/Elisp with auto-insert on library symbol use. 7/7 tests pass. |

View File

@@ -746,6 +746,10 @@ target_link_libraries(step132_test PRIVATE
imgui::imgui
nlohmann_json::nlohmann_json)
add_executable(step133_test tests/step133_test.cpp)
target_include_directories(step133_test PRIVATE src)
target_link_libraries(step133_test PRIVATE nlohmann_json::nlohmann_json)
find_package(SDL2 CONFIG REQUIRED)
find_package(OpenGL REQUIRED)
find_package(glad CONFIG REQUIRED)

View File

@@ -46,6 +46,7 @@
#include "DependencyPanel.h"
#include "LibraryIndexer.h"
#include "LibraryBrowserPanel.h"
#include "ImportManager.h"
#include "IncrementalOptimizer.h"
#include "ast/Serialization.h"
#include "ast/Generator.h"
@@ -367,6 +368,37 @@ struct EditorState {
}
}
void ensureImportForSymbol(const std::string& library, const std::string& symbol) {
if (!active() || library.empty()) return;
std::string clean = symbol;
auto paren = clean.find('(');
if (paren != std::string::npos) clean = clean.substr(0, paren);
ImportEditResult result = ensureImport(active()->editBuf,
active()->language,
library,
clean);
if (result.changed) {
active()->editBuf = result.text;
onTextChanged();
}
}
void appendUnusedImportDiagnostics(std::vector<EditorDiagnostic>& diags) {
if (!active()) return;
auto issues = findUnusedImports(active()->editBuf, active()->language);
std::string uri = toFileUri(active()->path);
for (const auto& issue : issues) {
EditorDiagnostic ed;
ed.uri = uri;
ed.line = issue.line;
ed.character = 0;
ed.severity = 2;
ed.message = "[Imports] " + issue.message;
ed.source = "ImportManager";
diags.push_back(std::move(ed));
}
}
std::string makeUntitledName() const {
if (!buffers.hasBuffer("(untitled)")) return "(untitled)";
int i = 1;

423
editor/src/ImportManager.h Normal file
View File

@@ -0,0 +1,423 @@
#pragma once
#include <string>
#include <vector>
#include <sstream>
#include <algorithm>
#include <cctype>
struct ImportIssue {
int line = 0;
std::string symbol;
std::string message;
};
struct ImportEditResult {
std::string text;
bool changed = false;
};
static inline std::vector<std::string> splitLines(const std::string& text) {
std::vector<std::string> lines;
std::stringstream ss(text);
std::string line;
while (std::getline(ss, line)) {
lines.push_back(line);
}
if (!text.empty() && text.back() == '\n') lines.push_back("");
return lines;
}
static inline std::string joinLines(const std::vector<std::string>& lines) {
std::string out;
for (size_t i = 0; i < lines.size(); ++i) {
out += lines[i];
if (i + 1 < lines.size()) out += "\n";
}
return out;
}
static inline std::string trimStr(const std::string& s) {
size_t start = s.find_first_not_of(" \t\r\n");
size_t end = s.find_last_not_of(" \t\r\n");
if (start == std::string::npos || end == std::string::npos) return "";
return s.substr(start, end - start + 1);
}
static inline bool startsWith(const std::string& s, const std::string& prefix) {
return s.rfind(prefix, 0) == 0;
}
static inline bool isIdentChar(char c) {
return std::isalnum((unsigned char)c) || c == '_' || c == '-';
}
static inline bool containsWord(const std::string& text, const std::string& word) {
if (word.empty()) return false;
size_t pos = 0;
while ((pos = text.find(word, pos)) != std::string::npos) {
bool leftOk = (pos == 0) || !isIdentChar(text[pos - 1]);
bool rightOk = (pos + word.size() >= text.size()) ||
!isIdentChar(text[pos + word.size()]);
if (leftOk && rightOk) return true;
pos += word.size();
}
return false;
}
static inline std::string lastPathSegment(const std::string& path) {
auto pos = path.find_last_of("/.");
if (pos == std::string::npos) return path;
return path.substr(pos + 1);
}
static inline ImportEditResult ensureImportPython(const std::string& text,
const std::string& library,
const std::string& symbol) {
ImportEditResult out{ text, false };
std::string line;
if (startsWith(symbol, library + ".")) {
line = "import " + library;
} else {
line = "from " + library + " import " + symbol;
}
auto lines = splitLines(text);
std::vector<std::string> imports;
std::vector<std::string> rest;
for (const auto& l : lines) {
std::string t = trimStr(l);
if (startsWith(t, "import ") || startsWith(t, "from ")) {
if (t != line) imports.push_back(l);
} else {
rest.push_back(l);
}
}
imports.push_back(line);
std::sort(imports.begin(), imports.end());
out.text = joinLines(imports) + "\n" + joinLines(rest);
out.changed = true;
return out;
}
static inline ImportEditResult ensureImportJs(const std::string& text,
const std::string& library,
const std::string& symbol) {
ImportEditResult out{ text, false };
std::string name = symbol;
auto dot = name.find_last_of('.');
if (dot != std::string::npos) name = name.substr(dot + 1);
std::string line = "import { " + name + " } from '" + library + "';";
auto lines = splitLines(text);
std::vector<std::string> imports;
std::vector<std::string> rest;
for (const auto& l : lines) {
std::string t = trimStr(l);
if (startsWith(t, "import ")) {
if (t != line) imports.push_back(l);
} else {
rest.push_back(l);
}
}
imports.push_back(line);
std::sort(imports.begin(), imports.end());
out.text = joinLines(imports) + "\n" + joinLines(rest);
out.changed = true;
return out;
}
static inline ImportEditResult ensureImportRust(const std::string& text,
const std::string& library,
const std::string& symbol) {
ImportEditResult out{ text, false };
std::string line;
if (symbol.find("::") != std::string::npos) line = "use " + symbol + ";";
else line = "use " + library + "::" + symbol + ";";
auto lines = splitLines(text);
std::vector<std::string> imports;
std::vector<std::string> rest;
for (const auto& l : lines) {
std::string t = trimStr(l);
if (startsWith(t, "use ")) {
if (t != line) imports.push_back(l);
} else {
rest.push_back(l);
}
}
imports.push_back(line);
std::sort(imports.begin(), imports.end());
out.text = joinLines(imports) + "\n" + joinLines(rest);
out.changed = true;
return out;
}
static inline ImportEditResult ensureImportGo(const std::string& text,
const std::string& library) {
ImportEditResult out{ text, false };
std::string path = "\"" + library + "\"";
std::string line = "import " + path;
auto lines = splitLines(text);
int pkgLine = -1;
int importStart = -1;
int importEnd = -1;
for (int i = 0; i < (int)lines.size(); ++i) {
std::string t = trimStr(lines[i]);
if (startsWith(t, "package ")) pkgLine = i;
if (startsWith(t, "import (")) { importStart = i; }
if (importStart >= 0 && t == ")") { importEnd = i; break; }
if (importStart < 0 && startsWith(t, "import \"")) {
importStart = i;
importEnd = i;
break;
}
}
if (importStart >= 0 && importEnd >= importStart && importStart != importEnd) {
std::vector<std::string> imports;
for (int i = importStart + 1; i < importEnd; ++i) {
std::string t = trimStr(lines[i]);
if (!t.empty()) imports.push_back(t);
}
if (std::find(imports.begin(), imports.end(), path) == imports.end()) {
imports.push_back(path);
}
std::sort(imports.begin(), imports.end());
std::vector<std::string> outLines;
for (int i = 0; i <= importStart; ++i) outLines.push_back(lines[i]);
for (const auto& imp : imports) outLines.push_back(" " + imp);
for (int i = importEnd; i < (int)lines.size(); ++i) outLines.push_back(lines[i]);
out.text = joinLines(outLines);
out.changed = true;
return out;
}
if (importStart >= 0 && importStart == importEnd) {
std::string existing = trimStr(lines[importStart]);
std::vector<std::string> imports = { existing };
if (existing != line) imports.push_back(line);
std::sort(imports.begin(), imports.end());
std::vector<std::string> outLines;
for (int i = 0; i < importStart; ++i) outLines.push_back(lines[i]);
outLines.push_back("import (");
for (const auto& imp : imports) {
std::string t = trimStr(imp);
if (startsWith(t, "import ")) t = trimStr(t.substr(6));
outLines.push_back(" " + t);
}
outLines.push_back(")");
for (int i = importStart + 1; i < (int)lines.size(); ++i) outLines.push_back(lines[i]);
out.text = joinLines(outLines);
out.changed = true;
return out;
}
int insertAt = (pkgLine >= 0) ? pkgLine + 1 : 0;
std::vector<std::string> outLines;
for (int i = 0; i < insertAt; ++i) outLines.push_back(lines[i]);
outLines.push_back("import (");
outLines.push_back(" " + path);
outLines.push_back(")");
for (int i = insertAt; i < (int)lines.size(); ++i) outLines.push_back(lines[i]);
out.text = joinLines(outLines);
out.changed = true;
return out;
}
static inline ImportEditResult ensureImportElisp(const std::string& text,
const std::string& library) {
ImportEditResult out{ text, false };
std::string line = "(require '" + library + ")";
auto lines = splitLines(text);
for (const auto& l : lines) {
if (trimStr(l) == line) return out;
}
std::vector<std::string> outLines;
outLines.push_back(line);
outLines.insert(outLines.end(), lines.begin(), lines.end());
out.text = joinLines(outLines);
out.changed = true;
return out;
}
static inline ImportEditResult ensureImportCpp(const std::string& text,
const std::string& library) {
ImportEditResult out{ text, false };
std::string line = "#include <" + library + ">";
auto lines = splitLines(text);
std::vector<std::string> imports;
std::vector<std::string> rest;
for (const auto& l : lines) {
std::string t = trimStr(l);
if (startsWith(t, "#include")) {
if (t != line) imports.push_back(l);
} else {
rest.push_back(l);
}
}
imports.push_back(line);
std::sort(imports.begin(), imports.end());
out.text = joinLines(imports) + "\n" + joinLines(rest);
out.changed = true;
return out;
}
static inline ImportEditResult ensureImport(const std::string& text,
const std::string& language,
const std::string& library,
const std::string& symbol) {
if (library.empty()) return {text, false};
ImportEditResult res;
if (language == "python") res = ensureImportPython(text, library, symbol);
else if (language == "javascript" || language == "typescript")
res = ensureImportJs(text, library, symbol);
else if (language == "rust") res = ensureImportRust(text, library, symbol);
else if (language == "go") res = ensureImportGo(text, library);
else if (language == "elisp") res = ensureImportElisp(text, library);
else res = ensureImportCpp(text, library);
if (res.text == text) res.changed = false;
return res;
}
static inline std::vector<ImportIssue> findUnusedImportsPython(const std::string& text) {
std::vector<ImportIssue> issues;
auto lines = splitLines(text);
std::string body;
for (const auto& l : lines) {
std::string t = trimStr(l);
if (startsWith(t, "import ") || startsWith(t, "from ")) continue;
body += l;
body += "\n";
}
for (int i = 0; i < (int)lines.size(); ++i) {
std::string t = trimStr(lines[i]);
std::string symbol;
if (startsWith(t, "import ")) {
symbol = trimStr(t.substr(7));
} else if (startsWith(t, "from ")) {
auto pos = t.find("import");
if (pos != std::string::npos) symbol = trimStr(t.substr(pos + 6));
}
if (!symbol.empty() && symbol != "*") {
if (!containsWord(body, symbol)) {
issues.push_back({i, symbol, "Unused import: " + symbol});
}
}
}
return issues;
}
static inline std::vector<ImportIssue> findUnusedImportsJs(const std::string& text) {
std::vector<ImportIssue> issues;
auto lines = splitLines(text);
std::string body;
for (const auto& l : lines) {
std::string t = trimStr(l);
if (startsWith(t, "import ")) continue;
body += l;
body += "\n";
}
for (int i = 0; i < (int)lines.size(); ++i) {
std::string t = trimStr(lines[i]);
if (!startsWith(t, "import ")) continue;
auto brace = t.find('{');
auto from = t.find(" from ");
if (brace != std::string::npos && from != std::string::npos) {
auto end = t.find('}', brace);
if (end == std::string::npos) continue;
std::string names = t.substr(brace + 1, end - brace - 1);
std::stringstream ss(names);
std::string item;
while (std::getline(ss, item, ',')) {
item = trimStr(item);
if (!item.empty() && !containsWord(body, item)) {
issues.push_back({i, item, "Unused import: " + item});
}
}
}
}
return issues;
}
static inline std::vector<ImportIssue> findUnusedImportsRust(const std::string& text) {
std::vector<ImportIssue> issues;
auto lines = splitLines(text);
std::string body;
for (const auto& l : lines) {
std::string t = trimStr(l);
if (startsWith(t, "use ")) continue;
body += l;
body += "\n";
}
for (int i = 0; i < (int)lines.size(); ++i) {
std::string t = trimStr(lines[i]);
if (!startsWith(t, "use ")) continue;
auto pos = t.find("::");
if (pos == std::string::npos) continue;
std::string symbol = t.substr(t.find_last_of(':') + 1);
if (!symbol.empty() && symbol.back() == ';') symbol.pop_back();
if (!symbol.empty() && !containsWord(body, symbol)) {
issues.push_back({i, symbol, "Unused import: " + symbol});
}
}
return issues;
}
static inline std::vector<ImportIssue> findUnusedImportsGo(const std::string& text) {
std::vector<ImportIssue> issues;
auto lines = splitLines(text);
std::string body;
for (const auto& l : lines) {
std::string t = trimStr(l);
if (startsWith(t, "import ")) continue;
body += l;
body += "\n";
}
for (int i = 0; i < (int)lines.size(); ++i) {
std::string t = trimStr(lines[i]);
if (startsWith(t, "import \"")) {
std::string path = t.substr(8);
if (!path.empty() && path.back() == '"') path.pop_back();
std::string symbol = lastPathSegment(path);
if (!symbol.empty() && body.find(symbol + ".") == std::string::npos) {
issues.push_back({i, symbol, "Unused import: " + symbol});
}
}
}
return issues;
}
static inline std::vector<ImportIssue> findUnusedImportsElisp(const std::string& text) {
std::vector<ImportIssue> issues;
auto lines = splitLines(text);
std::string body;
for (const auto& l : lines) {
std::string t = trimStr(l);
if (startsWith(t, "(require '")) continue;
body += l;
body += "\n";
}
for (int i = 0; i < (int)lines.size(); ++i) {
std::string t = trimStr(lines[i]);
if (startsWith(t, "(require '")) {
auto start = t.find('\'');
auto end = t.find(')', start);
if (start == std::string::npos || end == std::string::npos) continue;
std::string symbol = t.substr(start + 1, end - start - 1);
if (!symbol.empty() && body.find(symbol) == std::string::npos) {
issues.push_back({i, symbol, "Unused import: " + symbol});
}
}
}
return issues;
}
static inline std::vector<ImportIssue> findUnusedImports(const std::string& text,
const std::string& language) {
if (language == "python") return findUnusedImportsPython(text);
if (language == "javascript" || language == "typescript") return findUnusedImportsJs(text);
if (language == "rust") return findUnusedImportsRust(text);
if (language == "go") return findUnusedImportsGo(text);
if (language == "elisp") return findUnusedImportsElisp(text);
return {};
}

View File

@@ -57,8 +57,10 @@ static bool renderLibraryBrowser(LibraryBrowserState& state,
Module* ast,
const LibraryIndexData& index,
std::string& outInsert,
std::string& outLibrary,
std::string& outputLog) {
outInsert.clear();
outLibrary.clear();
if (!ast) {
ImGui::TextDisabled("(no structured AST)");
return false;
@@ -109,6 +111,7 @@ static bool renderLibraryBrowser(LibraryBrowserState& state,
std::string usage = formatUsageTemplate(state.selectedSymbol);
if (ImGui::Button("Insert")) {
outInsert = usage;
outLibrary = state.selectedLibrary;
outputLog += "[libs] Inserted " + usage + "\n";
inserted = true;
}

View File

@@ -573,11 +573,14 @@ int main(int, char**) {
ImGui::Begin("Libraries", &state.showLibraryBrowserPanel);
ImGui::PushFont(uiFont);
std::string insertText;
std::string insertLibrary;
if (renderLibraryBrowser(state.libraryBrowser,
state.activeAST(),
state.libraryIndex,
insertText,
insertLibrary,
state.outputLog)) {
state.ensureImportForSymbol(insertLibrary, insertText);
state.insertTextAtCursor(insertText);
}
ImGui::PopFont();
@@ -1290,6 +1293,7 @@ int main(int, char**) {
collectWhetstoneDiagnostics(result.validationDiags,
result.violations,
EditorState::toFileUri(state.active()->path));
state.appendUnusedImportDiagnostics(state.whetstoneDiagnostics);
} else {
state.whetstoneDiagnostics.clear();
}

View File

@@ -0,0 +1,43 @@
// Step 133 TDD Test: Import statement generation + unused import detection
#include "ImportManager.h"
#include <iostream>
static void expect(bool cond, const std::string& name, int& passed, int& failed) {
if (cond) {
std::cout << "Test " << (passed + failed + 1) << " PASS: " << name << "\n";
++passed;
} else {
std::cout << "Test " << (passed + failed + 1) << " FAIL: " << name << "\n";
++failed;
}
}
int main() {
int passed = 0;
int failed = 0;
std::string py = "import os\n\nprint('hi')\n";
auto pyRes = ensureImport(py, "python", "numpy", "array");
expect(pyRes.changed, "python import changed", passed, failed);
expect(pyRes.text.find("from numpy import array") != std::string::npos,
"python import inserted", passed, failed);
std::string js = "import { useEffect } from 'react';\n\nconsole.log('x');\n";
auto jsRes = ensureImport(js, "javascript", "react", "useState");
expect(jsRes.changed, "js import changed", passed, failed);
expect(jsRes.text.find("import { useState } from 'react';") != std::string::npos,
"js import inserted", passed, failed);
std::string go = "package main\n\nfunc main() {}\n";
auto goRes = ensureImport(go, "go", "fmt", "fmt");
expect(goRes.changed, "go import changed", passed, failed);
expect(goRes.text.find("import (\n \"fmt\"\n)") != std::string::npos,
"go import block formatting", passed, failed);
std::string pyUnused = "import os\nprint('hi')\n";
auto issues = findUnusedImports(pyUnused, "python");
expect(!issues.empty(), "unused import detected", passed, failed);
std::cout << "\n=== Step 133 Results: " << passed << " passed, " << failed << " failed ===\n";
return failed == 0 ? 0 : 1;
}

View File

@@ -78,7 +78,7 @@ The foundation: import libraries, parse their APIs, index their symbols.
Drag a symbol into the editor → inserts a usage template.
*New:* `LibraryBrowser` panel
- [ ] **Step 133: Import statement generation**
- [x] **Step 133: Import statement generation**
When user references a symbol from an `ExternalModule`, auto-generate
the correct import statement for the target language:
- Python: `from numpy import array`