Step 145: add Org-mode rendering and source blocks
This commit is contained in:
239
editor/src/OrgMode.h
Normal file
239
editor/src/OrgMode.h
Normal file
@@ -0,0 +1,239 @@
|
||||
#pragma once
|
||||
#include "imgui.h"
|
||||
#include "CodeEditorWidget.h"
|
||||
#include "SyntaxHighlighter.h"
|
||||
#include "EditorMode.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <sstream>
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
|
||||
enum class OrgBlockType {
|
||||
Heading,
|
||||
Paragraph,
|
||||
SourceBlock
|
||||
};
|
||||
|
||||
struct OrgBlock {
|
||||
OrgBlockType type = OrgBlockType::Paragraph;
|
||||
int level = 0;
|
||||
std::string title;
|
||||
std::string text;
|
||||
std::string language;
|
||||
std::string code;
|
||||
std::string result;
|
||||
};
|
||||
|
||||
struct OrgDocumentState {
|
||||
std::string source;
|
||||
std::vector<OrgBlock> blocks;
|
||||
std::vector<CodeEditorWidget> editors;
|
||||
std::vector<EditorMode> modes;
|
||||
std::vector<int> sourceBlockMap;
|
||||
bool dirty = true;
|
||||
};
|
||||
|
||||
static inline std::string orgTrim(const std::string& s) {
|
||||
size_t start = s.find_first_not_of(" \t");
|
||||
if (start == std::string::npos) return "";
|
||||
size_t end = s.find_last_not_of(" \t");
|
||||
return s.substr(start, end - start + 1);
|
||||
}
|
||||
|
||||
static inline bool orgStartsWith(const std::string& line, const std::string& prefix) {
|
||||
return line.rfind(prefix, 0) == 0;
|
||||
}
|
||||
|
||||
static std::vector<OrgBlock> parseOrgBlocks(const std::string& text) {
|
||||
std::vector<OrgBlock> blocks;
|
||||
std::vector<std::string> paragraphLines;
|
||||
std::vector<std::string> codeLines;
|
||||
bool inSrc = false;
|
||||
std::string srcLang;
|
||||
|
||||
auto flushParagraph = [&]() {
|
||||
if (paragraphLines.empty()) return;
|
||||
OrgBlock block;
|
||||
block.type = OrgBlockType::Paragraph;
|
||||
std::ostringstream oss;
|
||||
for (size_t i = 0; i < paragraphLines.size(); ++i) {
|
||||
if (i) oss << "\n";
|
||||
oss << paragraphLines[i];
|
||||
}
|
||||
block.text = oss.str();
|
||||
blocks.push_back(std::move(block));
|
||||
paragraphLines.clear();
|
||||
};
|
||||
|
||||
auto flushSource = [&]() {
|
||||
OrgBlock block;
|
||||
block.type = OrgBlockType::SourceBlock;
|
||||
block.language = srcLang.empty() ? "text" : srcLang;
|
||||
std::ostringstream oss;
|
||||
for (size_t i = 0; i < codeLines.size(); ++i) {
|
||||
if (i) oss << "\n";
|
||||
oss << codeLines[i];
|
||||
}
|
||||
block.code = oss.str();
|
||||
blocks.push_back(std::move(block));
|
||||
codeLines.clear();
|
||||
srcLang.clear();
|
||||
};
|
||||
|
||||
std::istringstream ss(text);
|
||||
std::string line;
|
||||
while (std::getline(ss, line)) {
|
||||
if (inSrc) {
|
||||
if (orgStartsWith(orgTrim(line), "#+end_src")) {
|
||||
inSrc = false;
|
||||
flushSource();
|
||||
} else {
|
||||
codeLines.push_back(line);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string trimmed = orgTrim(line);
|
||||
if (trimmed.empty()) {
|
||||
flushParagraph();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (orgStartsWith(trimmed, "#+begin_src")) {
|
||||
flushParagraph();
|
||||
std::string rest = trimmed.substr(std::string("#+begin_src").size());
|
||||
srcLang = orgTrim(rest);
|
||||
inSrc = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!trimmed.empty() && trimmed[0] == '*') {
|
||||
flushParagraph();
|
||||
size_t level = 0;
|
||||
while (level < trimmed.size() && trimmed[level] == '*') ++level;
|
||||
std::string title = orgTrim(trimmed.substr(level));
|
||||
OrgBlock block;
|
||||
block.type = OrgBlockType::Heading;
|
||||
block.level = (int)level;
|
||||
block.title = title;
|
||||
blocks.push_back(std::move(block));
|
||||
continue;
|
||||
}
|
||||
|
||||
paragraphLines.push_back(line);
|
||||
}
|
||||
|
||||
if (inSrc) {
|
||||
flushSource();
|
||||
}
|
||||
flushParagraph();
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
static std::string buildOrgText(const std::vector<OrgBlock>& blocks) {
|
||||
std::ostringstream out;
|
||||
for (size_t i = 0; i < blocks.size(); ++i) {
|
||||
const auto& block = blocks[i];
|
||||
if (block.type == OrgBlockType::Heading) {
|
||||
out << std::string(std::max(1, block.level), '*') << " " << block.title << "\n";
|
||||
} else if (block.type == OrgBlockType::Paragraph) {
|
||||
out << block.text << "\n";
|
||||
} else if (block.type == OrgBlockType::SourceBlock) {
|
||||
out << "#+begin_src " << (block.language.empty() ? "text" : block.language) << "\n";
|
||||
out << block.code << "\n";
|
||||
out << "#+end_src\n";
|
||||
if (!block.result.empty()) {
|
||||
out << "#+RESULTS:\n" << block.result << "\n";
|
||||
}
|
||||
}
|
||||
if (i + 1 < blocks.size()) out << "\n";
|
||||
}
|
||||
return out.str();
|
||||
}
|
||||
|
||||
static void updateOrgDocument(OrgDocumentState& state, const std::string& sourceText) {
|
||||
if (!state.dirty && sourceText == state.source) return;
|
||||
state.source = sourceText;
|
||||
state.blocks = parseOrgBlocks(sourceText);
|
||||
state.sourceBlockMap.clear();
|
||||
state.editors.clear();
|
||||
state.modes.clear();
|
||||
for (size_t i = 0; i < state.blocks.size(); ++i) {
|
||||
if (state.blocks[i].type == OrgBlockType::SourceBlock) {
|
||||
state.sourceBlockMap.push_back((int)i);
|
||||
state.editors.emplace_back();
|
||||
state.modes.emplace_back(state.blocks[i].language);
|
||||
}
|
||||
}
|
||||
state.dirty = false;
|
||||
}
|
||||
|
||||
static bool renderOrgDocument(OrgDocumentState& state,
|
||||
const std::string& sourceText,
|
||||
bool showWhitespace,
|
||||
ImFont* monoFont,
|
||||
ImFont* uiFont,
|
||||
const std::function<void(const std::string&)>& onOrgTextChanged,
|
||||
const std::function<void(int, const std::string&, const std::string&)>& onBlockChanged,
|
||||
const std::function<std::string(const std::string&, const std::string&)>& onRunBlock) {
|
||||
updateOrgDocument(state, sourceText);
|
||||
bool changed = false;
|
||||
|
||||
int srcIndex = 0;
|
||||
for (size_t i = 0; i < state.blocks.size(); ++i) {
|
||||
auto& block = state.blocks[i];
|
||||
ImGui::PushID((int)i);
|
||||
if (block.type == OrgBlockType::Heading) {
|
||||
ImGui::PushFont(uiFont ? uiFont : ImGui::GetFont());
|
||||
ImGui::Text("%s%s", std::string(block.level, '#').c_str(), block.title.c_str());
|
||||
ImGui::PopFont();
|
||||
} else if (block.type == OrgBlockType::Paragraph) {
|
||||
ImGui::TextWrapped("%s", block.text.c_str());
|
||||
} else if (block.type == OrgBlockType::SourceBlock) {
|
||||
ImGui::PushFont(uiFont ? uiFont : ImGui::GetFont());
|
||||
ImGui::Text("Source (%s)", block.language.c_str());
|
||||
ImGui::PopFont();
|
||||
|
||||
CodeEditorOptions opts;
|
||||
opts.readOnly = false;
|
||||
opts.showWhitespace = showWhitespace;
|
||||
opts.showLineNumbers = true;
|
||||
EditorMode& mode = state.modes[srcIndex];
|
||||
mode.setLanguage(block.language);
|
||||
opts.mode = &mode;
|
||||
|
||||
auto spans = SyntaxHighlighter::highlight(block.code, block.language);
|
||||
CodeEditorResult result = state.editors[srcIndex].render(
|
||||
"##orgBlockEditor", block.code, spans, opts,
|
||||
ImVec2(0, 180), monoFont ? monoFont : ImGui::GetFont());
|
||||
|
||||
if (result.changed) {
|
||||
changed = true;
|
||||
onBlockChanged((int)i, block.language, block.code);
|
||||
}
|
||||
|
||||
if (ImGui::Button("Run Block")) {
|
||||
block.result = onRunBlock(block.language, block.code);
|
||||
changed = true;
|
||||
}
|
||||
if (!block.result.empty()) {
|
||||
ImGui::SameLine();
|
||||
ImGui::TextDisabled("Result:");
|
||||
ImGui::BeginChild("##orgResult", ImVec2(0, 80), true);
|
||||
ImGui::TextWrapped("%s", block.result.c_str());
|
||||
ImGui::EndChild();
|
||||
}
|
||||
srcIndex++;
|
||||
}
|
||||
ImGui::Spacing();
|
||||
ImGui::PopID();
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
std::string rebuilt = buildOrgText(state.blocks);
|
||||
onOrgTextChanged(rebuilt);
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
Reference in New Issue
Block a user