Imported from denprog/yutovo-editor (
AGENTS.md). Install upstream withnpx skills add denprog/yutovo-editor. Copyright stays with the author.
Yutovo Editor Agent Notes
Project
Document editor with MathML rendering and solver integration.
Agent Rules
- Use
repowisefor codebase navigation and search. Prefermcp__repowise__get_answer,mcp__repowise__get_context,mcp__repowise__search_codebase, and related tools over manualRead/Grepwhen exploring unfamiliar code, locating symbols, or analyzing architecture. - Never delete files without explicit user permission. Do not remove source files, test files, core dumps, logs, build artifacts, or any other files unless the user explicitly asks for it. When in doubt, leave the file in place and ask.
- Never create separate namespaces (such as
namespace detailor anonymous namespaces) without explicit user permission. Helper functions should be placed in the commonyutovo_calculatornamespace, for example inutils.h/utils.cpporgiac_utils.h/giac_utils.cpp, or asstaticmethods of the appropriate class. - Never commit without explicit user permission. Do not run
git commit,git push,git reset,git rebase, or any other git mutations unless explicitly asked to do so. Ask for confirmation each time when git mutations are needed. - Never delete existing tests. When fixing regressions or refactoring, update test expectations to match the new correct behavior, but do not remove tests. If
git checkoutor similar commands are used to revert a file, verify that no user-added tests were lost. - Prefer targeted tests. Run only the tests affected by the change (e.g.
./yutovo-editor_tests --gtest_filter='FormulaTest.function_definition*'). Before running the fullyutovo-editor_testssuite, ask the user at the beginning of the task (after planning) whether the complete suite is needed. - Add new
ElementTypevalues only at the end of the enum.editor_utils.hserializes element types as integers, so inserting values in the middle changes the numeric IDs of existing types and breaks saved documents.
Symbolic Integration
ElementType::SYMBOLIC_REAL_RESULT,SYMBOLIC_RATIONAL_RESULT,SYMBOLIC_COMPLEX_RESULTadded (oldSYMBOLIC_RESULTremoved).Config::AutoResultConfignow containssymbolic_real_result(RealResultConfig),symbolic_rational_result(RationalResultConfig),symbolic_complex_result(ComplexResultConfig).SymbolicRealResult/SymbolicRationalResult/SymbolicComplexResultinherit fromRealResult/RationalResult/ComplexResultand overridePutResultto parse symbolic strings viaAddSymbolicElementswhen the result contains alphabetic characters.SymbolicRealSolverTask/SymbolicRationalSolverTask/SymbolicComplexSolverTasksend JSON withresult_type=SYMBOLIC_REAL/RATIONAL/COMPLEX.Equation::UpdateResultmust handleResultType::SYMBOLIC_REAL/RATIONAL/COMPLEXby creating the corresponding result class; missing this causes tests to hang because no solver task is ever dispatched.ResultTask::Executemust handleElementType::SYMBOLIC_REAL/RATIONAL/COMPLEX_RESULTto deliver the solver response to the correspondingPutResult; missing this leaves the waiting symbol (~) forever.
Definite Integral Element
ElementType::DEFINITE_INTEGRAL, classDefiniteIntegral : public Formulainsrc/formulas/definite_integral.h/.cpp(definite integral, symbol∫).- Children:
CodeRowlower limit (0),Shapeintegral symbol (1),CodeRowupper limit (2),CodeRowintegrand (3), non-editableCodeString"d" (4,editable = false),CodeRowintegration variable (5). ToText()andToParserString()producedefinite_integral(lower,upper,integrand,var)— all 4 editable rows are passed to the calculator throughdefinite_integral().- Insert via
document.InsertDefiniteIntegral(with_undo); undo stores/restores children 0, 2, 3, 5 (see undo.cpp); registered in editor_utils.cppcreate_elements. - Tests:
test/definite_integral.cpp(FormulaTest.definite_integral1..definite_integral9).
Indefinite Integral Element
ElementType::INDEFINITE_INTEGRAL, classIndefiniteIntegral : public Formulainsrc/formulas/indefinite_integral.h/.cpp(indefinite integral, symbol∫).- Children:
Shapeintegral symbol (0),CodeRowintegrand (1), non-editableCodeString"d" (2,editable = false),CodeRowintegration variable (3). ToText()andToParserString()produceindefinite_integral(integrand,var)— the 2 editable rows are passed to the calculator throughindefinite_integral().- Insert via
document.InsertIndefiniteIntegral(with_undo); undo stores/restores children 1, 3 (see undo.cpp); registered in editor_utils.cppcreate_elements. - Tests:
test/indefinite_integral.cpp(FormulaTest.indefinite_integral1..indefinite_integral9).
Dependency Re-solve and Subscripted Identifiers
- The calculator reports the identifiers used by a solve in
Result::dependencies(filled byAddDependencyin yutovo-calculatorsrc/solver.h): a subscripted reference is reported asName{subscript}(e.g.Ставки{i}inside a sum), a plain reference asName. Error replies carry the dependencies collected before the failure, and everyPutResultsets them on the element unconditionally. Equation/Assignment/Graphcachedependencies;ResolveDependenciesTaskre-solves the elements below a re-registered declaration whenDepends(identifier)matches. The matching isFormula::MatchDependency(src/formulas/formula.h/.cpp): it compares the base name before the first{, so re-registering a plain array declarationСтавкиre-solves formulas referencing onlyСтавки{i}(and vice versa). An exact-string match here leaves such dependents stuck with a stale value/error after deleting, editing, or undoing the declaration (fixed aftervariables35..37).- Deleting an assignment element keeps its now-empty line, and the dead logical id resolves fuzzily to that line's
CodeRow, soResolveDependenciesTaskstill scans the formulas below. Deleting the whole line works through a different path:Elements::UpdateIds→Assignment::LogicalIdChanged→SetIdentifierper shifted declaration. - Tests:
test/variables.cpp(VariablesTest.variables35value edit,variables36delete →Unknown identifier,variables37undo → re-solve; the shared builder isVariablesTest::CreateArrayAndSumin mock.h).
Derivative Element
Derivatives are represented by a regular editable Division fraction so that the d/∂ prefixes, the function, and the variables are all editable.
- A derivative fraction has the form
d f / d x(or∂ f / ∂ xfor partial derivatives). For ordern > 1the numerator starts withpow(d, n)/pow(∂, n). Division::BuildDerivativeParserString()detects the derivative pattern inToText()/ToParserString()and emits nestedderivative(...)calls. The total differentiation order in the numerator must equal the sum of differentiation operators in the denominator; otherwise the fraction falls back to plain(num)/(den).d f / d x→derivative(f,x)pow(d,2) f / pow(d x, 2)→derivative(derivative(f,x),x)(single-variable higher-order)pow(d,2) f / d x d y→derivative(derivative(f,y),x)(rightmost denominator variable is the innermost derivative)pow(d,3) f(x,y,z) / d x d y d z→derivative(derivative(derivative(f(x,y,z),z),y),x)∂ f / ∂ x→derivative(f,x)(partial)
- The total differentiation order is the sum of operators in the denominator (
d/∂= 1,pow(d,n)/pow(∂,n)=n). If it does not equal the numerator order, the fraction falls back to ordinary(num)/(den)output. - The parser recognizes the derivative marker even when it is merged with the function or variable in a single
CodeString(e.g.dg(x,y)/dxdy), becauseBuildDerivativeParserString()scans the text character-by-character. - The
d/∂prefix strings and the empty function/variable placeholders are created withcan_merge = falseso they remain distinct editable elements (seeString::Merge). - Insert via
document.InsertDerivative,InsertSecondDerivative,InsertPartialDerivative,InsertPartialSecondDerivative(all implemented withCreateDerivativeDivision()indocument.cpp). - Undo/redo stores/restores the editable
CodeStringchildren inside theDivision(seeundo.cpp). - Tests:
test/derivative.cpp(FormulaTest.derivative1..derivative17) andtest/solver_derivative.cpp(SolverAutoTest.derivative1/derivative2/derivative_second1/derivative_third1/derivative_mixed_func/derivative_mixed_func_g/derivative_tan,SolverSymbolicTest.derivative1). Derivative tests that target an explicit numeric result type (e.g.ResultType::REAL) belong in the matchingtest/solver_<type>.cppfile (e.g.SolverRealTest.derivative_mixed_func_gandSolverRealTest.derivative_mixed_func_g_realintest/solver_real.cpp). - Test helpers for building derivative
Divisioninstances live as methods ofSolverTest(test/mock.h), not as file-scopestaticfunctions. The helper must emulate user input (InsertDerivative,InsertString,InsertPower,MoveCaretDown, etc.) instead of constructingDivision/CodeString/Powerobjects by hand, because the desktop insertion/remake path differs from direct element construction. - Trigonometric/hyperbolic aliases (
tg/tan,ctg/cot,cosec/csc,sh/sinh,ch/cosh,th/tanh,cth/coth,sch/sech,cosech/csch) and inverse trig aliases (arctg/arctan,arcctg/arccot,arccosec/arccsc) are registered in all three symbolic parsers. Canonical symbolic function names aretg,arctg,arcctg(renamed fromtan/arctan/arccot). - TermDegree
known_funcsinyutovo-calculator/src/symbolic.handFunctionSortRankinyutovo-calculator/src/giac_utils.cppinclude all aliases and canonical output names (tan,asin,acos,atan, etc.). CalcTestSymbolicReal.all_symbolic_functionscovers every symbolic trig/hyperbolic/inverse function with four variants: numeric evaluation, symbolic form, derivative, and alias equivalence.
Evaluation at a Point
- Evaluating an expression at a point
expr |_{x=2}reuses the existingEvaluationBarSubscript(evaluation bar with assignments in the subscript) — no new element types. The bar is inserted withdocument.InsertEvaluationBarSubscript(with_undo); users type the expression before it and fill thex=2rows in the subscript. Document::InsertFunctionAtPoint(with_undo)(document.cpp, afterInsertDerivativeAtPoint) inserts the template: the evaluation bar, then round brackets( )before it (same pattern asInsertRoundBrackets:InsertFormulas(..., select_pos=1)+ oneMoveCaretLeft) — the caret lands between the brackets; fourMoveCaretRightpresses get into the first bar assignment row, two more get out of the bar after filling the value.CodeRow::ToParserString(src/formulas/code_row.cpp) wraps the segment before such a bar intoevaluate(expr,[x=2,...]). The bar is skipped when it directly follows a derivative fraction (Division::BuildDerivativeAtPointParserStringconsumes it intoderivative(f,[x=2])), or when the assignments or the expression are empty (soft degradation while editing).ToText()stays the plain concatenationexpr[x=2].- yutovo-calculator parses
evaluate(expr,[x=2,y=3])in all six parsers (grammar ruleevaluate_at_point, AST nodeEvaluateAtPointNode, first alternative ofunary): numeric types evaluate viaSolver::EvaluateWithTempVariables(temp variables), symbolic types parse the expression string and applysubsper variable."evaluate"is in TermDegreeknown_funcs(symbolic.h). - Tests:
test/evaluation_bar.cpp(FormulaTest.evaluate1: editing, fullToHtml(), caret, undo/redo;FormulaTest.function_at_point1: the inserted template( ) |, filling it, fullToHtml(), caret, undo/redo),test/solver_evaluate.cpp(SolverAutoTest.evaluate1..4: value, multi variable, user function, bar after a plain fraction →(x)/(y)[y=2,x=6]=3.;SolverAutoTest.function_at_point1: template + solve →(pow(x,2))[x=3]=9.), calculator testsevaluate*in all six calculator test files. - Front-ends: the "Evaluate at point" button inserts the bar — desktop
actionEvaluationBar(algebra toolbar,MainWindow::OnEvaluationBar, testTestToolbar::testEvaluationBar), webonEvalutionBar()inYutovoWeb.vuemustModule.cwrap('OnEvaluationBar', ...)(the wasm export insrc/main.cppis spelled without the typo). The "Function at point" button inserts the template — desktopactionFunctionAtPoint(calculus toolbar after "Derivative at point", iconimages/algebra/function_at_point.png, testTestToolbar::testFunctionAtPoint), webonFunctionAtPoint()→OnFunctionAtPoint(test/function_at_point.spec.js).
Rebuild and Deploy
- Projects link prebuilt libraries from
$YUTOVO_DEPLOY, andmakedoes not track imported.achanges: aftermake installof yutovo-calculator/yutovo-solver/yutovo-editor, delete the dependant build artifacts to force relinking (rmthe executables, orfind . -name "*.a" -deleteinside their build dir, then rebuild). A stale link shows up asSyntax errorresults for new syntax. - Editor tests use the builtin solver through a separate worker process (
yutovo-solver-calculator-workerd):FindWorkerExecutablelooks next to the test binary first, then$YUTOVO_DEPLOY/bin, then the working directory. Stale workerd copies (e.g.build/debug/yutovo-solver-calculator-workerd) keep parsing with the old calculator after changes.
Text Block
ElementType::TEXT_BLOCK,TEXT_EQUATION,TEXT_ASSIGNMENT(appended at the end of the enum). ClassesTextBlock : public Block(src/formulas/text_block.h/.cpp),TextEquation : public Equation(src/formulas/text_equation.h/.cpp),TextAssignment : public Assignment(src/formulas/text_assignment.h/.cpp).- TextBlock is a block for entering formulas without solving: editing works exactly like in CodeBlock (CodeParagraph/CodeRow/CodeString children, same "Calculator"/"Code"/"Formula" formats), but nothing is computed. It can be inserted into document text via
document.InsertTextBlock(with_undo)and inside a CodeBlock (stays a nested block). Neither a CodeBlock nor another TextBlock can be inserted inside a TextBlock —CodeRow::InsertElementsrejects both. TextBlock::AfterFromJsonconverts plainSTRINGelements saved by older versions intoCODE_STRING(TextBlock::ConvertStringsToCode, which must not descend intoSTRING/CODE_STRING/LINK- their element lists return the element itself).TextEquation/TextAssignmentnever solve:TextEquationoverridesSolve/ReSolvewith empty bodies,TextAssignmentsetsauto_solve = false. The right part stays a plain editable CodeRow, andTextEquation::AfterInsertputs the caret into the right row (typing on the=shape is rejected byMiddleShapeFormula::InsertElements).TextAssignmentuses the plain:sign (solve_sign/draw_sign, ToTextx:5), unlike the:=of a computing Assignment.- Typing
=/:inside a TextBlock:InsertFormulasTask::Executeconverts the clonedEQUATION/ASSIGNMENTelements into their text counterparts viaTextBlock::ConvertToText, so front-ends callingInsertEquation/InsertAssignmentwork unchanged. Pasting a whole CodeBlock into a TextBlock flattens its paragraphs and converts them the same way (CodeRow::InsertElementscallsTextBlock::ConvertToTexton the collected rows whenTextBlock::BlocksSolving(this)); a solved result row is flattened into a plain editable row byTextBlock::CollectRowElements, which recurses through nested rows and results (anAutoResultholds aRealResultchild after solving - without the recursion the result element would be pasted as-is and lost on save). An empty code block is still rejected. Config::TextBlockConfig text_blockcontainsbackground_color(default#cfcab0, yellowish) andframe_color(default White). Like the other colors these are application settings - they are not serialized into the document config (Config::ToJson/FromJsonmust not write them, otherwise a saved document would override the application colors on load). The frame is gated by the sharedcode_block_borderflag; other visual parameters come from the CodeBlock formats. yutovo-desktop persists both colors in QSettings astext_block_background_color/text_block_frame_color.- Undo/redo:
TEXT_EQUATION/TEXT_ASSIGNMENTare stored likeASSIGNMENT(children 0 and 2 inUndoFormula),TEXT_BLOCKhasUndoTextBlock(formats + children, seeundo.cpp). - Tests:
test/text_block.cpp(TextBlockTest.text_block1..15,text_block15copies a code block with an assignment and a solved equation and pastes it into a text block,text_block12/text_block13check thatInsertCode/InsertTextBlockinside a text block are rejected,text_block14loads a handwritten json with a legacy plainSTRINGin the right part and checks it becomes a code string);text_block10checks that a text block inserted into document text accepts formulas after=(exactToHtml()MathML with anmfrac),text_block11saves to a.yutfile and checks the stored json (a gzip stream unpacked viaBoost::iostreams, which the test target already links). Do not useFindAllByTypefrommock.hon documents containing strings —StringElements::Get(pos)returns the string itself, causing infinite recursion; count elements with a helper that skipsSTRING/CODE_STRING. - Front-ends: formula commands ("=", ":", "+", ...) must be allowed inside a text block placed in the document text too —
CommandContext::Formulain bothcommand_map.cppfiles (desktop and web) acceptsTEXT_BLOCKas well asCODE_BLOCKancestors, otherwise "=" is typed as a plain string. yutovo-desktop has a toolbar button next to "Insert calculator" (text_block_action,MainWindow::OnInsertTextBlockinmainwindow.cpp, iconimages/format/text_block.png); yutovo-web has#insert-text-block-button(YutovoWeb.vue→ wasm exportOnInsertTextBlockinsrc/main.cpp). Both link the prebuilt editor from$YUTOVO_DEPLOY— reinstall yutovo-editor (native and wasm builds) into deploy before rebuilding them. Button tests: desktopTestToolbar::testTextBlock,TestToolbar::testTextBlockInText(keyboard input into a block placed in the document text, clicks viaQTest::mouseClick), (test/toolbar.cpp, the action is found by objectNametext_block_action; run the wholeyutovo-desktop_testwith a real display —TestFiles::testCopyPasteGraphaborts offscreen), webtest/text_block.spec.js(utils.insertTextBlock; click the button only after the page settled — a click right aftersetLanguagemisses while the language list is closing).
3D Surface Graph
ElementType::GRAPH_SURFACE(appended at the end of the enum). ClassGraphSurface : public Graphlives insrc/formulas/graph.h/.cppnext toGraphLineand reuses theGraphbase (ranges,SetNumber, MathGL instance, solving/error state).- Children (in caret traversal order): 0 y_top
CodeRow, 1 expressionsCodeParagraphsBlock(one paragraph = one surface, like plots inGraphLine), 2 y variable, 3 y_bottom, 4 x_left, 5 x variable, 6 x_right, 7Shape(can_resize/can_move_picture). Layout: left column top→bottom = y_top, [expressions block | y variable side by side at mid height], y_bottom; bottom row = x_left, x variable (center), x_right. The y variable row sits right of the expressions block at the middle of the height. The 2D getters fromGraphuse different indices, soGraphSurfaceshadowsGetYVariable/GetYBottom/GetXLeft/GetVariable/GetXRight/GetShapeand overridesInit/Remake/MovePicture/ZoomPicture/UpdateLevel.ZoomPicturemust use the surface getters — the base version writes the bound rows by the 2D child indices.AfterFromJsonvalidates the restored child types (CODE_ROW / CODE_PARAGRAPHS_BLOCK / CODE_ROW×5 / SHAPE) and returns false on a mismatch - a wrong structure turns into a clean load error (document not parsed), never a crash. Do not try to reorder children withElements::Remove/Insertduring load -Removeresolves the position by id and corrupts the tree when the ids do not match.ToTextkeeps the public ordergraph_surface(y_top, expr, y_bottom, x_left, x_var, x_right, y_var)regardless of the child order (it is also called whileInitbuilds the children - guardCount() < 8). - Rendering (MathGL):
graph.Rotate(rot_x, rot_z)(defaults 50/60, stored in the element and serialized asrot_x/rot_z), z-range computed from the finite samples,SetRanges(x_left, x_right, y_bottom, y_top, z_min, z_max),Box+Axis("xyz")+Grid("xyz"), then per plot byPlotFormat::style:SurfaceStyle::HEIGHT→Surf(z, "")(gradient by height),UNIFORM→Surf(z, "{xRRGGBB}"),HEIGHT_MESH→Surf(z, "#"),WIREFRAME→Mesh(z, "{xRRGGBB}-<width>"),POINTS→Surf(z, "{xRRGGBB}.<width clamped to 1..9>")— the point size comes from the pen width digit of the scheme (mglCanvas::mark_plot:type=='.'usesPenWidth, notMarkSize).Color::ToRGB()already returns the MathGL"xRRGGBB"form. The grid ismglData::Set(z.data(), nx, ny)— x index fastest, matching the calculator layout. - Mouse: drag rotates the view (
MovePicture(dx, dy, shift=false)changes the angles, redraw only, no re-solve; the vertical rotation is inverted so the surface follows the mouse - down means tilt down); Shift+drag pans the x/y ranges so the surface follows the cursor by the mouse distance (rewrites the bound rows viaSetNumber, re-solves); wheel zooms (GraphSurface::ZoomPictureoverride). The shift flag is plumbed throughElement::MovePicture(dx, dy, bool shift=false)→MovePictureTask→Document::MouseMove(x, y, shift=false)→ front-ends (desktopmouseMoveEventpassesQt::ShiftModifier, webOnMouseMovepassesmouse_event->shiftKey). - The mouse tasks (
MovePictureTask,ZoomPictureTask,ResizeElementTask) have no undo, but they change saved content (ranges,rot_x/rot_z,format.size) - each of them callsDocument::SetLastModifyTaskId(Task::id)after executing soUpdateChanged()marks the document modified (Task::idmust be qualified because these tasks shadowidwith theirElementId idmember).Document::UpdateChangedmust NOT forcechanged = falsewhensave_task_id == 0 && undo_tasks.empty()- a loaded document keeps exactly that state, and the forced branch silently swallowed every non-undoable modification of it. - The 3D Shift-pan converts the pixel delta into data units through the current projection (
GraphSurface::MovePicture): it draws the surfaces into the MathGL frame, finalizes it withGetRGBA()(the z buffer is only filled during rasterization), samplesCalcXYZat the plot center and ±10px around it, and combines the samples linearly. A bareCalcXYZwithout a drawn z buffer unprojects onto the most visible coordinate plane (the x component is identically 0 at the default rotation) - that is why the frame must be drawn first. - Data protocol: no new
ResultType— the graph solves asARRAY_REAL.GraphSurface::Solve()sendsgraph_surface(expr, x_var, y_var, x_left, x_right, y_bottom, y_top, nx, ny)per expression paragraph, nx/ny = clamp(shape_size/8, 10..80). InPutResultvalues[0..3] are the bounds, values[4]/[5] are nx/ny, the rest are z values row-major (x fastest); failed points are NaN (holes in the surface). - Soft degradation while editing:
Solve()does not dispatch while any of the six bound/variable rows or any expression paragraph is empty (the incompletegraph_surface(...)string would be a parser error); it resets plots/error state and shows the empty frame instead.ReSolve()early-returns whilelast_expressionsis empty so no waiting~is shown.Document::InsertGraphSurfaceprefills the variable rows withx/yCodeStrings so the template fields label themselves ("y" above the y bounds in the left column, "x" in the bottom row center); a caret entering a prefilled row stops before the string, so passing through a variable row takes threeMoveCaretRightpresses (enter / cross the string / exit).GraphSurface::ToHtml(andGraphLine::ToHtml) read the marker color bounds-safely — before the first solveplotsmay be shorter than the paragraph list, the fallback color isdefault_colors[i % size]. - yutovo-calculator: grammar rule
surface_graphinsrc/graph.h(connected intograph = line_graph | bar_graph | surface_graph), AST nodeSurfaceGraphNodeinsrc/ast.h— a new graph node needs aBOOST_FUSION_ADAPT_STRUCTfor every number type (8 adaptors, next to theGraphNodeadaptors) andAnnotationoverloads insrc/annotation.h(both the main functor andOperandVisitor), otherwise parser.cpp fails to compile. TheSolver<Array<Real>>specialization insrc/solver.cppsamples the edge-inclusive grid with both loop variables as temp variables; declare the specialization at the bottom ofsolver.h. Tests:CalcTestArrayReal.surface_graph1..4intest/array.cpp. PlotFormatgainedSurfaceStyle style(serialized as"style"insideplot_format, default 0 — backward compatible);GetPlotFormat/SetPlotFormatare virtual onGraphnow (GetPlotFormat(pos, PlotFormat&)is used byCodeParagraph::AfterInsertfor the█marker color), andDocument::Get/SetGraphFormat,Get/SetPlotFormat,GetGraphImageaccept bothGRAPH_LINEandGRAPH_SURFACE.Paragraph::SetMarkerresets the cachedmarker_draw_format— without the reset the marker keeps drawing with the old color after a plot color change (the cache is otherwise only cleared byRescale).- Registration points:
editor_utils.cppFromJson map (GRAPH_SURFACE→GraphSurface::FromJson),UndoGraphSurface+ factory case inundo.cpp,ResultTask::Executedefault branch (save the element id before the firstFindElementOrParent— the pointer is reassigned to nullptr on the first miss),ResolveDependenciesTask'sresolve_graphslambda collects both graph types. - Tests:
test/graph_surface.cpp(FormulaTest.graph_surface1..18: ToHtml/caret/undo, sampled values (grid step = range/(n-1), edge-inclusive), two surfaces, rotation without re-solve, Shift-pan, wheel zoom row texts, graph/plot formats, error marks, save/load with rotation angles, NaN cells, user-function dependency re-solve, partially filled template does not solve and shows no error, caret traversal right/left through the fields in their visual order, the mouse movement tests also verifyIsChanged(save -> unchanged -> drag/pan/wheel -> changed), a loaded document becomes changed after a mouse drag, Shift-drag pans through the current projection). To edit the assignment in the dependency test, navigate withMoveCaretHome×2 +MoveCaretUp+MoveCaretEnd+MoveCaretLeftfrom the last graph field (a coordinate click can land on the solved result row instead of the right part). Note the caret granularity: entering a row stops at an element boundary and every typed character is its ownCodeString(nothing merges, not even with the prefilled "x"/"y"), so the exact number of arrows between two fields depends on the current row contents - collect the counts fromGetEditorState().ToString()likegraph_surface15does. - Front-ends: desktop "Graphs" toolbar action
graph_surface_action(MainWindow::GraphSurface, iconimages/graphs/graph_surface.png) + context menu "Graph format"/"Copy image" now findGRAPH_SURFACEtoo +PlotFormatDialoggained a Style combo (hidden for line graphs via thesurfacector flag); web#graph-surface-button→OnGraphSurface,PlotFormatDialogEM_JS/OnPlotFormatcarry the style,IsGraph()/GetGraphImage()accept both types (icon copied tosrc/site/public/images/graphs/). Desktop testTestToolbar::testGraphSurface; webtest/graph_surface.spec.js(utils.insertGraphSurface).
Histogram Graph
ElementType::GRAPH_HISTOGRAM(appended at the end of the enum). ClassGraphHistogram : public Graphinsrc/formulas/graph.h/.cpp— draws an array as bars: one array element = one bar, x axis = element index (1..n), y = value. No newResultType— solves asARRAY_REAL.- Children (only 2): 0 expressions
CodeParagraphsBlock(one paragraph = one bar series, like plots inGraphLine), 1Shape. Shadowed gettersGetExpression()→0,GetShape()→1 (the baseGraphgetters use the 2D indices).AfterFromJsonvalidates both child types strictly (CODE_PARAGRAPHS_BLOCK with CODE_PARAGRAPH children + SHAPE) and returns false on a mismatch. The shape hascan_move_picture = false— the bars are placed by their indices, so dragging/wheel do nothing (bothDocument::MouseMove/MouseWheelgate oncan_move_picture;MovePicture/ZoomPictureare empty overrides as a safety net), whilecan_resize = truestill works.UpdateLevelmust be overridden — the base version walks the 2D bound-row getters which do not exist here. - Data protocol (
ARRAY_REAL,graph_barfunction):GraphHistogram::Solve()sendsgraph_bar(expr)per expression paragraph. InPutResultvalues[0..3] are the per-series bounds header, values[4] is the number of bars, values[5..] are the bar heights; NaN elements stay NaN (holes between the bars). yutovo-calculator computes the per-series bounds:x_left=0.5,x_right=n+0.5,y_bottom=min(0, values)(bars grow from the zero line),y_top=max(0, values), and a degenerate range falls back toy_top=y_bottom+1; an empty array throwsIncorrectOperation.PutResultignores the header bounds and recomputes the graph bounds locally as the union of all series (y_bottom/y_topover every value of every plot,x_right = max_n + 0.5) — the header of the last arriving result would clip the other series; all series of one graph must share a single scale, while separate graph elements scale independently (the standard behavior of separate charts). - yutovo-calculator: the
bar_graphrule insrc/graph.hwas changed tograph_bar(expression)(the old unusedpoints_countargument and theBarGraphNode::points_countfield + its 8 adaptors were removed) and got its missingon_successannotation hook. TheSolver<Array<Real>>specialization lives insrc/solver.cpp(declared at the bottom ofsolver.hnext to the Line/Surface ones); it evaluates the expression once (no temp variables) and requiresSize() > 0. Tests:CalcTestArrayReal.graph_bar1..6intest/array.cpp. - Soft degradation while editing:
Solve()does not dispatch while any expression paragraph is empty; it resets plots/error state and shows the empty frame instead.ReSolve()early-returns whilelast_expressionsis empty so no waiting~is shown. One plot is created lazily at construction for the paragraph marker color (CodeParagraphsBlock::AddEmptyElementnow colors markers for all three graph types through the virtualGraph::GetPlotFormat(pos, PlotFormat&)), so an empty template hasplots.size() == 1withplots[0].yempty. - Rendering (MathGL): same frame setup as
GraphLine(SetRanges+Axis("xy")+Grid), then per plot byPlotFormat::histogram_style:HistogramStyle::BARS(default) →Bars(x, y, "{color}");BARS_LINE→Bars+Plot(x, y, "{color}-<width>")connecting the tops;BARS_SOLID→BarswithSetBarWidth(1.)(restored to 0.7 after);STEM→Stem(spectrum lines);AREA→Area(filled to the zero line);STEP→Step(stairs);MARKS→Plot(x, y, "{color}.<width clamped 1..9>")(points, no "-" in the pen so no line is drawn). Two frame rules:graph.SetOrigin(x_left, 0)must be called afterSetRanges— MathGL draws bars/stems/areas from the axis plane, whose default is the bottom of the range, so without it a negative value draws no bar, andAxisdraws the axis lines at the origin, so a middle-x origin would put the y axis in the middle of the plot. The drawn x positions must be the explicit integer array1..n(buildmglData x_dataand use the(x, y)overloads) — MathGL's automatic x for 1D data spreads the points over the whole range instead of putting them on the integer ticks. The█marker click opens the plot format dialog (MouseLButtonHoldlikeGraphLine);GraphHistogram::SetPlotFormatassigns the wholePlotFormatincluding the style. - Bar styles live in their own
HistogramStyleenum (BARS = 0..MARKS, the default first likeSurfaceStyle::HEIGHT) stored in the separatePlotFormat::histogram_stylefield and serialized as its own"histogram_style"key ofplot_format(both style keys are always written; each graph kind reads only its own field, so no variant/any is needed - the graph type never switches).PlotFormat::FromJsonalso migrates the interim format: interim builds serialized the histogram styles inside"style"starting at 8 - values 8..14 are moved tohistogram_style(value - 8,histogram19covers it). The graph format dialog color paints only the axes and the grid (like inGraphLine) — the bar series colors are controlled solely by the per-series plot format dialog (the█marker click). Undo of a format change replaces the element - re-find it before asserting onplots(the old pointer stays on the detached element). - Registration points (add
GRAPH_HISTOGRAMnext toGRAPH_LINE/GRAPH_SURFACE):editor_utils.cppFromJson map,UndoGraphHistogram+ factory case inundo.cpp,ResultTask::Executedefault branch,ResolveDependenciesTask'sresolve_graphs,Document::InsertGraphHistogram+ the five gates inGet/SetGraphFormat/Get/SetPlotFormat/GetGraphImage(all five go throughDocument::IsGraph(ElementPtr/ElementId)),CodeParagraph::AfterInsertparent chain.CodeParagraphsBlock::AddEmptyElementchecks the parent type locally — during graph construction the element is not in the document tree yet, so theDocument::IsGraphoverloads resolving through the tree cannot see it (the marker would lose its color).ToText()isgraph_bar(<expr>); multiple paragraphs are joined with\nby the block. - Tests:
test/graph_histogram.cpp(FormulaTest.histogram1..19: ToHtml/caret/undo,[1,5,3,2]values + bounds, negatives, two bar series, NaN holes, error marks, array-assignment dependency re-solve — the caret edit inside[...]needsMoveCaretEnd+ twoMoveCaretLeft(a]does not merge with strings, oneLeftstill stops after it), plot/graph format + undo, save/load, empty template does not solve, mouse drag/wheel do nothing, pixel tests viagraph->graph.GetRGBA()afterGetImage— the negative-value bar must grow from the zero line (histogram13also checks the positive bar does not extend below zero; a fully negative array must fill the plot,histogram14), style save/load roundtrip (histogram15), the graph format color leaves the series colors untouched (histogram16), two series share the union scale (histogram17), the stems sit on the integer ticks and the y axis stays at the left edge (histogram18, both via pixel checks withgrid_width = 0- exclude the bottom rows from the "empty middle" check, the x axis tick marks stick up from the axis line), the interim"style"8..14 values migrate tohistogram_style(histogram19, a handwrittenLoadJsondocument - build the json with a script, a hand-wrapped literal easily breaks). Arrays are typed withInsertOpenSquareBracket/InsertComma/InsertCloseSquareBracket. - Front-ends: desktop "Graphs" toolbar action
graph_histogram_action(MainWindow::GraphHistogram, iconimages/graphs/graph_histogram.png) + context menu "Graph format"/"Copy image" findGRAPH_HISTOGRAM; web#graph-histogram-button→OnGraphHistogram. Web graph lookups must useDocument::FindCurrentGraph()(IsGraph/GetGraphFormat/GetGraphImage/OnGraphFormat/OnPlotFormatfallback): a right click on the graph picture puts the caret on the non-editableShape,Caret::GetElementthen returns the graph itself andFindCurrentParentByTypeskips it (it starts from the parent of the given element) — with the plain parent chainGetGraphFormatreturns an empty string, the vueJSON.parse('')throws and the dialog never opens.PlotFormatDialogtakes a secondhistogramflag (desktop ctor(PlotFormat&, surface, histogram)reading/writinghistogram_style; web EM_JS/vue passhistogram, the style value/combo index is the enum value directly -OnPlotFormatwriteshistogram_styleforGRAPH_HISTOGRAMandstyleotherwise) and shows the bar style items for histograms, surface items for surfaces, nothing for line graphs. Desktop testTestToolbar::testGraphHistogram(run the wholeyutovo-desktop_test— passing a singleTestToolbar::...filter makes the firstqExecofTestFilesexit the process); webtest/graph_histogram.spec.js(utils.insertGraphHistogram); editor-side regressionFormulaTest.histogram20(click on the image →FindCurrentGraphresolves the graph). Rebuild order: yutovo-calculator wasm → yutovo-editor wasm (build_web/debugwithemcmake) → yutovo-web, each installed into$YUTOVO_DEPLOYfirst.
Graph Axis Settings
- Per-graph axis settings shared by all three graph types:
AxisFormatstruct (color,width,ticks) insrc/style.h, stored asGraphFormat::axisand serialized as a nested"axis"object inside"graph_format"(style.cpp). A missing"axis"key (old documents) loads with defaults = the previous behavior. The oldGraphFormat::coloris now the grid color only; the axis color is separate (default Black like before). Hiding the axes is done bywidth == 0(there is no separate show flag); a logarithmic axis scale was tried and removed — do not reintroduceSetFuncfor it without handling the MathGL pitfalls (the transform survivesNewFrameand breaks the~/errorPuts(mglPoint(0,0))branches and the surface Shift-pan unprojection unless reset everywhere). - Drawing goes through the shared protected
Graph::DrawAxes(dirs, box, z_min, z_max)(src/formulas/graph.cpp), called from the threeInit()draw lambdas:GraphLine/GraphHistogrampass"xy"(no box),GraphSurfacepasses"xyz"with the box. MathGL pitfalls:SetTickLen(0)is not zero — MathGL substitutes the default 0.02 — so hidden ticks use a subpixel length1e-4. Zero axis width draws nothing on a surface either —Boxis skipped too; ticks hidden passBox(pen, false). - Front-ends: desktop
GraphSettingsDialog(graph_settings_dialog.ui— buttonaxis_color+ slotOnAxisColorClicked, spinaxis_width0..10 (0 hides the axis lines), checkboxaxis_ticks; the "Color" label became "Grid color"), testTestToolbar::testGraphFormatAxes(constructs the dialog directly, noexec). Web:GetGraphFormatadds an"axis"object to the JSON,OnGraphFormattakes 7 positional args,GraphFormatDialog.vuehas the color-button/input/checkbox (#axis-width), testtest/graph_axes.spec.js. Both link the prebuilt editor — reinstall yutovo-editor (native + wasm) into$YUTOVO_DEPLOYfirst. - Tests:
test/graph.cppgraphs22(roundtrip/undo/redo/save/load + old document defaults),graphs23(pixel checks: hidden ticks reduce the black count, zero width leaves no black pixels inside the plot area — setgrid_width = 0first so only the axes are black),test/graph_surface.cppgraph_surface19,test/graph_histogram.cpphistogram21. - The resize frame of a shape is drawn in
Shape::Drawonly whilecaret->IsVisible(); front-ends hide the caret on focus loss (DocumentWidget::focusOutEvent/ web focus handlers viaSetCaretVisible). A format dialog appliesSetGraphFormatafter exec returns, and on X11 the focus-in event can arrive later — the format redraw then runs with the hidden caret and erases the frame permanently.MoveCaretTask(dirNONE, task.cpp) therefore enqueues adocument->Redraw(text->id, false)when the caret transitions hidden→visible; covered byFormulaTest.graphs24(counts theDrawRectcalls of the frame in the window mock).
Code Style
Parenthesized expressions
Keep the contents of parentheses (function argument lists, conditions, initializers, etc.) on a single line when it fits. Only wrap to a new line if the expression would exceed 140 columns. When a parenthesized expression is wrapped, each continuation line uses the normal 4-space indent; do not align arguments with the opening parenthesis.
// CORRECT
void ShortFunction(int a, int b, int c);
void LongFunctionName(const std::u32string& first_argument, const std::u32string& second_argument,
int third_argument);
auto result = SomeFunction(first_argument, second_argument,
third_argument, fourth_argument);
if (condition_a && condition_b)
{
// ...
}
// WRONG
void LongFunctionName(
const std::u32string& first_argument,
const std::u32string& second_argument,
int third_argument);
void LongFunctionName(const std::u32string& first_argument,
const std::u32string& second_argument,
int third_argument);
try {
// ...
} catch (...) {
// ...
}
Spaces around brackets
Do not put spaces before or after square brackets [] and round brackets ():
// CORRECT
int arr[10];
void foo(int a);
arr[0] = foo(1);
// WRONG
int arr [10];
void foo (int a);
arr [0] = foo (1);
Comments
Comments that precede a function, method, or class (header/descriptive comments above the definition) start with a capital letter, e.g. //Group of code paragraphs, //TextEquation, //Insert a text block into the document text.
Lambdas
Place the capture clause on a new line, indented by 4 spaces. Parameters, the -> return type, and the opening brace follow the normal rules: parameters and return type stay on the same line as the capture clause, and the opening brace goes on its own line.
// CORRECT
auto callback =
[](int value) -> bool
{
return value > 0;
};
auto reference =
[&]() -> void
{
DoWork();
};
// WRONG
auto callback = [](int value) -> bool {
return value > 0;
};
auto callback =
[](int value) -> bool {
return value > 0;
};
Editor Test Patterns
Test fixture declarations
Declare new test fixtures (struct XTest : DocumentTest, SolverTest, ...) in test/mock.h next to the other corresponding fixture declarations — do not declare them in the individual test/*.cpp file (e.g. TextBlockTest lives in mock.h next to ArrayTest/VariablesTest, not in text_block.cpp). Shared helpers of a fixture are methods of that fixture in mock.h.
Checking the caret state
Editor tests must also verify the caret position - add ASSERT_TRUE(document.GetEditorState() == MakeEditorState({0, 0, 0, ...})) << document.GetEditorState().ToString(); after each full ToHtml() check (see TextBlockTest.text_block16). The braced list is the ElementId path of the caret; collect actual values from GetEditorState().ToString() (positions before [).
Entering expressions in code blocks
Do not put operators inside InsertString:
// WRONG
document.InsertString("x+1", true);
// CORRECT
document.InsertString("x", true);
document.InsertPlus(true);
document.InsertString("1", true);
Use dedicated insert methods for operators:
InsertPlus(true)/InsertMinus(true)InsertMultiply(true)/InsertDivision(true)InsertPower(true)— wraps the preceding string element into the base automatically; caret moves to exponent.InsertOpenRoundBracket(true)/InsertCloseRoundBracket(true)InsertComma(true)
Power behavior
InsertPower(true) after a string moves that string into the base and places the caret in the exponent.
Example:
document.InsertString("x", true);
document.InsertPower(true);
document.InsertString("2", true);
// Parser text: pow(x,2)
Checking results with ToText()
For AUTO-mode symbolic fallback tests, ToText() is sufficient:
ASSERT_TRUE(document.ToText() == U"x+1=1+x") << ToBasicString(document.ToText());
Checking results with ToHtml()
In editor tests verify the document by comparing the full ToHtml() instead of spot checks (ToText(), substring searches, element counting) - the complete MathML distinguishes solved results (ResultRow wrapping), waiting symbols, formulas vs plain strings, and nested blocks. Check full ToHtml() exactly like SolverSymbolicTest::solver1:
ASSERT_TRUE(document.ToHtml() ==
"<body>"
"<p>"
"<math xmlns='http://www.w3.org/1998/Math/MathML'>"
"<mrow>"
"<mrow>"
"<mi>x</mi>"
"<mo>+</mo>"
"<mi>1</mi>"
"</mrow>"
"<mo>=</mo>"
"<mrow>"
"<mrow>"
"<mi>1</mi>"
"<mo>+</mo>"
"<mi>x</mi>"
"</mrow>"
"</mrow>"
"</mrow>"
"</math>"
"</p>"
"</body>") << document.ToHtml();
HTML structure:
CodeParagraph→<math xmlns='http://www.w3.org/1998/Math/MathML'>CodeRow→<mrow>CodeString→<mi>text</mi>Plus/Minus→<mo>+</mo>/<mo>-</mo>Multiply→<mo>×</mo>(not<mo>*</mo>)Power→<msup><mrow>base</mrow><mrow>exp</mrow></msup>SymbolicResult(inside lastCodeRow) produces<mrow><mrow>elements...</mrow></mrow>becauseResultRow(CodeColumn) wraps itsCodeRowcontent.
Undo checks
If the test calls Undo(), always assert the document state after undo:
document.Undo();
document.WaitUndo();
std::this_thread::sleep_for(200ms);
ASSERT_TRUE(document.ToHtml() == "<body>...</body>") << document.ToHtml();
Undo/Redo in editing tests
Any test that edits an element (typing, inserting subformulas, clearing contents, etc.) must verify both Undo() and Redo(). Wait for each operation and assert the document state:
document.Undo();
document.WaitUndo();
std::this_thread::sleep_for(200ms);
ASSERT_TRUE(document.ToText() == U"...") << ToBasicString(document.ToText());
document.Redo();
document.WaitRedo();
std::this_thread::sleep_for(200ms);
ASSERT_TRUE(document.ToText() == U"...") << ToBasicString(document.ToText());
Async operations and timeouts
Editor and caret methods return a task id, but the editor runs a single thread and executes the commands sequentially - do not put extra document.WaitTask() calls in tests. Wait only before document.WaitSolver() (wrap the InsertEquation/InsertAssignment call that starts solving) and before checking the result (wrap the last command of a sequence, e.g. Copy before using the returned clipboard json, Save before reading the file or Loading it, LoadJson before checks). Do not rely only on std::this_thread::sleep_for.
Changing config
Do not mutate document.config directly and then call document.SetConfig(document.config, false) — SetConfigTask compares the passed config with the current document.config, so a direct mutation makes the comparison see no change. Instead, copy document.config into a local Config, modify the copy, and pass it to SetConfig.
Emulate user input when building formulas in tests
Editor/solver tests should construct expressions the same way a user would — by calling the document's Insert* APIs — not by allocating Division/Power/CodeString objects directly and attaching them with InsertFormula. The desktop code path performs caret placement, merging, remake and other logic that direct construction skips, so tests built from raw elements can pass in the test binary but fail on desktop.
// WRONG
Division* div = new Division(&document);
// ... manually populate numerator/denominator ...
document.WaitTask(document.InsertFormula(div, true));
// CORRECT
document.WaitTask(document.InsertDivision(true));
document.InsertString("d", true);
document.InsertPower(true);
document.InsertString("2", true);
document.InsertString("g", true);
document.InsertOpenRoundBracket(true);
document.InsertString("x", true);
document.InsertComma(true);
document.InsertString("y", true);
document.InsertCloseRoundBracket(true);
document.WaitTask(document.MoveCaretDown(false));
document.InsertString("d", true);
document.InsertString("x", true);
document.InsertString("d", true);
document.InsertString("y", true);
Shared test helpers (e.g. SolverAutoTest::CreateDerivativeDivision in test/mock.h) must also emulate input rather than construct elements by hand.
Result-type-specific tests belong in the matching solver file
Tests that explicitly set a non-AUTO result type should be placed in the corresponding test file:
ResultType::REAL→test/solver_real.cpp(SolverRealTest)ResultType::INTEGER→test/solver_integer.cpp(SolverIntegerTest)ResultType::RATIONAL→test/solver_rational.cpp(SolverRationalTest)ResultType::COMPLEX→test/solver_complex.cpp(SolverComplexTest)ResultType::ARRAY_REAL→test/solver_array_real.cpp(SolverArrayRealTest)ResultType::SYMBOLIC_REAL/SYMBOLIC_RATIONAL/SYMBOLIC_COMPLEX→test/solver_derivative.cpp/test/solver_symbolic.cpp/ etc., depending on the feature.
AUTO-mode tests and generic derivative tests live in test/solver_derivative.cpp (SolverAutoTest/SolverSymbolicTest).
File formats
.yutfiles are gzip-compressed json (not plain text, not a zip archive):gunzip -c file.yutshows the json. In release the json is compact, in debug it is pretty printed and saved uncompressed (Documentctors setcompressed_file = falseunderDEBUG;LoadTaskre-enables compression only in release builds).
Build
Projects are built and tested in build/debug/. Build with the number of physical cores, not nproc — nproc counts hyperthreads too and -j on them makes the build slower and the machine unresponsive. The physical core count is lscpu -p=Core,Socket | grep -v '^#' | sort -u | wc -l (16 on the 5950X, where nproc says 32):
cd build/debug && cmake ../.. && make -j$(lscpu -p=Core,Socket | grep -v '^#' | sort -u | wc -l) yutovo-editor_tests && ./test/yutovo-editor_tests --gtest_filter="FormulaTest.power23"
Run tests from build/debug, not from build/debug/test
Tests that load documents use paths relative to the current working directory, e.g. ../../test/tests/solver42.yut. Run the binary as ./test/yutovo-editor_tests ... with the working directory build/debug — from build/debug/test those paths resolve to a nonexistent build/test/tests/ and the test fails with File not open and then hangs forever in WaitLoad/WaitSolver (observed with solver42 and lists1); it looks like a product hang but is a runner error.
Testing
Full test suite
Before running the full yutovo-editor_tests suite, ask the user at the beginning of the task (after planning) whether to run the complete suite or only targeted tests.
Debug giac linkage
yutovo-editor creates its own imported giac_imported target in src/CMakeLists.txt. Debug builds must use ${INSTALL_PATH}/lib/libgiacd.a and release builds ${INSTALL_PATH}/lib/libgiac.a; linking a debug yutovo-calculator/yutovo-solver against the release giac library causes an ABI mismatch and memory corruption inside giac (e.g., CodeTest.code35 crashing in giac::expand).
Network Errors
If an operation fails with a "Network connection failed" error:
- Wait 2 seconds and retry automatically on your own.
- If the retry still fails, continue retrying with a 10-second interval.
Repowise
Use repowise tools (e.g. mcp__repowise__get_overview, mcp__repowise__get_context, mcp__repowise__get_risk, mcp__repowise__get_answer) for codebase exploration, risk analysis, and architecture understanding.
