Phase 3d complete: editor infrastructure (Steps 55-58)

Step 55: WelcomeScreen (actions, recent files, tips, visibility) - 7/7 tests
Step 56: ElispCommandBuilder + MockEmacsConnection (escaping, daemon lifecycle) - 6/6 tests
Step 57: BufferManager (multi-file open/close/switch, language/modified tracking) - 7/7 tests
Step 58: EditorMode (per-language indent, comment toggle, bracket pairs, snippets) - 8/8 tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bill
2026-02-08 16:27:22 -07:00
parent 16cc60b36c
commit bb7981d4d7
10 changed files with 1030 additions and 114 deletions

240
editor/src/EditorMode.h Normal file
View File

@@ -0,0 +1,240 @@
#pragma once
// Step 58: Mode-specific editor behavior
//
// EditorMode provides language-aware editing features:
// - Auto-indent rules per language
// - Comment toggling per language
// - Bracket/quote auto-close per language
// - Language-specific snippet templates
//
// The editor queries the active EditorMode to determine behavior
// when the user types or performs editing operations.
#include <string>
#include <vector>
#include <map>
struct IndentRule {
std::string increasePattern; // regex-like pattern that increases indent
std::string decreasePattern; // regex-like pattern that decreases indent
int tabSize = 4;
bool useTabs = false;
};
struct CommentStyle {
std::string lineComment; // e.g. "//", "#", ";;"
std::string blockStart; // e.g. "/*", "\"\"\""
std::string blockEnd; // e.g. "*/", "\"\"\""
};
struct BracketPair {
char open;
char close;
};
struct SnippetTemplate {
std::string trigger; // e.g. "def", "for", "if"
std::string expansion; // template with $1, $2 placeholders
std::string description;
};
enum class EditorModeType {
Python,
Cpp,
Elisp,
PlainText
};
class EditorMode {
public:
explicit EditorMode(const std::string& language = "python") {
setLanguage(language);
}
void setLanguage(const std::string& language) {
language_ = language;
if (language == "python") loadPython();
else if (language == "cpp") loadCpp();
else if (language == "elisp") loadElisp();
else loadPlainText();
}
std::string getLanguage() const { return language_; }
EditorModeType getType() const { return type_; }
// --- Indentation ---
const IndentRule& getIndentRule() const { return indent_; }
// Should we increase indent after this line?
bool shouldIndentAfter(const std::string& line) const {
if (indent_.increasePattern.empty()) return false;
// Simple pattern matching (not full regex)
return lineEndsWith(line, indent_.increasePattern);
}
// Should we decrease indent for this line?
bool shouldDedent(const std::string& line) const {
if (indent_.decreasePattern.empty()) return false;
return trimmedLineStartsWith(line, indent_.decreasePattern);
}
// Get indent string (tabs or spaces)
std::string getIndentString() const {
if (indent_.useTabs) return "\t";
return std::string(indent_.tabSize, ' ');
}
int getTabSize() const { return indent_.tabSize; }
// --- Comments ---
const CommentStyle& getCommentStyle() const { return comment_; }
// Toggle line comment: add or remove line comment prefix
std::string toggleLineComment(const std::string& line) const {
if (comment_.lineComment.empty()) return line;
std::string trimmed = trimLeft(line);
std::string prefix = comment_.lineComment + " ";
if (trimmed.substr(0, prefix.size()) == prefix) {
// Remove comment
size_t commentPos = line.find(prefix);
return line.substr(0, commentPos) + line.substr(commentPos + prefix.size());
} else if (trimmed.substr(0, comment_.lineComment.size()) == comment_.lineComment) {
// Remove comment without space
size_t commentPos = line.find(comment_.lineComment);
return line.substr(0, commentPos) + line.substr(commentPos + comment_.lineComment.size());
} else {
// Add comment at the start of content (preserving indentation)
size_t firstNonSpace = line.find_first_not_of(" \t");
if (firstNonSpace == std::string::npos)
return line; // empty/whitespace line
return line.substr(0, firstNonSpace) + prefix + line.substr(firstNonSpace);
}
}
// --- Auto-close brackets ---
const std::vector<BracketPair>& getBracketPairs() const { return brackets_; }
// Given an opening character, return the closing character (0 if not a bracket)
char getClosingBracket(char open) const {
for (const auto& bp : brackets_) {
if (bp.open == open) return bp.close;
}
return 0;
}
// --- Snippets ---
const std::vector<SnippetTemplate>& getSnippets() const { return snippets_; }
// Find snippet by trigger
const SnippetTemplate* findSnippet(const std::string& trigger) const {
for (const auto& s : snippets_) {
if (s.trigger == trigger) return &s;
}
return nullptr;
}
// --- Static helpers ---
static const char* modeName(EditorModeType t) {
switch (t) {
case EditorModeType::Python: return "Python";
case EditorModeType::Cpp: return "C++";
case EditorModeType::Elisp: return "Elisp";
case EditorModeType::PlainText: return "Plain Text";
}
return "Unknown";
}
static EditorModeType modeFromLanguage(const std::string& lang) {
if (lang == "python") return EditorModeType::Python;
if (lang == "cpp") return EditorModeType::Cpp;
if (lang == "elisp") return EditorModeType::Elisp;
return EditorModeType::PlainText;
}
private:
static std::string trimLeft(const std::string& s) {
size_t start = s.find_first_not_of(" \t");
return (start == std::string::npos) ? "" : s.substr(start);
}
static bool lineEndsWith(const std::string& line, const std::string& suffix) {
std::string trimmed = line;
// Remove trailing whitespace
while (!trimmed.empty() && (trimmed.back() == ' ' || trimmed.back() == '\t'))
trimmed.pop_back();
if (trimmed.size() < suffix.size()) return false;
return trimmed.substr(trimmed.size() - suffix.size()) == suffix;
}
static bool trimmedLineStartsWith(const std::string& line, const std::string& prefix) {
std::string trimmed = trimLeft(line);
return trimmed.substr(0, prefix.size()) == prefix;
}
void loadPython() {
type_ = EditorModeType::Python;
indent_ = {":", "return", 4, false};
comment_ = {"#", "\"\"\"", "\"\"\""};
brackets_ = {{'(', ')'}, {'[', ']'}, {'{', '}'}, {'\'', '\''}, {'"', '"'}};
snippets_ = {
{"def", "def $1($2):\n $0", "Function definition"},
{"class", "class $1:\n def __init__(self):\n $0", "Class definition"},
{"if", "if $1:\n $0", "If statement"},
{"for", "for $1 in $2:\n $0", "For loop"},
{"while", "while $1:\n $0", "While loop"},
{"try", "try:\n $0\nexcept $1:\n pass", "Try/except block"},
};
}
void loadCpp() {
type_ = EditorModeType::Cpp;
indent_ = {"{", "}", 4, false};
comment_ = {"//", "/*", "*/"};
brackets_ = {{'(', ')'}, {'[', ']'}, {'{', '}'}, {'\'', '\''}, {'"', '"'}, {'<', '>'}};
snippets_ = {
{"main", "int main(int argc, char* argv[]) {\n $0\n return 0;\n}", "Main function"},
{"class", "class $1 {\npublic:\n $1();\n ~$1();\nprivate:\n $0\n};", "Class definition"},
{"if", "if ($1) {\n $0\n}", "If statement"},
{"for", "for (int $1 = 0; $1 < $2; ++$1) {\n $0\n}", "For loop"},
{"while", "while ($1) {\n $0\n}", "While loop"},
{"switch", "switch ($1) {\n case $2:\n $0\n break;\n default:\n break;\n}", "Switch statement"},
{"inc", "#include <$1>", "Include directive"},
};
}
void loadElisp() {
type_ = EditorModeType::Elisp;
indent_ = {"(", ")", 2, false};
comment_ = {";;", "", ""};
brackets_ = {{'(', ')'}, {'[', ']'}, {'"', '"'}};
snippets_ = {
{"defun", "(defun $1 ($2)\n \"$3\"\n $0)", "Function definition"},
{"let", "(let (($1 $2))\n $0)", "Let binding"},
{"if", "(if $1\n $2\n $0)", "If expression"},
{"cond", "(cond\n (($1) $2)\n (t $0))", "Cond expression"},
{"lambda", "(lambda ($1) $0)", "Lambda expression"},
};
}
void loadPlainText() {
type_ = EditorModeType::PlainText;
indent_ = {"", "", 4, false};
comment_ = {"", "", ""};
brackets_ = {{'(', ')'}, {'[', ']'}, {'{', '}'}, {'"', '"'}};
snippets_ = {};
}
std::string language_;
EditorModeType type_;
IndentRule indent_;
CommentStyle comment_;
std::vector<BracketPair> brackets_;
std::vector<SnippetTemplate> snippets_;
};