Step 145: add Org-mode rendering and source blocks
This commit is contained in:
@@ -492,3 +492,4 @@ Sprint 5 in progress. Step 141 (Emacs daemon with user config) done. Next: Step
|
||||
| 2026-02-10 | Codex | Step 142: Emacs package browser panel with loaded/available status, load actions, and package queries via emacsclient. 7/7 tests pass. |
|
||||
| 2026-02-10 | Codex | Step 143: Elisp function discovery via apropos/describe-function, Emacs function indexing into ExternalModule + LibraryIndex, and elisp primitive updates. 7/7 tests pass. |
|
||||
| 2026-02-10 | Codex | Step 144: Emacs keybinding integration (prefix handling, M-x minibuffer, mode line display, key-binding lookup via daemon). 11/11 tests pass. |
|
||||
| 2026-02-10 | Codex | Step 145: Org-mode rendering with headings/blocks, editable source blocks, inline results, org temp runner, and tree-sitter-org integration. 11/11 tests pass. |
|
||||
|
||||
@@ -76,6 +76,13 @@ FetchContent_Declare(
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
|
||||
FetchContent_Declare(
|
||||
tree-sitter-org
|
||||
GIT_REPOSITORY https://github.com/milisims/tree-sitter-org.git
|
||||
GIT_TAG v1.3.1
|
||||
GIT_SHALLOW TRUE
|
||||
)
|
||||
|
||||
FetchContent_Declare(
|
||||
tinyfiledialogs
|
||||
GIT_REPOSITORY https://github.com/native-toolkit/tinyfiledialogs.git
|
||||
@@ -124,6 +131,11 @@ if(NOT tree-sitter-go_POPULATED)
|
||||
FetchContent_Populate(tree-sitter-go)
|
||||
endif()
|
||||
|
||||
FetchContent_GetProperties(tree-sitter-org)
|
||||
if(NOT tree-sitter-org_POPULATED)
|
||||
FetchContent_Populate(tree-sitter-org)
|
||||
endif()
|
||||
|
||||
FetchContent_GetProperties(tinyfiledialogs)
|
||||
if(NOT tinyfiledialogs_POPULATED)
|
||||
FetchContent_Populate(tinyfiledialogs)
|
||||
@@ -204,6 +216,15 @@ add_library(tree_sitter_go STATIC ${_ts_go_src})
|
||||
target_include_directories(tree_sitter_go PUBLIC ${tree-sitter-go_SOURCE_DIR}/src)
|
||||
target_link_libraries(tree_sitter_go PUBLIC unofficial::tree-sitter::tree-sitter)
|
||||
|
||||
# Build tree-sitter-org grammar as a static C library
|
||||
set(_ts_org_src ${tree-sitter-org_SOURCE_DIR}/src/parser.c)
|
||||
if(EXISTS "${tree-sitter-org_SOURCE_DIR}/src/scanner.c")
|
||||
list(APPEND _ts_org_src "${tree-sitter-org_SOURCE_DIR}/src/scanner.c")
|
||||
endif()
|
||||
add_library(tree_sitter_org STATIC ${_ts_org_src})
|
||||
target_include_directories(tree_sitter_org PUBLIC ${tree-sitter-org_SOURCE_DIR}/src)
|
||||
target_link_libraries(tree_sitter_org PUBLIC unofficial::tree-sitter::tree-sitter)
|
||||
|
||||
# tinyfiledialogs (single C file)
|
||||
add_library(tinyfiledialogs STATIC ${tinyfiledialogs_SOURCE_DIR}/tinyfiledialogs.c)
|
||||
target_include_directories(tinyfiledialogs PUBLIC ${tinyfiledialogs_SOURCE_DIR})
|
||||
@@ -794,6 +815,10 @@ add_executable(step144_test tests/step144_test.cpp)
|
||||
target_include_directories(step144_test PRIVATE src)
|
||||
target_link_libraries(step144_test PRIVATE nlohmann_json::nlohmann_json imgui::imgui)
|
||||
|
||||
add_executable(step145_test tests/step145_test.cpp)
|
||||
target_include_directories(step145_test PRIVATE src)
|
||||
target_link_libraries(step145_test PRIVATE nlohmann_json::nlohmann_json imgui::imgui tree_sitter_org)
|
||||
|
||||
find_package(SDL2 CONFIG REQUIRED)
|
||||
find_package(OpenGL REQUIRED)
|
||||
find_package(glad CONFIG REQUIRED)
|
||||
@@ -834,6 +859,7 @@ target_include_directories(whetstone_editor PRIVATE
|
||||
${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/include/imgui
|
||||
${VCPKG_INSTALLED_DIR}/${VCPKG_TARGET_TRIPLET}/include
|
||||
)
|
||||
target_compile_definitions(whetstone_editor PRIVATE WHETSTONE_ENABLE_ORG=1)
|
||||
if(IMGUI_SDL2_BACKEND)
|
||||
get_filename_component(_sdl2_backend_dir "${IMGUI_SDL2_BACKEND}" DIRECTORY)
|
||||
target_include_directories(whetstone_editor PRIVATE "${_sdl2_backend_dir}")
|
||||
@@ -853,7 +879,8 @@ target_link_libraries(whetstone_editor PRIVATE
|
||||
tree_sitter_typescript
|
||||
tree_sitter_java
|
||||
tree_sitter_rust
|
||||
tree_sitter_go)
|
||||
tree_sitter_go
|
||||
tree_sitter_org)
|
||||
target_link_libraries(whetstone_editor PRIVATE tinyfiledialogs)
|
||||
|
||||
add_executable(orchestrator src/orchestrator_main.cpp)
|
||||
|
||||
@@ -47,6 +47,7 @@ enum class EditorModeType {
|
||||
Java,
|
||||
Rust,
|
||||
Go,
|
||||
Org,
|
||||
PlainText
|
||||
};
|
||||
|
||||
@@ -66,6 +67,7 @@ public:
|
||||
else if (language == "java") loadJava();
|
||||
else if (language == "rust") loadRust();
|
||||
else if (language == "go") loadGo();
|
||||
else if (language == "org") loadOrg();
|
||||
else loadPlainText();
|
||||
}
|
||||
|
||||
@@ -162,6 +164,7 @@ public:
|
||||
case EditorModeType::Java: return "Java";
|
||||
case EditorModeType::Rust: return "Rust";
|
||||
case EditorModeType::Go: return "Go";
|
||||
case EditorModeType::Org: return "Org";
|
||||
case EditorModeType::PlainText: return "Plain Text";
|
||||
}
|
||||
return "Unknown";
|
||||
@@ -176,6 +179,7 @@ public:
|
||||
if (lang == "java") return EditorModeType::Java;
|
||||
if (lang == "rust") return EditorModeType::Rust;
|
||||
if (lang == "go") return EditorModeType::Go;
|
||||
if (lang == "org") return EditorModeType::Org;
|
||||
return EditorModeType::PlainText;
|
||||
}
|
||||
|
||||
@@ -308,6 +312,17 @@ private:
|
||||
};
|
||||
}
|
||||
|
||||
void loadOrg() {
|
||||
type_ = EditorModeType::Org;
|
||||
indent_ = {"", "", 2, false};
|
||||
comment_ = {"#", "", ""};
|
||||
brackets_ = {{'(', ')'}, {'[', ']'}, {'{', '}'}, {'\"', '\"'}};
|
||||
snippets_ = {
|
||||
{"src", "#+begin_src $1\\n$0\\n#+end_src", "Source block"},
|
||||
{"hdr", "* $1\\n$0", "Heading"},
|
||||
};
|
||||
}
|
||||
|
||||
std::string language_;
|
||||
EditorModeType type_;
|
||||
IndentRule indent_;
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
#include "EmacsPackageBrowser.h"
|
||||
#include "EmacsFunctionDiscovery.h"
|
||||
#include "EmacsKeybinding.h"
|
||||
#include "OrgMode.h"
|
||||
#include "ast/Serialization.h"
|
||||
#include "ast/Generator.h"
|
||||
#include "ast/Annotation.h"
|
||||
@@ -187,6 +188,9 @@ struct EditorState {
|
||||
EmacsFunctionIndex emacsFunctionIndex;
|
||||
bool emacsFunctionIndexDirty = true;
|
||||
EmacsKeybindingState emacsKeys;
|
||||
bool showOrgPanel = true;
|
||||
OrgDocumentState orgDoc;
|
||||
int orgTempCounter = 0;
|
||||
struct LibraryIndexRequest {
|
||||
std::string name;
|
||||
std::string version;
|
||||
@@ -430,6 +434,8 @@ struct EditorState {
|
||||
void createBuffer(const std::string& path, const std::string& content,
|
||||
const std::string& language,
|
||||
BufferManager::BufferMode mode = BufferManager::BufferMode::Structured) {
|
||||
BufferManager::BufferMode effectiveMode = mode;
|
||||
if (language == "org") effectiveMode = BufferManager::BufferMode::Text;
|
||||
if (buffers.hasBuffer(path)) {
|
||||
buffers.switchToBuffer(path);
|
||||
activeBuffer = bufferStates[path].get();
|
||||
@@ -450,8 +456,8 @@ struct EditorState {
|
||||
state->generatedHighlightsDirty = true;
|
||||
state->modified = false;
|
||||
state->lspVersion = 1;
|
||||
buffers.openBuffer(path, content, language, mode);
|
||||
state->bufferMode = mode;
|
||||
buffers.openBuffer(path, content, language, effectiveMode);
|
||||
state->bufferMode = effectiveMode;
|
||||
activeBuffer = state.get();
|
||||
bufferStates[path] = std::move(state);
|
||||
active()->orchestratorDirty = true;
|
||||
@@ -802,6 +808,49 @@ struct EditorState {
|
||||
refreshBuildSystem();
|
||||
}
|
||||
|
||||
std::string orgTempExtension(const std::string& language) const {
|
||||
if (language == "python") return ".py";
|
||||
if (language == "cpp") return ".cpp";
|
||||
if (language == "elisp") return ".el";
|
||||
if (language == "javascript") return ".js";
|
||||
if (language == "typescript") return ".ts";
|
||||
if (language == "java") return ".java";
|
||||
if (language == "rust") return ".rs";
|
||||
if (language == "go") return ".go";
|
||||
return ".txt";
|
||||
}
|
||||
|
||||
std::string writeOrgTempFile(const std::string& language, const std::string& code) {
|
||||
std::filesystem::path root = workspaceRoot.empty()
|
||||
? std::filesystem::current_path()
|
||||
: std::filesystem::path(workspaceRoot);
|
||||
std::filesystem::path dir = root / ".whetstone_org";
|
||||
std::filesystem::create_directories(dir);
|
||||
std::string name = "org_block_" + std::to_string(++orgTempCounter) + orgTempExtension(language);
|
||||
std::filesystem::path filePath = dir / name;
|
||||
std::ofstream out(filePath.string(), std::ios::binary);
|
||||
out << code;
|
||||
return filePath.string();
|
||||
}
|
||||
|
||||
std::string runOrgBlock(const std::string& language, const std::string& code) {
|
||||
if (language == "elisp") {
|
||||
std::string result = emacs.sendCommand(ElispCommandBuilder::eval(code));
|
||||
if (!emacs.getLastError().empty() && result == "error") {
|
||||
return emacs.getLastError();
|
||||
}
|
||||
return result.empty() ? "OK" : result;
|
||||
}
|
||||
std::string path = writeOrgTempFile(language, code);
|
||||
std::string cmd = buildRunCommand(path, language, false);
|
||||
if (cmd.empty()) return "No runner for language: " + language;
|
||||
showTerminalPanel = true;
|
||||
std::string output;
|
||||
int codeExit = terminal.runAndCapture(workspaceRoot, cmd, output);
|
||||
output += "[exit code: " + std::to_string(codeExit) + "]";
|
||||
return output;
|
||||
}
|
||||
|
||||
void startEmacsDaemonFromSettings() {
|
||||
emacsDiagnostics.clear();
|
||||
std::string initPath = resolveEmacsInitPath();
|
||||
@@ -1369,6 +1418,10 @@ struct EditorState {
|
||||
if (lang == "elisp") {
|
||||
emacsFunctionIndexDirty = true;
|
||||
}
|
||||
if (lang == "org") {
|
||||
active()->bufferMode = BufferManager::BufferMode::Text;
|
||||
buffers.setBufferMode(active()->path, active()->bufferMode);
|
||||
}
|
||||
}
|
||||
|
||||
bool handleEmacsKeyChord(const std::string& chord) {
|
||||
@@ -1569,6 +1622,8 @@ struct EditorState {
|
||||
language = "rust";
|
||||
else if (path.size() > 3 && path.substr(path.size() - 3) == ".go")
|
||||
language = "go";
|
||||
else if (path.size() > 4 && path.substr(path.size() - 4) == ".org")
|
||||
language = "org";
|
||||
createBuffer(path, content, language, modeOverride);
|
||||
welcome.addRecentFile(path, language, bufferModeToString(modeOverride));
|
||||
saveRecentFiles();
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -24,6 +24,12 @@ extern "C" {
|
||||
const TSLanguage* tree_sitter_java();
|
||||
const TSLanguage* tree_sitter_rust();
|
||||
const TSLanguage* tree_sitter_go();
|
||||
#if !defined(WHETSTONE_ENABLE_ORG)
|
||||
#define WHETSTONE_ENABLE_ORG 0
|
||||
#endif
|
||||
#if WHETSTONE_ENABLE_ORG
|
||||
const TSLanguage* tree_sitter_org();
|
||||
#endif
|
||||
}
|
||||
|
||||
enum class TokenCategory {
|
||||
@@ -65,6 +71,9 @@ public:
|
||||
else if (language == "java") lang = tree_sitter_java();
|
||||
else if (language == "rust") lang = tree_sitter_rust();
|
||||
else if (language == "go") lang = tree_sitter_go();
|
||||
#if WHETSTONE_ENABLE_ORG
|
||||
else if (language == "org") lang = tree_sitter_org();
|
||||
#endif
|
||||
|
||||
if (!lang) {
|
||||
ts_parser_delete(parser);
|
||||
@@ -85,6 +94,9 @@ public:
|
||||
else if (language == "java") walkJava(root, source, spans);
|
||||
else if (language == "rust") walkRust(root, source, spans);
|
||||
else if (language == "go") walkGo(root, source, spans);
|
||||
#if WHETSTONE_ENABLE_ORG
|
||||
else if (language == "org") walkOrgSimple(source, spans);
|
||||
#endif
|
||||
|
||||
ts_tree_delete(tree);
|
||||
ts_parser_delete(parser);
|
||||
@@ -732,4 +744,28 @@ private:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Org -----------------------------------------------------------
|
||||
|
||||
static void walkOrgSimple(const std::string& source,
|
||||
std::vector<HighlightSpan>& spans) {
|
||||
size_t start = 0;
|
||||
while (start < source.size()) {
|
||||
size_t end = source.find('\n', start);
|
||||
if (end == std::string::npos) end = source.size();
|
||||
std::string line = source.substr(start, end - start);
|
||||
std::string trimmed = line;
|
||||
while (!trimmed.empty() && (trimmed.back() == '\r' || trimmed.back() == '\n')) {
|
||||
trimmed.pop_back();
|
||||
}
|
||||
if (!trimmed.empty() && trimmed[0] == '*') {
|
||||
spans.push_back({(uint32_t)start, (uint32_t)end, TokenCategory::Keyword});
|
||||
} else if (trimmed.rfind("#+begin_src", 0) == 0 ||
|
||||
trimmed.rfind("#+end_src", 0) == 0 ||
|
||||
trimmed.rfind("#+", 0) == 0) {
|
||||
spans.push_back({(uint32_t)start, (uint32_t)end, TokenCategory::Comment});
|
||||
}
|
||||
start = end + 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -280,7 +280,7 @@ int main(int, char**) {
|
||||
}
|
||||
if (ImGui::MenuItem("Open...", state.keys.getBinding("file.open").toString().c_str()))
|
||||
{
|
||||
auto path = FileDialog::openFile({"Open File", {"*.py","*.cpp","*.h","*.el","*.js","*.ts","*.java","*.rs","*.go"}, lastDialogPath});
|
||||
auto path = FileDialog::openFile({"Open File", {"*.py","*.cpp","*.h","*.el","*.js","*.ts","*.java","*.rs","*.go","*.org"}, lastDialogPath});
|
||||
if (!path.empty()) {
|
||||
lastDialogPath = path;
|
||||
state.doOpen(path);
|
||||
@@ -429,6 +429,8 @@ int main(int, char**) {
|
||||
state.setLanguage("rust");
|
||||
if (ImGui::MenuItem("Go", nullptr, state.active() && state.active()->language == "go"))
|
||||
state.setLanguage("go");
|
||||
if (ImGui::MenuItem("Org", nullptr, state.active() && state.active()->language == "org"))
|
||||
state.setLanguage("org");
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
if (ImGui::BeginMenu("Refactor")) {
|
||||
@@ -1256,117 +1258,138 @@ int main(int, char**) {
|
||||
}
|
||||
}
|
||||
|
||||
CodeEditorOptions opts;
|
||||
opts.showWhitespace = state.showWhitespace;
|
||||
opts.readOnly = buf->readOnly;
|
||||
opts.mode = &buf->mode;
|
||||
opts.enableFolding = true;
|
||||
opts.showMinimap = state.showMinimap;
|
||||
opts.showAnnotations = state.showAnnotations;
|
||||
opts.showLineNumbers = state.showLineNumbers;
|
||||
if (state.layoutPreset == LayoutPreset::Emacs) opts.annotationLayout = 1;
|
||||
else if (state.layoutPreset == LayoutPreset::JetBrains) opts.annotationLayout = 2;
|
||||
else opts.annotationLayout = 0;
|
||||
opts.errorLines = &errorLines;
|
||||
opts.warningLines = &warningLines;
|
||||
opts.diagnostics = &diagRanges;
|
||||
opts.annotations = &annoMarkers;
|
||||
opts.suggestions = &suggestionMarkers;
|
||||
opts.conflicts = &conflictMarkers;
|
||||
|
||||
CodeEditorResult res;
|
||||
if (buf->bufferMode == BufferManager::BufferMode::Structured) {
|
||||
opts.syncScrollX = &buf->splitScrollX;
|
||||
opts.syncScrollY = &buf->splitScrollY;
|
||||
opts.scrollMaster = true;
|
||||
|
||||
ImGui::BeginTable("##editorSplit", 2,
|
||||
ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp);
|
||||
ImGui::TableSetupColumn("Source", ImGuiTableColumnFlags_WidthStretch, 0.55f);
|
||||
ImGui::TableSetupColumn("Generated", ImGuiTableColumnFlags_WidthStretch, 0.45f);
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
res = buf->widget.render("##editor",
|
||||
buf->editBuf, buf->highlights, opts, ImVec2(0, avail.y), monoFont);
|
||||
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
ImGui::BeginGroup();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Generated");
|
||||
ImGui::SameLine();
|
||||
const char* targetLabels[] = {"Python", "C++", "Elisp"};
|
||||
const char* targetValues[] = {"python", "cpp", "elisp"};
|
||||
int targetIndex = 0;
|
||||
for (int i = 0; i < IM_ARRAYSIZE(targetValues); ++i) {
|
||||
if (buf->generatedLanguage == targetValues[i]) {
|
||||
targetIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
if (ImGui::Combo("##targetLang", &targetIndex,
|
||||
targetLabels, IM_ARRAYSIZE(targetLabels))) {
|
||||
buf->generatedLanguage = targetValues[targetIndex];
|
||||
buf->generatedMode.setLanguage(buf->generatedLanguage);
|
||||
buf->generatedHighlightsDirty = true;
|
||||
state.updateGenerated();
|
||||
}
|
||||
ImVec2 genAvail = ImGui::GetContentRegionAvail();
|
||||
CodeEditorOptions genOpts;
|
||||
genOpts.readOnly = true;
|
||||
genOpts.showWhitespace = state.showWhitespace;
|
||||
genOpts.mode = &buf->generatedMode;
|
||||
genOpts.showLineNumbers = state.showLineNumbers;
|
||||
genOpts.showCurrentLine = false;
|
||||
genOpts.highlightLine = buf->generatedHighlightLine;
|
||||
genOpts.syncScrollX = &buf->splitScrollX;
|
||||
genOpts.syncScrollY = &buf->splitScrollY;
|
||||
genOpts.scrollMaster = false;
|
||||
buf->generatedWidget.render("##generated",
|
||||
buf->generatedBuf, buf->generatedHighlights, genOpts,
|
||||
ImVec2(0, genAvail.y), monoFont);
|
||||
ImGui::EndGroup();
|
||||
ImGui::EndTable();
|
||||
if (buf->language == "org") {
|
||||
renderOrgDocument(state.orgDoc,
|
||||
buf->editBuf,
|
||||
state.showWhitespace,
|
||||
monoFont,
|
||||
uiFont,
|
||||
[&](const std::string& newText) {
|
||||
buf->editBuf = newText;
|
||||
state.onTextChanged();
|
||||
},
|
||||
[&](int, const std::string&, const std::string&) {
|
||||
buf->modified = true;
|
||||
},
|
||||
[&](const std::string& lang, const std::string& code) {
|
||||
return state.runOrgBlock(lang, code);
|
||||
});
|
||||
res.cursorByte = buf->widget.getCursor();
|
||||
} else {
|
||||
res = buf->widget.render("##editor",
|
||||
buf->editBuf, buf->highlights, opts, avail, monoFont);
|
||||
CodeEditorOptions opts;
|
||||
opts.showWhitespace = state.showWhitespace;
|
||||
opts.readOnly = buf->readOnly;
|
||||
opts.mode = &buf->mode;
|
||||
opts.enableFolding = true;
|
||||
opts.showMinimap = state.showMinimap;
|
||||
opts.showAnnotations = state.showAnnotations;
|
||||
opts.showLineNumbers = state.showLineNumbers;
|
||||
if (state.layoutPreset == LayoutPreset::Emacs) opts.annotationLayout = 1;
|
||||
else if (state.layoutPreset == LayoutPreset::JetBrains) opts.annotationLayout = 2;
|
||||
else opts.annotationLayout = 0;
|
||||
opts.errorLines = &errorLines;
|
||||
opts.warningLines = &warningLines;
|
||||
opts.diagnostics = &diagRanges;
|
||||
opts.annotations = &annoMarkers;
|
||||
opts.suggestions = &suggestionMarkers;
|
||||
opts.conflicts = &conflictMarkers;
|
||||
|
||||
if (buf->bufferMode == BufferManager::BufferMode::Structured) {
|
||||
opts.syncScrollX = &buf->splitScrollX;
|
||||
opts.syncScrollY = &buf->splitScrollY;
|
||||
opts.scrollMaster = true;
|
||||
|
||||
ImGui::BeginTable("##editorSplit", 2,
|
||||
ImGuiTableFlags_Resizable | ImGuiTableFlags_SizingStretchProp);
|
||||
ImGui::TableSetupColumn("Source", ImGuiTableColumnFlags_WidthStretch, 0.55f);
|
||||
ImGui::TableSetupColumn("Generated", ImGuiTableColumnFlags_WidthStretch, 0.45f);
|
||||
ImGui::TableNextRow();
|
||||
ImGui::TableSetColumnIndex(0);
|
||||
res = buf->widget.render("##editor",
|
||||
buf->editBuf, buf->highlights, opts, ImVec2(0, avail.y), monoFont);
|
||||
|
||||
ImGui::TableSetColumnIndex(1);
|
||||
ImGui::BeginGroup();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextUnformatted("Generated");
|
||||
ImGui::SameLine();
|
||||
const char* targetLabels[] = {"Python", "C++", "Elisp"};
|
||||
const char* targetValues[] = {"python", "cpp", "elisp"};
|
||||
int targetIndex = 0;
|
||||
for (int i = 0; i < IM_ARRAYSIZE(targetValues); ++i) {
|
||||
if (buf->generatedLanguage == targetValues[i]) {
|
||||
targetIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
ImGui::SetNextItemWidth(120.0f);
|
||||
if (ImGui::Combo("##targetLang", &targetIndex,
|
||||
targetLabels, IM_ARRAYSIZE(targetLabels))) {
|
||||
buf->generatedLanguage = targetValues[targetIndex];
|
||||
buf->generatedMode.setLanguage(buf->generatedLanguage);
|
||||
buf->generatedHighlightsDirty = true;
|
||||
state.updateGenerated();
|
||||
}
|
||||
ImVec2 genAvail = ImGui::GetContentRegionAvail();
|
||||
CodeEditorOptions genOpts;
|
||||
genOpts.readOnly = true;
|
||||
genOpts.showWhitespace = state.showWhitespace;
|
||||
genOpts.mode = &buf->generatedMode;
|
||||
genOpts.showLineNumbers = state.showLineNumbers;
|
||||
genOpts.showCurrentLine = false;
|
||||
genOpts.highlightLine = buf->generatedHighlightLine;
|
||||
genOpts.syncScrollX = &buf->splitScrollX;
|
||||
genOpts.syncScrollY = &buf->splitScrollY;
|
||||
genOpts.scrollMaster = false;
|
||||
buf->generatedWidget.render("##generated",
|
||||
buf->generatedBuf, buf->generatedHighlights, genOpts,
|
||||
ImVec2(0, genAvail.y), monoFont);
|
||||
ImGui::EndGroup();
|
||||
ImGui::EndTable();
|
||||
} else {
|
||||
res = buf->widget.render("##editor",
|
||||
buf->editBuf, buf->highlights, opts, avail, monoFont);
|
||||
}
|
||||
}
|
||||
|
||||
state.updateCursorPos(res.cursorByte);
|
||||
if (res.ctrlClick && state.active()) {
|
||||
state.goToDefinitionAt(res.ctrlClickLine, res.ctrlClickCol);
|
||||
}
|
||||
if (res.lineClicked && buf->bufferMode == BufferManager::BufferMode::Structured) {
|
||||
Module* ast = state.activeAST();
|
||||
if (ast) {
|
||||
int genLines = countLines(buf->generatedBuf);
|
||||
int targetLine = std::max(0, std::min(res.clickedLine, genLines - 1));
|
||||
buf->generatedHighlightLine = targetLine;
|
||||
} else {
|
||||
buf->generatedHighlightLine = -1;
|
||||
if (buf->language != "org") {
|
||||
state.updateCursorPos(res.cursorByte);
|
||||
if (res.ctrlClick && state.active()) {
|
||||
state.goToDefinitionAt(res.ctrlClickLine, res.ctrlClickCol);
|
||||
}
|
||||
}
|
||||
if (res.changed) {
|
||||
state.onTextChanged();
|
||||
state.completionPending = true;
|
||||
state.completionLastChange = ImGui::GetTime();
|
||||
state.completionVisible = false;
|
||||
state.completionSelected = 0;
|
||||
if (state.lsp) state.lsp->clearCompletionItems();
|
||||
if (state.lsp && state.active() && state.active()->path.rfind("(untitled", 0) != 0) {
|
||||
int cursor = state.active()->widget.getCursor();
|
||||
if (cursor > 0 && state.active()->editBuf[cursor - 1] == '(') {
|
||||
int lineZero = std::max(0, state.active()->cursorLine - 1);
|
||||
int colZero = std::max(0, state.active()->cursorCol - 1);
|
||||
state.lsp->requestSignatureHelp(EditorState::toFileUri(state.active()->path),
|
||||
lineZero, colZero);
|
||||
} else if (cursor > 0 && state.active()->editBuf[cursor - 1] == ')') {
|
||||
state.lsp->clearSignatureHelp();
|
||||
if (res.lineClicked && buf->bufferMode == BufferManager::BufferMode::Structured) {
|
||||
Module* ast = state.activeAST();
|
||||
if (ast) {
|
||||
int genLines = countLines(buf->generatedBuf);
|
||||
int targetLine = std::max(0, std::min(res.clickedLine, genLines - 1));
|
||||
buf->generatedHighlightLine = targetLine;
|
||||
} else {
|
||||
buf->generatedHighlightLine = -1;
|
||||
}
|
||||
}
|
||||
if (buf->bufferMode == BufferManager::BufferMode::Structured) {
|
||||
state.analysisPending = true;
|
||||
state.analysisLastChange = ImGui::GetTime();
|
||||
if (res.changed) {
|
||||
state.onTextChanged();
|
||||
state.completionPending = true;
|
||||
state.completionLastChange = ImGui::GetTime();
|
||||
state.completionVisible = false;
|
||||
state.completionSelected = 0;
|
||||
if (state.lsp) state.lsp->clearCompletionItems();
|
||||
if (state.lsp && state.active() && state.active()->path.rfind("(untitled", 0) != 0) {
|
||||
int cursor = state.active()->widget.getCursor();
|
||||
if (cursor > 0 && state.active()->editBuf[cursor - 1] == '(') {
|
||||
int lineZero = std::max(0, state.active()->cursorLine - 1);
|
||||
int colZero = std::max(0, state.active()->cursorCol - 1);
|
||||
state.lsp->requestSignatureHelp(EditorState::toFileUri(state.active()->path),
|
||||
lineZero, colZero);
|
||||
} else if (cursor > 0 && state.active()->editBuf[cursor - 1] == ')') {
|
||||
state.lsp->clearSignatureHelp();
|
||||
}
|
||||
}
|
||||
if (buf->bufferMode == BufferManager::BufferMode::Structured) {
|
||||
state.analysisPending = true;
|
||||
state.analysisLastChange = ImGui::GetTime();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
49
editor/tests/step145_test.cpp
Normal file
49
editor/tests/step145_test.cpp
Normal file
@@ -0,0 +1,49 @@
|
||||
// Step 145 TDD Test: Org-mode parsing and source blocks
|
||||
#include "OrgMode.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 text =
|
||||
"* Title\n"
|
||||
"\n"
|
||||
"Some paragraph text.\n"
|
||||
"\n"
|
||||
"#+begin_src python\n"
|
||||
"print('hi')\n"
|
||||
"#+end_src\n";
|
||||
|
||||
auto blocks = parseOrgBlocks(text);
|
||||
expect(blocks.size() == 3, "parsed block count", passed, failed);
|
||||
expect(blocks[0].type == OrgBlockType::Heading, "heading block", passed, failed);
|
||||
expect(blocks[0].title == "Title", "heading title", passed, failed);
|
||||
expect(blocks[1].type == OrgBlockType::Paragraph, "paragraph block", passed, failed);
|
||||
expect(blocks[2].type == OrgBlockType::SourceBlock, "source block", passed, failed);
|
||||
expect(blocks[2].language == "python", "source language", passed, failed);
|
||||
expect(blocks[2].code.find("print") != std::string::npos, "source code", passed, failed);
|
||||
|
||||
std::string rebuilt = buildOrgText(blocks);
|
||||
expect(rebuilt.find("#+begin_src python") != std::string::npos, "build org text", passed, failed);
|
||||
|
||||
OrgDocumentState state;
|
||||
updateOrgDocument(state, text);
|
||||
expect(!state.blocks.empty(), "doc blocks stored", passed, failed);
|
||||
expect(state.editors.size() == 1, "source editors count", passed, failed);
|
||||
expect(state.modes.size() == 1, "source modes count", passed, failed);
|
||||
|
||||
std::cout << "\n=== Step 145 Results: " << passed << " passed, "
|
||||
<< failed << " failed ===\n";
|
||||
return failed == 0 ? 0 : 1;
|
||||
}
|
||||
@@ -203,7 +203,7 @@ functionality through the Whetstone UI.
|
||||
Keybindings loaded from user's Emacs config (key-binding query to daemon).
|
||||
*Modifies:* `KeybindingManager.h`, `LayoutManager.h`
|
||||
|
||||
- [ ] **Step 145: Org-mode and Literate programming support**
|
||||
- [x] **Step 145: Org-mode and Literate programming support**
|
||||
Parse `.org` files using a tree-sitter-org grammar. Render org-mode
|
||||
documents with headings, source blocks, and prose. Source blocks
|
||||
are editable with full syntax highlighting and LSP support.
|
||||
|
||||
Reference in New Issue
Block a user