Add code block execution to the GUI outline view
Each executable source block now shows a "Run" button in the outline. Clicking it runs the block server-side and renders the inserted #+RESULTS: in place, with an exit-status indicator. DocumentModel gains executeBlock(row, blockIndex), which runs /ctl execute <doc> <id> <n>, refreshes the section body, and emits dataChanged(BodyRole). isLanguageSupported(lang) backs button visibility via /ctl languages (loaded once, cached). OutlineView tags each parsed code part with its src-block ordinal so the GUI index matches the server's block indexing, and the code block header carries the Run control plus running/ok/error status.
This commit is contained in:
parent
d073c8fa5e
commit
36a9607576
|
|
@ -68,6 +68,7 @@ ListView {
|
|||
let inCode = false;
|
||||
let codeLang = "";
|
||||
let codeLines = [];
|
||||
let codeCounter = 0;
|
||||
let inTable = false;
|
||||
let tableLines = [];
|
||||
|
||||
|
|
@ -87,7 +88,8 @@ ListView {
|
|||
codeLang = trimmed.substring(11).trim().split(/\s+/)[0] || "";
|
||||
codeLines = [];
|
||||
} else if (inCode && trimmed.toUpperCase() === "#+END_SRC") {
|
||||
parts.push({type: "code", language: codeLang, content: codeLines.join('\n')});
|
||||
parts.push({type: "code", language: codeLang, content: codeLines.join('\n'), codeIndex: codeCounter});
|
||||
codeCounter++;
|
||||
inCode = false;
|
||||
codeLang = "";
|
||||
codeLines = [];
|
||||
|
|
@ -134,7 +136,8 @@ ListView {
|
|||
|
||||
// Flush remaining
|
||||
if (inCode && codeLines.length > 0) {
|
||||
parts.push({type: "code", language: codeLang, content: codeLines.join('\n')});
|
||||
parts.push({type: "code", language: codeLang, content: codeLines.join('\n'), codeIndex: codeCounter});
|
||||
codeCounter++;
|
||||
} else if (inTable && tableLines.length > 0) {
|
||||
let rows = [];
|
||||
let hasHeader = false;
|
||||
|
|
@ -368,23 +371,97 @@ ListView {
|
|||
Component {
|
||||
id: codeBlockComponent
|
||||
Rectangle {
|
||||
id: codeBlock
|
||||
color: theme.alternateBaseColor
|
||||
radius: outline.baseFontSize / 3
|
||||
implicitHeight: codeColumn.implicitHeight + outline.baseFontSize
|
||||
|
||||
|
||||
// Last execution status for this block: "", "running", "ok", "error"
|
||||
property string runState: ""
|
||||
property int runStatus: 0
|
||||
|
||||
ColumnLayout {
|
||||
id: codeColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: outline.baseFontSize * 0.5
|
||||
spacing: outline.baseFontSize * 0.3
|
||||
|
||||
// Language label
|
||||
Label {
|
||||
visible: (modelData.language ?? "") !== ""
|
||||
text: modelData.language
|
||||
color: theme.linkColor
|
||||
font.pointSize: outline.baseFontSize - 2
|
||||
font.bold: true
|
||||
// Header row: language label + run control
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: outline.baseFontSize * 0.5
|
||||
|
||||
// Language label
|
||||
Label {
|
||||
visible: (modelData.language ?? "") !== ""
|
||||
text: modelData.language
|
||||
color: theme.linkColor
|
||||
font.pointSize: outline.baseFontSize - 2
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
// Run status indicator
|
||||
Label {
|
||||
visible: codeBlock.runState !== ""
|
||||
text: {
|
||||
switch (codeBlock.runState) {
|
||||
case "running": return "running…"
|
||||
case "ok": return "✓ exit 0"
|
||||
case "error": return "✗ exit " + codeBlock.runStatus
|
||||
default: return ""
|
||||
}
|
||||
}
|
||||
color: codeBlock.runState === "error"
|
||||
? theme.cancelledColor
|
||||
: (codeBlock.runState === "ok"
|
||||
? theme.doneColor
|
||||
: theme.placeholderTextColor)
|
||||
font.pointSize: outline.baseFontSize - 2
|
||||
}
|
||||
|
||||
// Run button — only for executable languages
|
||||
Rectangle {
|
||||
id: runButton
|
||||
visible: docModel.isLanguageSupported(modelData.language ?? "")
|
||||
radius: 3
|
||||
implicitWidth: runLabel.implicitWidth + 14
|
||||
implicitHeight: runLabel.implicitHeight + 6
|
||||
color: runMouse.containsMouse ? theme.highlightColor : "transparent"
|
||||
border.color: theme.linkColor
|
||||
border.width: 1
|
||||
|
||||
Label {
|
||||
id: runLabel
|
||||
anchors.centerIn: parent
|
||||
text: "▶ Run"
|
||||
color: theme.linkColor
|
||||
font.pointSize: outline.baseFontSize - 2
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: runMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
codeBlock.runState = "running"
|
||||
let out = docModel.executeBlock(del.index, modelData.codeIndex ?? 0)
|
||||
// The report contains a "status: N" line per block.
|
||||
let m = out.match(/status:\s*(\d+)/)
|
||||
let hadError = /\berror:/.test(out)
|
||||
if (m) {
|
||||
codeBlock.runStatus = parseInt(m[1])
|
||||
codeBlock.runState = codeBlock.runStatus === 0 ? "ok" : "error"
|
||||
} else {
|
||||
codeBlock.runStatus = hadError ? 1 : 0
|
||||
codeBlock.runState = hadError ? "error" : "ok"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Code content
|
||||
|
|
|
|||
|
|
@ -135,6 +135,57 @@ void DocumentModel::reload()
|
|||
loadDocument();
|
||||
}
|
||||
|
||||
QString DocumentModel::executeBlock(int row, int blockIndex)
|
||||
{
|
||||
if (!m_client || m_document.isEmpty())
|
||||
return QStringLiteral("error: not connected");
|
||||
if (row < 0 || row >= m_visibleRows.size())
|
||||
return QStringLiteral("error: invalid row");
|
||||
|
||||
const QString &id = m_visibleRows[row];
|
||||
if (!m_sections.contains(id))
|
||||
return QStringLiteral("error: unknown section");
|
||||
|
||||
// Document name without a leading slash for the ctl verb.
|
||||
QString doc = m_document;
|
||||
if (doc.startsWith('/'))
|
||||
doc = doc.mid(1);
|
||||
|
||||
const QString cmd = QStringLiteral("execute %1 %2 %3")
|
||||
.arg(doc, id)
|
||||
.arg(blockIndex);
|
||||
const QString result = m_client->rdwr(QStringLiteral("/ctl"), cmd);
|
||||
|
||||
// Refresh the section body so the inserted #+RESULTS: block is rendered.
|
||||
Section &sec = m_sections[id];
|
||||
sec.bodyLoaded = false;
|
||||
loadBody(sec);
|
||||
|
||||
const QModelIndex idx = index(row);
|
||||
emit dataChanged(idx, idx, {BodyRole});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool DocumentModel::isLanguageSupported(const QString &lang)
|
||||
{
|
||||
if (lang.isEmpty())
|
||||
return false;
|
||||
|
||||
if (!m_languagesLoaded) {
|
||||
m_supportedLanguages.clear();
|
||||
if (m_client) {
|
||||
const QString out = m_client->rdwr(QStringLiteral("/ctl"),
|
||||
QStringLiteral("languages"));
|
||||
for (const QString &line : out.split('\n', Qt::SkipEmptyParts))
|
||||
m_supportedLanguages.append(line.trimmed().toLower());
|
||||
}
|
||||
m_languagesLoaded = true;
|
||||
}
|
||||
|
||||
return m_supportedLanguages.contains(lang.trimmed().toLower());
|
||||
}
|
||||
|
||||
void DocumentModel::loadDocument()
|
||||
{
|
||||
beginResetModel();
|
||||
|
|
|
|||
|
|
@ -60,6 +60,16 @@ public:
|
|||
// Reload from the server
|
||||
Q_INVOKABLE void reload();
|
||||
|
||||
// Execute the Nth source block (0-based, counting only src blocks) in the
|
||||
// section at the given visible row. Runs `/ctl execute <doc> <id> <n>`,
|
||||
// refreshes that section's body so the inserted #+RESULTS: becomes visible,
|
||||
// and returns the server's execution report.
|
||||
Q_INVOKABLE QString executeBlock(int row, int blockIndex);
|
||||
|
||||
// Whether a source block language can be executed by the server. Backed by
|
||||
// `/ctl languages` (loaded once and cached). Case-insensitive.
|
||||
Q_INVOKABLE bool isLanguageSupported(const QString &lang);
|
||||
|
||||
signals:
|
||||
void documentChanged();
|
||||
|
||||
|
|
@ -121,4 +131,8 @@ private:
|
|||
|
||||
// Visible row order (section ids)
|
||||
QStringList m_visibleRows;
|
||||
|
||||
// Supported src-block languages from `/ctl languages` (loaded once).
|
||||
mutable QStringList m_supportedLanguages;
|
||||
mutable bool m_languagesLoaded = false;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue