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:
240
editor/src/EditorMode.h
Normal file
240
editor/src/EditorMode.h
Normal 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_;
|
||||
};
|
||||
Reference in New Issue
Block a user