Step 94a: AST source spans
This commit is contained in:
@@ -194,6 +194,7 @@ All 38 steps implemented and passing. Each step has a corresponding test (`step1
|
||||
- [x] Step 92: **IMPLEMENTED** — LSP completion requests, response parsing, and completion popup with filtering/acceptance (1/1 tests pass)
|
||||
- [x] Step 93: **IMPLEMENTED** — LSP hover and signature help requests, response parsing, and inline tooltip/popup (2/2 tests pass)
|
||||
- [x] Step 94: **IMPLEMENTED** — Whetstone diagnostics aggregation via Pipeline, merged Problems panel, and gutter markers (1/1 tests pass)
|
||||
- [x] Step 94a: **IMPLEMENTED** — AST source span tracking via tree-sitter, serialized in JSON (2/2 tests pass)
|
||||
- [x] Step 95: **IMPLEMENTED** — Diagnostic gutter markers with tooltips and inline squiggles (1/1 tests pass)
|
||||
- [x] Step 96: **IMPLEMENTED** — LSP server settings UI with defaults, auto-detect, and schema validation hook (2/2 tests pass)
|
||||
|
||||
@@ -269,6 +270,7 @@ vcpkg's imgui 1.91.9 removed the `sdl2-binding` feature (only `sdl3-binding` exi
|
||||
**Step 92:** Compile and pass (1/1)
|
||||
**Step 93:** Compile and pass (2/2)
|
||||
**Step 94:** Compile and pass (1/1)
|
||||
**Step 94a:** Compile and pass (2/2)
|
||||
**Step 95:** Compile and pass (1/1)
|
||||
**Step 96:** Compile and pass (2/2)
|
||||
|
||||
@@ -373,5 +375,6 @@ Sprint 4 in progress. Step 76 (LayoutManager) done. Next: Step 77 (custom code e
|
||||
| 2026-02-09 | Codex | Step 92: LSP completion requests/response parsing; completion popup with filtering and acceptance. 1/1 tests pass. |
|
||||
| 2026-02-09 | Codex | Step 93: LSP hover and signature help requests/response parsing; tooltip and signature popup. 2/2 tests pass. |
|
||||
| 2026-02-09 | Codex | Step 94: Whetstone diagnostics aggregation via Pipeline; merged Problems panel and gutter markers. 1/1 tests pass. |
|
||||
| 2026-02-09 | Codex | Step 94a: AST source span tracking (tree-sitter) with JSON serialization. 2/2 tests pass. |
|
||||
| 2026-02-09 | Codex | Step 95: Diagnostic gutter markers with tooltips and inline squiggles. 1/1 tests pass. |
|
||||
| 2026-02-09 | Codex | Step 96: LSP server settings UI with defaults, auto-detect, and schema validation hook. 2/2 tests pass. |
|
||||
|
||||
@@ -522,6 +522,10 @@ add_executable(step94_test tests/step94_test.cpp)
|
||||
target_include_directories(step94_test PRIVATE src)
|
||||
target_link_libraries(step94_test PRIVATE nlohmann_json::nlohmann_json)
|
||||
|
||||
add_executable(step94a_test tests/step94a_test.cpp)
|
||||
target_include_directories(step94a_test PRIVATE src)
|
||||
target_link_libraries(step94a_test PRIVATE nlohmann_json::nlohmann_json unofficial::tree-sitter::tree-sitter tree_sitter_python)
|
||||
|
||||
add_executable(step95_test tests/step95_test.cpp)
|
||||
target_include_directories(step95_test PRIVATE src)
|
||||
target_link_libraries(step95_test PRIVATE imgui::imgui unofficial::tree-sitter::tree-sitter tree_sitter_python tree_sitter_cpp tree_sitter_elisp)
|
||||
|
||||
@@ -8,9 +8,24 @@ public:
|
||||
std::string id;
|
||||
std::string conceptType;
|
||||
ASTNode* parent = nullptr;
|
||||
int spanStartLine = -1;
|
||||
int spanStartCol = -1;
|
||||
int spanEndLine = -1;
|
||||
int spanEndCol = -1;
|
||||
|
||||
virtual ~ASTNode() = default;
|
||||
|
||||
void setSpan(int startLine, int startCol, int endLine, int endCol) {
|
||||
spanStartLine = startLine;
|
||||
spanStartCol = startCol;
|
||||
spanEndLine = endLine;
|
||||
spanEndCol = endCol;
|
||||
}
|
||||
|
||||
bool hasSpan() const {
|
||||
return spanStartLine >= 0 && spanStartCol >= 0 && spanEndLine >= 0 && spanEndCol >= 0;
|
||||
}
|
||||
|
||||
// Multi-valued child: append to role
|
||||
void addChild(const std::string& role, ASTNode* child) {
|
||||
child->parent = this;
|
||||
|
||||
@@ -67,6 +67,7 @@ public:
|
||||
module->id = IdGenerator::next("mod");
|
||||
module->name = "parsed_python_module";
|
||||
module->targetLanguage = "python";
|
||||
applySpan(module.get(), root);
|
||||
|
||||
convertPythonModule(root, source, module.get());
|
||||
|
||||
@@ -86,6 +87,7 @@ public:
|
||||
result.module->id = IdGenerator::next("mod");
|
||||
result.module->name = "parsed_python_module";
|
||||
result.module->targetLanguage = "python";
|
||||
applySpan(result.module.get(), root);
|
||||
|
||||
convertPythonModule(root, source, result.module.get());
|
||||
collectDiagnostics(root, source, result.diagnostics);
|
||||
@@ -108,6 +110,7 @@ public:
|
||||
module->id = IdGenerator::next("mod");
|
||||
module->name = "parsed_cpp_module";
|
||||
module->targetLanguage = "cpp";
|
||||
applySpan(module.get(), root);
|
||||
|
||||
convertCppTranslationUnit(root, source, module.get());
|
||||
|
||||
@@ -127,6 +130,7 @@ public:
|
||||
result.module->id = IdGenerator::next("mod");
|
||||
result.module->name = "parsed_cpp_module";
|
||||
result.module->targetLanguage = "cpp";
|
||||
applySpan(result.module.get(), root);
|
||||
|
||||
convertCppTranslationUnit(root, source, result.module.get());
|
||||
collectDiagnostics(root, source, result.diagnostics);
|
||||
@@ -149,6 +153,7 @@ public:
|
||||
module->id = IdGenerator::next("mod");
|
||||
module->name = "parsed_elisp_module";
|
||||
module->targetLanguage = "elisp";
|
||||
applySpan(module.get(), root);
|
||||
|
||||
convertElispSourceFile(root, source, module.get());
|
||||
|
||||
@@ -168,6 +173,7 @@ public:
|
||||
result.module->id = IdGenerator::next("mod");
|
||||
result.module->name = "parsed_elisp_module";
|
||||
result.module->targetLanguage = "elisp";
|
||||
applySpan(result.module.get(), root);
|
||||
|
||||
convertElispSourceFile(root, source, result.module.get());
|
||||
collectDiagnostics(root, source, result.diagnostics);
|
||||
@@ -192,6 +198,13 @@ private:
|
||||
return ts_node_type(node);
|
||||
}
|
||||
|
||||
static void applySpan(ASTNode* node, TSNode tsNode) {
|
||||
if (!node || ts_node_is_null(tsNode)) return;
|
||||
TSPoint start = ts_node_start_point(tsNode);
|
||||
TSPoint end = ts_node_end_point(tsNode);
|
||||
node->setSpan((int)start.row, (int)start.column, (int)end.row, (int)end.column);
|
||||
}
|
||||
|
||||
static bool isNamed(TSNode node) {
|
||||
return ts_node_is_named(node);
|
||||
}
|
||||
@@ -243,7 +256,10 @@ private:
|
||||
|
||||
auto* fn = new Function();
|
||||
fn->id = IdGenerator::next("fn");
|
||||
applySpan(fn, node);
|
||||
applySpan(fn, node);
|
||||
fn->name = nodeText(nameNode, source);
|
||||
applySpan(fn, node);
|
||||
|
||||
// Parameters
|
||||
TSNode paramsNode = childByFieldName(node, "parameters");
|
||||
@@ -271,6 +287,7 @@ private:
|
||||
std::string type = nodeType(child);
|
||||
if (type == "identifier") {
|
||||
auto* param = new Parameter(IdGenerator::next("param"), nodeText(child, source));
|
||||
applySpan(param, child);
|
||||
fn->addChild("parameters", param);
|
||||
} else if (type == "default_parameter") {
|
||||
// def f(x=10) → Parameter with defaultValue
|
||||
@@ -278,6 +295,7 @@ private:
|
||||
TSNode valueN = childByFieldName(child, "value");
|
||||
if (!ts_node_is_null(nameN)) {
|
||||
auto* param = new Parameter(IdGenerator::next("param"), nodeText(nameN, source));
|
||||
applySpan(param, child);
|
||||
if (!ts_node_is_null(valueN)) {
|
||||
ASTNode* defVal = convertPythonExpression(valueN, source);
|
||||
if (defVal) param->setChild("defaultValue", defVal);
|
||||
@@ -303,6 +321,7 @@ private:
|
||||
if (type == "return_statement") {
|
||||
auto* ret = new Return();
|
||||
ret->id = IdGenerator::next("ret");
|
||||
applySpan(ret, node);
|
||||
// The return value is the first named child (if any)
|
||||
uint32_t count = ts_node_named_child_count(node);
|
||||
if (count > 0) {
|
||||
@@ -314,6 +333,7 @@ private:
|
||||
} else if (type == "if_statement") {
|
||||
auto* ifStmt = new IfStatement();
|
||||
ifStmt->id = IdGenerator::next("if");
|
||||
applySpan(ifStmt, node);
|
||||
TSNode condNode = childByFieldName(node, "condition");
|
||||
if (!ts_node_is_null(condNode)) {
|
||||
ASTNode* cond = convertPythonExpression(condNode, source);
|
||||
@@ -331,6 +351,7 @@ private:
|
||||
} else if (type == "for_statement") {
|
||||
auto* forLoop = new ForLoop();
|
||||
forLoop->id = IdGenerator::next("for");
|
||||
applySpan(forLoop, node);
|
||||
TSNode leftNode = childByFieldName(node, "left");
|
||||
if (!ts_node_is_null(leftNode)) {
|
||||
forLoop->iteratorName = nodeText(leftNode, source);
|
||||
@@ -352,6 +373,7 @@ private:
|
||||
} else if (type == "expression_statement") {
|
||||
auto* exprStmt = new ExpressionStatement();
|
||||
exprStmt->id = IdGenerator::next("exprstmt");
|
||||
applySpan(exprStmt, node);
|
||||
uint32_t count = ts_node_named_child_count(node);
|
||||
if (count > 0) {
|
||||
ASTNode* expr = convertPythonExpression(ts_node_named_child(node, 0), source);
|
||||
@@ -364,6 +386,7 @@ private:
|
||||
if (expr) {
|
||||
auto* exprStmt = new ExpressionStatement();
|
||||
exprStmt->id = IdGenerator::next("exprstmt");
|
||||
applySpan(exprStmt, node);
|
||||
exprStmt->setChild("expression", expr);
|
||||
return exprStmt;
|
||||
}
|
||||
@@ -375,6 +398,7 @@ private:
|
||||
if (type == "binary_operator") {
|
||||
auto* binOp = new BinaryOperation();
|
||||
binOp->id = IdGenerator::next("binop");
|
||||
applySpan(binOp, node);
|
||||
TSNode opNode = childByFieldName(node, "operator");
|
||||
if (!ts_node_is_null(opNode)) {
|
||||
binOp->op = nodeText(opNode, source);
|
||||
@@ -392,20 +416,24 @@ private:
|
||||
return binOp;
|
||||
} else if (type == "identifier") {
|
||||
auto* ref = new VariableReference(IdGenerator::next("var"), nodeText(node, source));
|
||||
applySpan(ref, node);
|
||||
return ref;
|
||||
} else if (type == "integer") {
|
||||
std::string text = nodeText(node, source);
|
||||
int val = 0;
|
||||
try { val = std::stoi(text); } catch (...) {}
|
||||
auto* lit = new IntegerLiteral(IdGenerator::next("int"), val);
|
||||
applySpan(lit, node);
|
||||
return lit;
|
||||
} else if (type == "string" || type == "concatenated_string") {
|
||||
auto* lit = new StringLiteral(IdGenerator::next("str"), nodeText(node, source));
|
||||
applySpan(lit, node);
|
||||
return lit;
|
||||
} else if (type == "comparison_operator" || type == "boolean_operator") {
|
||||
// Treat like binary op
|
||||
auto* binOp = new BinaryOperation();
|
||||
binOp->id = IdGenerator::next("binop");
|
||||
applySpan(binOp, node);
|
||||
uint32_t count = ts_node_named_child_count(node);
|
||||
if (count >= 2) {
|
||||
ASTNode* left = convertPythonExpression(ts_node_named_child(node, 0), source);
|
||||
@@ -429,6 +457,7 @@ private:
|
||||
} else if (type == "call") {
|
||||
auto* call = new FunctionCall();
|
||||
call->id = IdGenerator::next("call");
|
||||
applySpan(call, node);
|
||||
TSNode funcNode = childByFieldName(node, "function");
|
||||
if (!ts_node_is_null(funcNode)) {
|
||||
call->functionName = nodeText(funcNode, source);
|
||||
@@ -448,6 +477,7 @@ private:
|
||||
} else if (type == "unary_operator") {
|
||||
auto* unOp = new UnaryOperation();
|
||||
unOp->id = IdGenerator::next("unop");
|
||||
applySpan(unOp, node);
|
||||
TSNode opNode = childByFieldName(node, "operator");
|
||||
if (!ts_node_is_null(opNode)) {
|
||||
unOp->op = nodeText(opNode, source);
|
||||
@@ -463,6 +493,7 @@ private:
|
||||
std::string text = nodeText(node, source);
|
||||
if (!text.empty()) {
|
||||
auto* ref = new VariableReference(IdGenerator::next("var"), text);
|
||||
applySpan(ref, node);
|
||||
return ref;
|
||||
}
|
||||
return nullptr;
|
||||
@@ -486,6 +517,7 @@ private:
|
||||
static Function* convertCppFunction(TSNode node, const std::string& source) {
|
||||
auto* fn = new Function();
|
||||
fn->id = IdGenerator::next("fn");
|
||||
applySpan(fn, node);
|
||||
|
||||
// In C++ grammar, the structure is:
|
||||
// function_definition: type declarator body
|
||||
@@ -578,6 +610,7 @@ private:
|
||||
static void convertCppParameter(TSNode paramNode, const std::string& source, Function* fn) {
|
||||
auto* param = new Parameter();
|
||||
param->id = IdGenerator::next("param");
|
||||
applySpan(param, paramNode);
|
||||
|
||||
// parameter_declaration has type and declarator fields
|
||||
TSNode typeNode = childByFieldName(paramNode, "type");
|
||||
@@ -586,6 +619,7 @@ private:
|
||||
if (!ts_node_is_null(typeNode)) {
|
||||
std::string typeText = nodeText(typeNode, source);
|
||||
auto* primType = new PrimitiveType(IdGenerator::next("type"), typeText);
|
||||
applySpan(primType, typeNode);
|
||||
param->setChild("type", primType);
|
||||
}
|
||||
|
||||
@@ -611,6 +645,7 @@ private:
|
||||
if (type == "return_statement") {
|
||||
auto* ret = new Return();
|
||||
ret->id = IdGenerator::next("ret");
|
||||
applySpan(ret, node);
|
||||
uint32_t count = ts_node_named_child_count(node);
|
||||
if (count > 0) {
|
||||
ASTNode* val = convertCppExpression(ts_node_named_child(node, 0), source);
|
||||
@@ -620,6 +655,7 @@ private:
|
||||
} else if (type == "expression_statement") {
|
||||
auto* exprStmt = new ExpressionStatement();
|
||||
exprStmt->id = IdGenerator::next("exprstmt");
|
||||
applySpan(exprStmt, node);
|
||||
uint32_t count = ts_node_named_child_count(node);
|
||||
if (count > 0) {
|
||||
ASTNode* expr = convertCppExpression(ts_node_named_child(node, 0), source);
|
||||
@@ -629,10 +665,12 @@ private:
|
||||
} else if (type == "declaration") {
|
||||
auto* exprStmt = new ExpressionStatement();
|
||||
exprStmt->id = IdGenerator::next("exprstmt");
|
||||
applySpan(exprStmt, node);
|
||||
return exprStmt;
|
||||
} else if (type == "if_statement") {
|
||||
auto* ifStmt = new IfStatement();
|
||||
ifStmt->id = IdGenerator::next("if");
|
||||
applySpan(ifStmt, node);
|
||||
return ifStmt;
|
||||
}
|
||||
return nullptr;
|
||||
@@ -643,6 +681,7 @@ private:
|
||||
if (type == "binary_expression") {
|
||||
auto* binOp = new BinaryOperation();
|
||||
binOp->id = IdGenerator::next("binop");
|
||||
applySpan(binOp, node);
|
||||
TSNode leftNode = childByFieldName(node, "left");
|
||||
TSNode rightNode = childByFieldName(node, "right");
|
||||
TSNode opNode = childByFieldName(node, "operator");
|
||||
@@ -659,14 +698,20 @@ private:
|
||||
}
|
||||
return binOp;
|
||||
} else if (type == "identifier") {
|
||||
return new VariableReference(IdGenerator::next("var"), nodeText(node, source));
|
||||
auto* ref = new VariableReference(IdGenerator::next("var"), nodeText(node, source));
|
||||
applySpan(ref, node);
|
||||
return ref;
|
||||
} else if (type == "number_literal") {
|
||||
std::string text = nodeText(node, source);
|
||||
int val = 0;
|
||||
try { val = std::stoi(text); } catch (...) {}
|
||||
return new IntegerLiteral(IdGenerator::next("int"), val);
|
||||
auto* lit = new IntegerLiteral(IdGenerator::next("int"), val);
|
||||
applySpan(lit, node);
|
||||
return lit;
|
||||
} else if (type == "string_literal" || type == "raw_string_literal") {
|
||||
return new StringLiteral(IdGenerator::next("str"), nodeText(node, source));
|
||||
auto* lit = new StringLiteral(IdGenerator::next("str"), nodeText(node, source));
|
||||
applySpan(lit, node);
|
||||
return lit;
|
||||
} else if (type == "parenthesized_expression") {
|
||||
uint32_t count = ts_node_named_child_count(node);
|
||||
if (count > 0) return convertCppExpression(ts_node_named_child(node, 0), source);
|
||||
@@ -674,7 +719,9 @@ private:
|
||||
// Fallback
|
||||
std::string text = nodeText(node, source);
|
||||
if (!text.empty()) {
|
||||
return new VariableReference(IdGenerator::next("var"), text);
|
||||
auto* ref = new VariableReference(IdGenerator::next("var"), text);
|
||||
applySpan(ref, node);
|
||||
return ref;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
@@ -726,6 +773,7 @@ private:
|
||||
static Function* convertElispDefun(TSNode node, const std::string& source) {
|
||||
auto* fn = new Function();
|
||||
fn->id = IdGenerator::next("fn");
|
||||
applySpan(fn, node);
|
||||
|
||||
// tree-sitter-elisp function_definition has:
|
||||
// field "name" → symbol (function name)
|
||||
@@ -761,6 +809,7 @@ private:
|
||||
if (expr) {
|
||||
auto* exprStmt = new ExpressionStatement();
|
||||
exprStmt->id = IdGenerator::next("exprstmt");
|
||||
applySpan(exprStmt, child);
|
||||
exprStmt->setChild("expression", expr);
|
||||
fn->addChild("body", exprStmt);
|
||||
}
|
||||
@@ -819,11 +868,13 @@ private:
|
||||
// Last form — wrap in ExpressionStatement (implicit return)
|
||||
auto* exprStmt = new ExpressionStatement();
|
||||
exprStmt->id = IdGenerator::next("exprstmt");
|
||||
applySpan(exprStmt, bodyChild);
|
||||
exprStmt->setChild("expression", expr);
|
||||
fn->addChild("body", exprStmt);
|
||||
} else {
|
||||
auto* exprStmt = new ExpressionStatement();
|
||||
exprStmt->id = IdGenerator::next("exprstmt");
|
||||
applySpan(exprStmt, bodyChild);
|
||||
exprStmt->setChild("expression", expr);
|
||||
fn->addChild("body", exprStmt);
|
||||
}
|
||||
@@ -865,6 +916,7 @@ private:
|
||||
if (expr) {
|
||||
auto* exprStmt = new ExpressionStatement();
|
||||
exprStmt->id = IdGenerator::next("exprstmt");
|
||||
applySpan(exprStmt, bodyChild);
|
||||
exprStmt->setChild("expression", expr);
|
||||
fn->addChild("body", exprStmt);
|
||||
}
|
||||
@@ -883,6 +935,7 @@ private:
|
||||
std::string type = nodeType(child);
|
||||
if (type == "symbol" || type == "identifier") {
|
||||
auto* param = new Parameter(IdGenerator::next("param"), nodeText(child, source));
|
||||
applySpan(param, child);
|
||||
fn->addChild("parameters", param);
|
||||
}
|
||||
}
|
||||
@@ -898,6 +951,7 @@ private:
|
||||
if (expr) {
|
||||
auto* exprStmt = new ExpressionStatement();
|
||||
exprStmt->id = IdGenerator::next("exprstmt");
|
||||
applySpan(exprStmt, child);
|
||||
exprStmt->setChild("expression", expr);
|
||||
fn->addChild("body", exprStmt);
|
||||
}
|
||||
@@ -907,6 +961,7 @@ private:
|
||||
if (expr) {
|
||||
auto* exprStmt = new ExpressionStatement();
|
||||
exprStmt->id = IdGenerator::next("exprstmt");
|
||||
applySpan(exprStmt, bodyNode);
|
||||
exprStmt->setChild("expression", expr);
|
||||
fn->addChild("body", exprStmt);
|
||||
}
|
||||
@@ -936,6 +991,7 @@ private:
|
||||
if (expr) {
|
||||
auto* exprStmt = new ExpressionStatement();
|
||||
exprStmt->id = IdGenerator::next("exprstmt");
|
||||
applySpan(exprStmt, child);
|
||||
exprStmt->setChild("expression", expr);
|
||||
fn->addChild("body", exprStmt);
|
||||
}
|
||||
@@ -956,6 +1012,7 @@ private:
|
||||
auto* binOp = new BinaryOperation();
|
||||
binOp->id = IdGenerator::next("binop");
|
||||
binOp->op = firstText;
|
||||
applySpan(binOp, node);
|
||||
ASTNode* left = convertElispExpression(ts_node_named_child(node, 1), source);
|
||||
ASTNode* right = convertElispExpression(ts_node_named_child(node, 2), source);
|
||||
if (left) binOp->setChild("left", left);
|
||||
@@ -967,6 +1024,7 @@ private:
|
||||
if (count >= 1) {
|
||||
auto* call = new FunctionCall();
|
||||
call->id = IdGenerator::next("call");
|
||||
applySpan(call, node);
|
||||
TSNode funcName = ts_node_named_child(node, 0);
|
||||
call->functionName = nodeText(funcName, source);
|
||||
for (uint32_t i = 1; i < count; ++i) {
|
||||
@@ -976,14 +1034,20 @@ private:
|
||||
return call;
|
||||
}
|
||||
} else if (type == "symbol" || type == "identifier") {
|
||||
return new VariableReference(IdGenerator::next("var"), nodeText(node, source));
|
||||
auto* ref = new VariableReference(IdGenerator::next("var"), nodeText(node, source));
|
||||
applySpan(ref, node);
|
||||
return ref;
|
||||
} else if (type == "integer" || type == "number") {
|
||||
std::string text = nodeText(node, source);
|
||||
int val = 0;
|
||||
try { val = std::stoi(text); } catch (...) {}
|
||||
return new IntegerLiteral(IdGenerator::next("int"), val);
|
||||
auto* lit = new IntegerLiteral(IdGenerator::next("int"), val);
|
||||
applySpan(lit, node);
|
||||
return lit;
|
||||
} else if (type == "string") {
|
||||
return new StringLiteral(IdGenerator::next("str"), nodeText(node, source));
|
||||
auto* lit = new StringLiteral(IdGenerator::next("str"), nodeText(node, source));
|
||||
applySpan(lit, node);
|
||||
return lit;
|
||||
} else if (type == "special_form") {
|
||||
// Could be (if ...), (let ...), etc.
|
||||
return convertElispSpecialForm(node, source);
|
||||
@@ -992,7 +1056,9 @@ private:
|
||||
// Fallback
|
||||
std::string text = nodeText(node, source);
|
||||
if (!text.empty()) {
|
||||
return new VariableReference(IdGenerator::next("var"), text);
|
||||
auto* ref = new VariableReference(IdGenerator::next("var"), text);
|
||||
applySpan(ref, node);
|
||||
return ref;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
@@ -1007,6 +1073,7 @@ private:
|
||||
if (formName == "if" && count >= 3) {
|
||||
auto* ifStmt = new IfStatement();
|
||||
ifStmt->id = IdGenerator::next("if");
|
||||
applySpan(ifStmt, node);
|
||||
ASTNode* cond = convertElispExpression(ts_node_named_child(node, 1), source);
|
||||
if (cond) ifStmt->setChild("condition", cond);
|
||||
return ifStmt;
|
||||
@@ -1015,6 +1082,7 @@ private:
|
||||
// Generic: treat as function call
|
||||
auto* call = new FunctionCall();
|
||||
call->id = IdGenerator::next("call");
|
||||
applySpan(call, node);
|
||||
call->functionName = formName;
|
||||
for (uint32_t i = 1; i < count; ++i) {
|
||||
ASTNode* arg = convertElispExpression(ts_node_named_child(node, i), source);
|
||||
|
||||
@@ -123,6 +123,12 @@ inline json toJson(const ASTNode* node) {
|
||||
j["id"] = node->id;
|
||||
j["concept"] = node->conceptType;
|
||||
j["properties"] = propertiesToJson(node);
|
||||
if (node->hasSpan()) {
|
||||
j["span"] = {
|
||||
{"start", {{"line", node->spanStartLine}, {"col", node->spanStartCol}}},
|
||||
{"end", {{"line", node->spanEndLine}, {"col", node->spanEndCol}}}
|
||||
};
|
||||
}
|
||||
|
||||
json children = json::object();
|
||||
for (const auto& role : node->childRoles()) {
|
||||
@@ -274,6 +280,17 @@ inline ASTNode* fromJson(const json& j) {
|
||||
if (!node) return nullptr;
|
||||
|
||||
node->id = j["id"].get<std::string>();
|
||||
if (j.contains("span")) {
|
||||
const auto& span = j["span"];
|
||||
if (span.contains("start") && span.contains("end")) {
|
||||
const auto& s = span["start"];
|
||||
const auto& e = span["end"];
|
||||
if (s.contains("line") && s.contains("col") && e.contains("line") && e.contains("col")) {
|
||||
node->setSpan(s["line"].get<int>(), s["col"].get<int>(),
|
||||
e["line"].get<int>(), e["col"].get<int>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (j.contains("properties")) {
|
||||
setPropertiesFromJson(node, j["properties"]);
|
||||
|
||||
39
editor/tests/step94a_test.cpp
Normal file
39
editor/tests/step94a_test.cpp
Normal file
@@ -0,0 +1,39 @@
|
||||
// Step 94a TDD Test: AST source span tracking
|
||||
//
|
||||
// Tests:
|
||||
// 1. Parser assigns spans to AST nodes
|
||||
// 2. Serialization preserves spans
|
||||
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
#include "ast/Parser.h"
|
||||
#include "ast/Serialization.h"
|
||||
|
||||
int main() {
|
||||
int passed = 0;
|
||||
int failed = 0;
|
||||
|
||||
std::string source = "def foo(x):\n return x\n";
|
||||
auto mod = TreeSitterParser::parsePython(source);
|
||||
assert(mod);
|
||||
auto funcs = mod->getChildren("functions");
|
||||
assert(!funcs.empty());
|
||||
auto* fn = funcs[0];
|
||||
assert(fn->hasSpan());
|
||||
assert(fn->spanStartLine == 0);
|
||||
assert(fn->spanEndLine >= 1);
|
||||
std::cout << "Test 1 PASS: parser spans set" << std::endl;
|
||||
++passed;
|
||||
|
||||
auto j = toJson(fn);
|
||||
assert(j.contains("span"));
|
||||
assert(j["span"].contains("start"));
|
||||
auto* round = fromJson(j);
|
||||
assert(round->hasSpan());
|
||||
assert(round->spanStartLine == fn->spanStartLine);
|
||||
std::cout << "Test 2 PASS: serialization preserves spans" << std::endl;
|
||||
++passed;
|
||||
|
||||
std::cout << "\n=== Step 94a Results: " << passed << " passed, " << failed << " failed ===" << std::endl;
|
||||
return failed > 0 ? 1 : 0;
|
||||
}
|
||||
@@ -168,6 +168,12 @@ This is the biggest architectural change from the original plan.
|
||||
Wire `Pipeline.h` for full analysis pass (debounced, cached).
|
||||
*Wires:* `AnnotationValidator.h`, `StrategyValidator.h`, `Pipeline.h`
|
||||
|
||||
- [ ] **Step 94a: Source span tracking**
|
||||
Add source spans (start/end line/column) to AST nodes and propagate them from
|
||||
tree-sitter CST nodes. Serialize spans in `Serialization.h`. Whetstone diagnostics
|
||||
map to exact ranges using node spans (no heuristic line guessing).
|
||||
*Modifies:* `ast/ASTNode.h`, `ast/Parser.h`, `ast/Serialization.h`
|
||||
|
||||
- [ ] **Step 95: Gutter markers and inline squiggles**
|
||||
Red circle (error) and yellow triangle (warning) icons in the gutter for
|
||||
lines with diagnostics (from both LSP and Whetstone). Hover shows message
|
||||
|
||||
Reference in New Issue
Block a user