Skip to content

Snippets#287

Merged
pbek merged 5 commits intoqownnotes:mainfrom
luginf:snippets-v2
May 2, 2026
Merged

Snippets#287
pbek merged 5 commits intoqownnotes:mainfrom
luginf:snippets-v2

Conversation

@luginf
Copy link
Copy Markdown
Contributor

@luginf luginf commented Apr 30, 2026

No description provided.

Comment thread snippets/info.json
"minAppVersion": "26.4.11",
"description": "Insert and manage reusable text snippets with placeholders ($CURRENT_YEAR, $CURRENT_DATE, $UUID…) into notes.",
"resources": [
"InsertSnippetDialog.qml",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't see that file in the PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it was missing, I'm adding it now

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new “Text Snippets” scripting extension to insert reusable text templates (with placeholders) into QOwnNotes notes, plus a UI for managing snippet definitions stored in JSON.

Changes:

  • Introduces snippets.qml script with placeholder processing (date/time, note context, OS name, ZK-style IDs) and custom actions for inserting/managing snippets.
  • Adds a “Manage Snippets” QML dialog for creating/editing/deleting snippets.
  • Adds extension metadata (info.json), bundled default snippets (snippets.json), and user documentation (README.md).

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
snippets/snippets.qml Core script: actions, placeholder expansion, snippet persistence, dialog loading.
snippets/ManageSnippetsDialog.qml UI to edit snippet name/content and manage the snippet list.
snippets/snippets.json Default snippet definitions bundled with the script.
snippets/info.json Extension manifest (name/id/version/resources/min app version).
snippets/README.md User-facing documentation of usage and placeholders.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread snippets/snippets.qml Outdated
}

