253 lines
9.1 KiB
Python
253 lines
9.1 KiB
Python
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)
|