basic openmodelica model emitting
This commit is contained in:
252
BEdit/src/bedit/gui/editors/openmodelica.py
Normal file
252
BEdit/src/bedit/gui/editors/openmodelica.py
Normal file
@@ -0,0 +1,252 @@
|
||||
import json
|
||||
from importlib.resources import files
|
||||
|
||||
from PySide6.QtCore import QRegularExpression, QStringListModel, Qt
|
||||
from PySide6.QtGui import (
|
||||
QColor,
|
||||
QFont,
|
||||
QKeyEvent,
|
||||
QSyntaxHighlighter,
|
||||
QTextCharFormat,
|
||||
QTextCursor,
|
||||
)
|
||||
from PySide6.QtWidgets import QCompleter, QPlainTextEdit
|
||||
|
||||
from bedit.gui.preferences import application_settings
|
||||
|
||||
|
||||
HIGHLIGHT_STYLES = {
|
||||
"keywords": ("Keywords", "#7c3aed", True, False),
|
||||
"types": ("Types", "#0369a1", True, False),
|
||||
"builtins": ("Built-ins", "#0f766e", False, False),
|
||||
"bevalues": ("BEvalues", "#c026d3", True, False),
|
||||
"inputs": ("Inputs", "#1d4ed8", False, False),
|
||||
"outputs": ("Outputs", "#be123c", False, False),
|
||||
"parameters": ("Parameters", "#a16207", False, False),
|
||||
"numbers": ("Numbers", "#b45309", False, False),
|
||||
"strings": ("Strings", "#15803d", False, False),
|
||||
"comments": ("Comments", "#6b7280", False, True),
|
||||
}
|
||||
|
||||
|
||||
def load_openmodelica_syntax() -> dict[str, list[str]]:
|
||||
resource = files("bedit").joinpath("data/syntax/openmodelica.json")
|
||||
with resource.open(encoding="utf-8") as file:
|
||||
values = json.load(file)
|
||||
return {
|
||||
category: [str(word) for word in values.get(category, [])]
|
||||
for category in ("keywords", "types", "builtins", "bevalues")
|
||||
}
|
||||
|
||||
|
||||
def _format(color: str, *, bold: bool = False, italic: bool = False) -> QTextCharFormat:
|
||||
value = QTextCharFormat()
|
||||
value.setForeground(QColor(color))
|
||||
value.setFontWeight(QFont.Weight.Bold if bold else QFont.Weight.Normal)
|
||||
value.setFontItalic(italic)
|
||||
return value
|
||||
|
||||
|
||||
def _as_bool(value) -> bool:
|
||||
if isinstance(value, str):
|
||||
return value.strip().lower() in {"1", "true", "yes", "on"}
|
||||
return bool(value)
|
||||
|
||||
|
||||
def highlighting_style(category: str) -> tuple[str, bool, bool]:
|
||||
_label, default_color, default_bold, default_italic = HIGHLIGHT_STYLES[category]
|
||||
settings = application_settings()
|
||||
prefix = f"syntax/{category}"
|
||||
return (
|
||||
str(settings.value(f"{prefix}/color", default_color)),
|
||||
_as_bool(settings.value(f"{prefix}/bold", default_bold)),
|
||||
_as_bool(settings.value(f"{prefix}/italic", default_italic)),
|
||||
)
|
||||
|
||||
|
||||
class OpenModelicaHighlighter(QSyntaxHighlighter):
|
||||
"""Syntax highlighter driven by the editable OpenModelica word list."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
document,
|
||||
syntax: dict[str, list[str]],
|
||||
symbols: dict[str, list[str]],
|
||||
) -> None:
|
||||
super().__init__(document)
|
||||
self.rules: list[tuple[QRegularExpression, QTextCharFormat]] = []
|
||||
for category in (
|
||||
"keywords",
|
||||
"types",
|
||||
"builtins",
|
||||
"inputs",
|
||||
"outputs",
|
||||
"parameters",
|
||||
):
|
||||
words = syntax.get(category, symbols.get(category, []))
|
||||
if words:
|
||||
pattern = r"\b(?:" + "|".join(map(QRegularExpression.escape, words)) + r")\b"
|
||||
color, bold, italic = highlighting_style(category)
|
||||
self.rules.append(
|
||||
(QRegularExpression(pattern), _format(color, bold=bold, italic=italic))
|
||||
)
|
||||
bevalue_style = highlighting_style("bevalues")
|
||||
number_style = highlighting_style("numbers")
|
||||
string_style = highlighting_style("strings")
|
||||
self.rules.extend(
|
||||
[
|
||||
(
|
||||
QRegularExpression(r"\$[A-Za-z_][A-Za-z0-9_]*\$"),
|
||||
_format(
|
||||
bevalue_style[0],
|
||||
bold=bevalue_style[1],
|
||||
italic=bevalue_style[2],
|
||||
),
|
||||
),
|
||||
(
|
||||
QRegularExpression(r"\b(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?\b"),
|
||||
_format(number_style[0], bold=number_style[1], italic=number_style[2]),
|
||||
),
|
||||
(
|
||||
QRegularExpression(r'"(?:\\.|[^"\\])*"'),
|
||||
_format(string_style[0], bold=string_style[1], italic=string_style[2]),
|
||||
),
|
||||
]
|
||||
)
|
||||
comment_style = highlighting_style("comments")
|
||||
self.comment_format = _format(
|
||||
comment_style[0], bold=comment_style[1], italic=comment_style[2]
|
||||
)
|
||||
self.rules.append((QRegularExpression(r"//.*$"), self.comment_format))
|
||||
self.comment_start = QRegularExpression(r"/\*")
|
||||
self.comment_end = QRegularExpression(r"\*/")
|
||||
|
||||
def highlightBlock(self, text: str) -> None: # noqa: N802
|
||||
for expression, text_format in self.rules:
|
||||
match = expression.globalMatch(text)
|
||||
while match.hasNext():
|
||||
result = match.next()
|
||||
self.setFormat(result.capturedStart(), result.capturedLength(), text_format)
|
||||
|
||||
self.setCurrentBlockState(0)
|
||||
start = (
|
||||
0
|
||||
if self.previousBlockState() == 1
|
||||
else self.comment_start.match(text).capturedStart()
|
||||
)
|
||||
while start >= 0:
|
||||
end_match = self.comment_end.match(text, start + 2)
|
||||
if end_match.hasMatch():
|
||||
length = end_match.capturedEnd() - start
|
||||
else:
|
||||
self.setCurrentBlockState(1)
|
||||
length = len(text) - start
|
||||
self.setFormat(start, length, self.comment_format)
|
||||
if not end_match.hasMatch():
|
||||
break
|
||||
start = self.comment_start.match(text, start + length).capturedStart()
|
||||
|
||||
|
||||
class OpenModelicaEditor(QPlainTextEdit):
|
||||
"""OpenModelica text editor with syntax highlighting and completion."""
|
||||
|
||||
def __init__(self, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
|
||||
self.setPlaceholderText("Enter OpenModelica equations here…")
|
||||
self.syntax = load_openmodelica_syntax()
|
||||
self.symbols = {"inputs": [], "outputs": [], "parameters": []}
|
||||
self.highlighter = OpenModelicaHighlighter(
|
||||
self.document(), self.syntax, self.symbols
|
||||
)
|
||||
self.completer = QCompleter(self)
|
||||
self.completer.setWidget(self)
|
||||
self.completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
|
||||
self.completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion)
|
||||
self.completer.activated.connect(self._insert_completion)
|
||||
self._rebuild_completions()
|
||||
|
||||
def set_symbols(
|
||||
self,
|
||||
inputs: list[str],
|
||||
outputs: list[str],
|
||||
parameters: list[str],
|
||||
) -> None:
|
||||
self.symbols = {
|
||||
"inputs": [name for name in inputs if name],
|
||||
"outputs": [name for name in outputs if name],
|
||||
"parameters": [name for name in parameters if name],
|
||||
}
|
||||
self.reload_highlighting()
|
||||
|
||||
def reload_highlighting(self) -> None:
|
||||
self.highlighter.setDocument(None)
|
||||
self.highlighter = OpenModelicaHighlighter(
|
||||
self.document(), self.syntax, self.symbols
|
||||
)
|
||||
self._rebuild_completions()
|
||||
|
||||
def _rebuild_completions(self) -> None:
|
||||
words = sorted(
|
||||
{
|
||||
*self.syntax["keywords"],
|
||||
*self.syntax["types"],
|
||||
*self.syntax["builtins"],
|
||||
*(f"${name.strip('$')}$" for name in self.syntax["bevalues"]),
|
||||
*self.symbols["inputs"],
|
||||
*self.symbols["outputs"],
|
||||
*self.symbols["parameters"],
|
||||
},
|
||||
key=str.casefold,
|
||||
)
|
||||
self.completer.setModel(QStringListModel(words, self.completer))
|
||||
|
||||
def _completion_prefix(self) -> str:
|
||||
cursor = self.textCursor()
|
||||
text = cursor.block().text()[: cursor.positionInBlock()]
|
||||
index = len(text)
|
||||
while index > 0 and (text[index - 1].isalnum() or text[index - 1] in "_$"):
|
||||
index -= 1
|
||||
return text[index:]
|
||||
|
||||
def _insert_completion(self, completion: str) -> None:
|
||||
prefix = self._completion_prefix()
|
||||
cursor = self.textCursor()
|
||||
cursor.movePosition(
|
||||
QTextCursor.MoveOperation.Left,
|
||||
QTextCursor.MoveMode.KeepAnchor,
|
||||
len(prefix),
|
||||
)
|
||||
cursor.insertText(completion)
|
||||
self.setTextCursor(cursor)
|
||||
|
||||
def keyPressEvent(self, event: QKeyEvent) -> None: # noqa: N802
|
||||
popup = self.completer.popup()
|
||||
if popup.isVisible() and event.key() in {
|
||||
Qt.Key.Key_Enter,
|
||||
Qt.Key.Key_Return,
|
||||
Qt.Key.Key_Escape,
|
||||
Qt.Key.Key_Tab,
|
||||
Qt.Key.Key_Backtab,
|
||||
}:
|
||||
event.ignore()
|
||||
return
|
||||
explicit = (
|
||||
event.modifiers() == Qt.KeyboardModifier.ControlModifier
|
||||
and event.key() == Qt.Key.Key_Space
|
||||
)
|
||||
if not explicit:
|
||||
super().keyPressEvent(event)
|
||||
prefix = self._completion_prefix()
|
||||
if not explicit and (len(prefix) < 2 or event.text() == ""):
|
||||
popup.hide()
|
||||
return
|
||||
self.completer.setCompletionPrefix(prefix)
|
||||
if self.completer.completionCount() == 0:
|
||||
popup.hide()
|
||||
return
|
||||
rectangle = self.cursorRect()
|
||||
rectangle.setWidth(
|
||||
popup.sizeHintForColumn(0) + popup.verticalScrollBar().sizeHint().width()
|
||||
)
|
||||
self.completer.complete(rectangle)
|
||||
Reference in New Issue
Block a user