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 inCode = false;
|
||||||
let codeLang = "";
|
let codeLang = "";
|
||||||
let codeLines = [];
|
let codeLines = [];
|
||||||
|
let codeCounter = 0;
|
||||||
let inTable = false;
|
let inTable = false;
|
||||||
let tableLines = [];
|
let tableLines = [];
|
||||||
|
|
||||||
|
|
@ -87,7 +88,8 @@ ListView {
|
||||||
codeLang = trimmed.substring(11).trim().split(/\s+/)[0] || "";
|
codeLang = trimmed.substring(11).trim().split(/\s+/)[0] || "";
|
||||||
codeLines = [];
|
codeLines = [];
|
||||||
} else if (inCode && trimmed.toUpperCase() === "#+END_SRC") {
|
} 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;
|
inCode = false;
|
||||||
codeLang = "";
|
codeLang = "";
|
||||||
codeLines = [];
|
codeLines = [];
|
||||||
|
|
@ -134,7 +136,8 @@ ListView {
|
||||||
|
|
||||||
// Flush remaining
|
// Flush remaining
|
||||||
if (inCode && codeLines.length > 0) {
|
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) {
|
} else if (inTable && tableLines.length > 0) {
|
||||||
let rows = [];
|
let rows = [];
|
||||||
let hasHeader = false;
|
let hasHeader = false;
|
||||||
|
|
@ -368,16 +371,26 @@ ListView {
|
||||||
Component {
|
Component {
|
||||||
id: codeBlockComponent
|
id: codeBlockComponent
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
id: codeBlock
|
||||||
color: theme.alternateBaseColor
|
color: theme.alternateBaseColor
|
||||||
radius: outline.baseFontSize / 3
|
radius: outline.baseFontSize / 3
|
||||||
implicitHeight: codeColumn.implicitHeight + outline.baseFontSize
|
implicitHeight: codeColumn.implicitHeight + outline.baseFontSize
|
||||||
|
|
||||||
|
// Last execution status for this block: "", "running", "ok", "error"
|
||||||
|
property string runState: ""
|
||||||
|
property int runStatus: 0
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: codeColumn
|
id: codeColumn
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: outline.baseFontSize * 0.5
|
anchors.margins: outline.baseFontSize * 0.5
|
||||||
spacing: outline.baseFontSize * 0.3
|
spacing: outline.baseFontSize * 0.3
|
||||||
|
|
||||||
|
// Header row: language label + run control
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: outline.baseFontSize * 0.5
|
||||||
|
|
||||||
// Language label
|
// Language label
|
||||||
Label {
|
Label {
|
||||||
visible: (modelData.language ?? "") !== ""
|
visible: (modelData.language ?? "") !== ""
|
||||||
|
|
@ -387,6 +400,70 @@ ListView {
|
||||||
font.bold: true
|
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
|
// Code content
|
||||||
Label {
|
Label {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,57 @@ void DocumentModel::reload()
|
||||||
loadDocument();
|
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()
|
void DocumentModel::loadDocument()
|
||||||
{
|
{
|
||||||
beginResetModel();
|
beginResetModel();
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,16 @@ public:
|
||||||
// Reload from the server
|
// Reload from the server
|
||||||
Q_INVOKABLE void reload();
|
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:
|
signals:
|
||||||
void documentChanged();
|
void documentChanged();
|
||||||
|
|
||||||
|
|
@ -121,4 +131,8 @@ private:
|
||||||
|
|
||||||
// Visible row order (section ids)
|
// Visible row order (section ids)
|
||||||
QStringList m_visibleRows;
|
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