function saveSnippets(snippets) {
script.writeToFile(snippetsFilePath(), JSON.stringify(snippets, null, 2), false);
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

saveSnippets calls script.writeToFile(...) with a third argument (false). In this repo, writeToFile is consistently invoked with only (filePath, content), and passing an extra argument may throw a runtime "too many arguments" error and prevent snippets from being saved. Use the standard 2-argument call (and consider checking the return value to detect write failures).

Suggested change
script.writeToFile(snippetsFilePath(), JSON.stringify(snippets, null, 2), false);
script.writeToFile(snippetsFilePath(), JSON.stringify(snippets, null, 2));

Copilot uses AI. Check for mistakes.
Comment thread snippets/snippets.qml
Comment on lines +119 to +135
var component = Qt.createComponent(Qt.resolvedUrl("InsertSnippetDialog.qml"));
if (component.status !== Component.Ready) {
script.log("snippets: failed to load InsertSnippetDialog — " + component.errorString());
return;
}
var dialog = component.createObject(null, {
"entries": entries
});
if (!dialog) {
script.log("snippets: failed to instantiate InsertSnippetDialog");
return;
}
// Re-process placeholders at insertion time so timestamps are fresh.
dialog.snippetChosen.connect(function (originalIndex) {
script.noteTextEditWrite(processPlaceholders(snippets[originalIndex].content));
});
dialog.manageRequested.connect(function () {
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

insertSnippet() loads InsertSnippetDialog.qml, but that file is not present in the snippets/ directory. This will make the main "Insert snippet" action fail at runtime. Add the missing QML resource (and ensure it is included in info.json resources), or update the code/resources to reference the correct dialog filename.

Suggested change
var component = Qt.createComponent(Qt.resolvedUrl("InsertSnippetDialog.qml"));
if (component.status !== Component.Ready) {
script.log("snippets: failed to load InsertSnippetDialog — " + component.errorString());
return;
}
var dialog = component.createObject(null, {
"entries": entries
});
if (!dialog) {
script.log("snippets: failed to instantiate InsertSnippetDialog");
return;
}
// Re-process placeholders at insertion time so timestamps are fresh.
dialog.snippetChosen.connect(function (originalIndex) {
script.noteTextEditWrite(processPlaceholders(snippets[originalIndex].content));
});
dialog.manageRequested.connect(function () {
var dialogQml =
'import QtQuick 2.15\n' +
'import QtQuick.Controls 2.15\n' +
'import QtQuick.Layouts 1.15\n' +
'Window {\n' +
' id: root\n' +
' width: 720\n' +
' height: 480\n' +
' visible: false\n' +
' title: "Insert snippet"\n' +
' modality: Qt.ApplicationModal\n' +
' flags: Qt.Dialog\n' +
' property var entries: []\n' +
' signal snippetChosen(int originalIndex)\n' +
' signal manageRequested()\n' +
' ColumnLayout {\n' +
' anchors.fill: parent\n' +
' anchors.margins: 12\n' +
' spacing: 8\n' +
' Label {\n' +
' text: "Choose a snippet to insert"\n' +
' font.bold: true\n' +
' }\n' +
' ListView {\n' +
' id: snippetList\n' +
' Layout.fillWidth: true\n' +
' Layout.fillHeight: true\n' +
' clip: true\n' +
' model: root.entries\n' +
' currentIndex: model.length > 0 ? 0 : -1\n' +
' delegate: ItemDelegate {\n' +
' width: snippetList.width\n' +
' highlighted: ListView.isCurrentItem\n' +
' onClicked: snippetList.currentIndex = index\n' +
' onDoubleClicked: {\n' +
' root.snippetChosen(modelData.originalIndex)\n' +
' root.close()\n' +
' root.destroy()\n' +
' }\n' +
' contentItem: Column {\n' +
' spacing: 4\n' +
' Label {\n' +
' text: modelData.name\n' +
' font.bold: true\n' +
' }\n' +
' Label {\n' +
' text: modelData.preview\n' +
' wrapMode: Text.Wrap\n' +
' maximumLineCount: 3\n' +
' elide: Text.ElideRight\n' +
' opacity: 0.8\n' +
' }\n' +
' }\n' +
' }\n' +
' }\n' +
' RowLayout {\n' +
' Layout.fillWidth: true\n' +
' Button {\n' +
' text: "Manage…"\n' +
' onClicked: root.manageRequested()\n' +
' }\n' +
' Item { Layout.fillWidth: true }\n' +
' Button {\n' +
' text: "Cancel"\n' +
' onClicked: {\n' +
' root.close()\n' +
' root.destroy()\n' +
' }\n' +
' }\n' +
' Button {\n' +
' text: "Insert"\n' +
' enabled: snippetList.currentIndex >= 0\n' +
' onClicked: {\n' +
' if (snippetList.currentIndex >= 0) {\n' +
' root.snippetChosen(root.entries[snippetList.currentIndex].originalIndex)\n' +
' root.close()\n' +
' root.destroy()\n' +
' }\n' +
' }\n' +
' }\n' +
' }\n' +
' }\n' +
'}\n';
var dialog = Qt.createQmlObject(dialogQml, script, "InsertSnippetDialogInline");
if (!dialog) {
script.log("snippets: failed to instantiate inline InsertSnippetDialog");
return;
}
dialog.entries = entries;
// Re-process placeholders at insertion time so timestamps are fresh.
dialog.snippetChosen.connect(function (originalIndex) {
script.noteTextEditWrite(processPlaceholders(snippets[originalIndex].content));
});
dialog.manageRequested.connect(function () {
dialog.close();
dialog.destroy();

Copilot uses AI. Check for mistakes.
Comment thread snippets/info.json
"minAppVersion": "26.4.11",
"description": "Insert and manage reusable text snippets with placeholders ($CURRENT_YEAR, $CURRENT_DATE, $UUID…) into notes.",
"resources": [
"InsertSnippetDialog.qml",
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resources lists InsertSnippetDialog.qml, but that file is not included under snippets/ in this PR. Either add the missing file or remove it from resources to keep the manifest accurate (otherwise installation/loading will fail).

Suggested change
"InsertSnippetDialog.qml",

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread snippets/README.md Outdated
Comment on lines +45 to +47
`$ZK_ID` generates a Zettelkasten ID using the same format string syntax as the
[Zettelkasten extension](../zettelkasten/). Configure **Zettelkasten ID format**
in _Settings → Scripting → Text Snippets_ to match the format you use there.
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README links to ../zettelkasten/, but there is no zettelkasten/ directory in this repository, so the link is broken. Point this to the correct script/location (if it exists under a different name) or remove the link and describe the format-token compatibility without referencing a non-existent path.

Suggested change
`$ZK_ID` generates a Zettelkasten ID using the same format string syntax as the
[Zettelkasten extension](../zettelkasten/). Configure **Zettelkasten ID format**
in _Settings → Scripting → Text Snippets_ to match the format you use there.
`$ZK_ID` generates a Zettelkasten ID using the format string syntax documented
below. Configure **Zettelkasten ID format** in _Settings → Scripting → Text
Snippets_ to match the token format you use for your Zettelkasten IDs.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

README.md — Broken ../zettelkasten/ link replaced with a self-contained description of the format.

Comment thread snippets/ManageSnippetsDialog.qml Outdated
var copy = items.slice();
copy.splice(idx, 1);
items = copy;
snippetsSaved(items);
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The delete (−) action emits snippetsSaved(items) immediately, while edits/additions require the user to click "Save". This mixes immediate-persist and explicit-save semantics in the same dialog and can lead to accidental irreversible deletes. Consider making deletes follow the same Save/isDirty flow (or switching the whole dialog to auto-save and removing the Save button).

Suggested change
snippetsSaved(items);

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ManageSnippetsDialog.qml — Deletion now sets listDirty = true instead of saving immediately. The Save
button is enabled when listDirty || isDirty, and saveCurrentItem works even with no item selected (empty
list after deleting all).

Comment thread snippets/snippets.json Outdated
Comment on lines +7 to +10
"name": "test",
"content": "just a test "
},
{
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

snippets.json ships with a snippet named "test" (with trailing whitespace). Since this file is bundled as a resource and will appear for all users by default, consider removing placeholder/test entries (or moving them to documentation) so the default snippet set looks intentional and production-ready.

Suggested change
"name": "test",
"content": "just a test "
},
{

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment thread snippets/snippets.qml Outdated
"windows": "Windows",
"unix": "Unix"
};
return text.replace(/\$CURRENT_YEAR_SHORT/g, String(now.getFullYear()).slice(-2)).replace(/\$CURRENT_YEAR/g, String(now.getFullYear())).replace(/\$CURRENT_MONTH_NAME_SHORT/g, loc.monthName(now.getMonth(), 1)).replace(/\$CURRENT_MONTH_NAME/g, loc.monthName(now.getMonth(), 0)).replace(/\$CURRENT_MONTH/g, pad(now.getMonth() + 1)).replace(/\$CURRENT_DATE/g, pad(now.getDate())).replace(/\$CURRENT_HOUR/g, pad(now.getHours())).replace(/\$CURRENT_MINUTE/g, pad(now.getMinutes())).replace(/\$CURRENT_SECOND/g, pad(now.getSeconds())).replace(/\$CURRENT_SECONDS_UNIX/g, String(Math.floor(now.getTime() / 1000))).replace(/\$UUID/g, generateUUID()).replace(/\$NOTE_TITLE/g, note ? note.name : "").replace(/\$NOTE_FILENAME/g, note ? note.fileName : "").replace(/\$OS_NAME/g, osMap[Qt.platform.os] || Qt.platform.os).replace(/\$ZK_ID/g, generateZkId());
Copy link

Copilot AI May 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

processPlaceholders replaces $CURRENT_SECOND before $CURRENT_SECONDS_UNIX. Because $CURRENT_SECONDS_UNIX starts with $CURRENT_SECOND, the earlier replacement will partially consume it and the Unix-timestamp placeholder will never resolve correctly. Reorder replacements so longer placeholders are handled first (or use a single-pass placeholder map / regex alternation).

Suggested change
return text.replace(/\$CURRENT_YEAR_SHORT/g, String(now.getFullYear()).slice(-2)).replace(/\$CURRENT_YEAR/g, String(now.getFullYear())).replace(/\$CURRENT_MONTH_NAME_SHORT/g, loc.monthName(now.getMonth(), 1)).replace(/\$CURRENT_MONTH_NAME/g, loc.monthName(now.getMonth(), 0)).replace(/\$CURRENT_MONTH/g, pad(now.getMonth() + 1)).replace(/\$CURRENT_DATE/g, pad(now.getDate())).replace(/\$CURRENT_HOUR/g, pad(now.getHours())).replace(/\$CURRENT_MINUTE/g, pad(now.getMinutes())).replace(/\$CURRENT_SECOND/g, pad(now.getSeconds())).replace(/\$CURRENT_SECONDS_UNIX/g, String(Math.floor(now.getTime() / 1000))).replace(/\$UUID/g, generateUUID()).replace(/\$NOTE_TITLE/g, note ? note.name : "").replace(/\$NOTE_FILENAME/g, note ? note.fileName : "").replace(/\$OS_NAME/g, osMap[Qt.platform.os] || Qt.platform.os).replace(/\$ZK_ID/g, generateZkId());
var placeholders = {
"$CURRENT_YEAR_SHORT": String(now.getFullYear()).slice(-2),
"$CURRENT_YEAR": String(now.getFullYear()),
"$CURRENT_MONTH_NAME_SHORT": loc.monthName(now.getMonth(), 1),
"$CURRENT_MONTH_NAME": loc.monthName(now.getMonth(), 0),
"$CURRENT_MONTH": pad(now.getMonth() + 1),
"$CURRENT_DATE": pad(now.getDate()),
"$CURRENT_HOUR": pad(now.getHours()),
"$CURRENT_MINUTE": pad(now.getMinutes()),
"$CURRENT_SECOND": pad(now.getSeconds()),
"$CURRENT_SECONDS_UNIX": String(Math.floor(now.getTime() / 1000)),
"$UUID": generateUUID(),
"$NOTE_TITLE": note ? note.name : "",
"$NOTE_FILENAME": note ? note.fileName : "",
"$OS_NAME": osMap[Qt.platform.os] || Qt.platform.os,
"$ZK_ID": generateZkId()
};
return text.replace(/\$(?:CURRENT_SECONDS_UNIX|CURRENT_YEAR_SHORT|CURRENT_YEAR|CURRENT_MONTH_NAME_SHORT|CURRENT_MONTH_NAME|CURRENT_MONTH|CURRENT_DATE|CURRENT_HOUR|CURRENT_MINUTE|CURRENT_SECOND|UUID|NOTE_TITLE|NOTE_FILENAME|OS_NAME|ZK_ID)/g, function (match) {
return placeholders[match];
});

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

snippets.qml — processPlaceholders now uses a single-pass regex with $CURRENT_SECONDS_UNIX prioritized, preventing $CURRENT_SECOND from partially consuming it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

everything should be done now

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to fix the formatting, while you are at it?

@luginf luginf changed the title Snippets v2 Snippets May 1, 2026
@pbek pbek merged commit fff60b9 into qownnotes:main May 2, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants