Compare commits

...

34 commits
0.1.6 ... main

Author SHA1 Message Date
Spencer Gouw
18204f227c chore: update manifest/versions 2023-10-08 19:26:55 -05:00
Spencer Gouw
8a88ae3a68 refac: move SnippetType into snippet.ts 2023-10-08 19:26:36 -05:00
Spencer Gouw
e05c88e941 refac: put extra .ts files into src/ 2023-10-08 18:55:01 -05:00
Spencer Gouw
7313ef9d44 Merge branch 'main' of https://github.com/rabirabirara/obsidian-jelly-snippets 2023-10-08 18:54:25 -05:00
Spencer Gouw
2e1764a3bf fix: use EditorView (CM) to dispatch changes 2023-10-08 18:54:15 -05:00
Spencer Gouw
4a50f993b7
Update README.md 2023-10-08 13:08:45 -07:00
Spencer Gouw
5737b07d36 chore: update manifest.json to 0.1.8 2023-10-08 04:07:38 -05:00
Spencer Gouw
bbc462f18c
Update README.md 2023-10-08 02:03:38 -07:00
Spencer Gouw
56935ca760 Merge branch 'main' of https://github.com/rabirabirara/obsidian-jelly-snippets 2023-10-08 03:59:45 -05:00
Spencer Gouw
eba93321df fix: stop trimming LHS, instead split by div\n 2023-10-08 03:59:25 -05:00
Spencer Gouw
32fc03607c
Update README.md 2023-09-24 19:40:38 -07:00
Spencer Gouw
6e15adc2d4 Merge branch 'main' of https://github.com/rabirabirara/obsidian-jelly-snippets 2023-09-24 21:13:08 -05:00
Spencer Gouw
257616bf9f chore: update manifest/versions 2023-09-24 21:12:30 -05:00
Spencer Gouw
8cfee7d4dd
Update README.md 2023-09-23 15:20:42 -07:00
Spencer Gouw
27e04f24ee
Update README.md
small tutorial and todos updating
2023-09-23 15:20:19 -07:00
Spencer Gouw
7e1c6f7954 feat: add Space to symbol list 2023-09-23 17:12:31 -05:00
Spencer Gouw
6b6787a552 feat: rewrite auto-enter logic to make more sense and work with symbols
AutoTriggerEnter logic had a few problems. It wasn't generalized at all and very cumbersome.
There is a simple way to think about the snippet-replacement cycle works.

1. You press enter. A newline is emitted *first*.
2. Peek up and to the right (your old position). Try snippet.
3. If didn't trigger, keep the cursor in the new position.
4. If did trigger, the cursor has moved to cursorEnd.
5. Get rid of the extra newline. It should be between the end of the replacement and the line after it.
6. If YesWS, then add a newline right where you are.

This is how the new logic works.
2023-09-23 16:47:52 -05:00
Spencer Gouw
66ba56328c doc: add comment for return undefined 2023-09-23 16:46:42 -05:00
Spencer Gouw
998489b576 fix: getSnippetType now properly ORs a number
The importance of writingn unit tests.
2023-09-23 13:12:02 -05:00
Spencer Gouw
b10ec1ade3 fix: delete only eol with autotrig enter nows 2023-09-22 22:38:05 -05:00
Spencer Gouw
8cf68cfc87 fix: start endFoundIdx at -1 2023-09-22 22:14:57 -05:00
Spencer Gouw
b769bf70be refac: properly acknowledge __MR snippet type info
Does not fix interaction with auto trigger by enter yet.
2023-09-22 02:44:57 -05:00
Spencer Gouw
641fef15df clean/feat: integrate symbols with main and clean old comments 2023-09-22 02:22:02 -05:00
Spencer Gouw
7c75776464 feat: replace symbols works 2023-09-22 02:09:10 -05:00
Spencer Gouw
65572b4632 feat: basic snippet data structure 2023-09-21 22:30:59 -05:00
Spencer Gouw
ebea47cde8 feat: symbol replace and more advanced enum 2023-09-21 22:03:46 -05:00
Spencer Gouw
18e4c2e28f feat: basic symbol enum 2023-09-21 21:42:41 -05:00
Spencer Gouw
86ba8bfade revert a4c6600
This doesn't work if the user doesn't add newlines for the snippets. It's also super manual and hacky. Better to have a solution that doesn't require precision.
2023-09-21 21:36:19 -05:00
Spencer Gouw
a17c4d0dd4 clean: some prints removed 2023-09-18 01:23:06 -05:00
Spencer Gouw
2236c94372 clean: some comments cleaned, one print adjusted 2023-09-18 01:21:57 -05:00
Spencer Gouw
56dd23b30d Merge branch 'main' of https://github.com/rabirabirara/obsidian-jelly-snippets 2023-08-31 00:22:13 -05:00
Spencer Gouw
a4c6600aaf feat: allow whitespace at ends of snippets 2023-08-31 00:22:06 -05:00
Spencer Gouw
b8984d2e69
Update README.md 2023-08-30 21:59:45 -07:00
Spencer Gouw
b43bd93d0f
Update README.md 2023-08-23 07:13:08 -07:00
5 changed files with 236 additions and 110 deletions

View file

@ -1,6 +1,9 @@
# jelly-snippets
A simple text snippets plugin for Obsidian.md.
## BEFORE UPDATING,READ:
If you have used an older version of this plugin (1.5) or below, updating to version 1.6 will erase your snippets. Backup your snippets before updating; I will update the plugin with a backup functionality later too in case I mess up again. Deepest apologies.
## What does it do?
@ -26,7 +29,9 @@ Pretty simple. You probably have run into snippets before.
Snippets are defined in a text area (like a file) in settings. Each snippet has two parts: lhs and rhs.
The lhs (left hand side) determines what text gets replaced; the rhs (right hand side) determines what replaces the text.
##### The lhs (left hand side) determines what text gets replaced; the rhs (right hand side) determines what replaces the text.
##### The lhs is *exactly* the text that will be replaced, while the rhs can have certain symbols to represent things like whitespace.
Naturally, there is a symbol to divide these two halves of a snippet, called a snippet part divider:
@ -40,6 +45,8 @@ And, there is a symbol to divide each separate snippet in the list of snippets,
`-==-`, for example.
You must keep the snippet divider on their own line - that is, the plugin expects a newline after the snippet divider. (Implicitly, no two snippets can be on the same line.)
So if you had the following snippets file:
```
@ -61,7 +68,34 @@ Then typing `lhs` and triggering the snippet command would replace `lhs` with:
rhs
superb |+| superbowls
```
## Using Symbols (simple)
Say you want to expand `hw` into a full hello-world function. Say we're using Rust. You define the snippet:
```
hw |+| fn hello() {
%\t%\e
}
```
When you trigger the snippet, you'll end up with this:
```
fn hello() {
|
}
```
... where `|` represents your cursor. Of course, Obsidian is a markdown editor, so expect the little indent guide.
#### List of supported symbols
- Newline: `%\n`
- Tab: `%\t`
- Space: `%\s`
- Cursor Ending position (after snippet replacement): `%\e`
## Why?
I wanted to make a generic snippets plugin that operated on text and worked as I needed. There is an existing snippets plugin already, [Text Snippets](https://github.com/ArianaKhit/text-snippets-obsidian) by ArianaKhit, but not only is the plugin code somewhat outdated and a little complex, it seems to use an older API. (It is a very good plugin by the way.)
@ -76,13 +110,16 @@ It's since been incorporated back into [julia.vim](https://github.com/JuliaEdito
## Future Improvements/TODO
- [ ] Control characters in snippets (e.g. whitespace, particularly newlines? currently snippets are trimmed on both ends; should probably not trim spaces)
- [ ] Semantic symbols in snippets (e.g. where does cursor go afterwards? a snippet with braces might benefit from placing the cursor inside...)
- [ ] Regex capabilities
- [ ] Bugfixes and auditing?
- [x] Add newline option for snippet definition to make simple snippets easy rather than cumbersome
- [x] Control characters in snippets (whitespace only)
- [x] Semantic symbols in snippets (e.g. where does cursor go afterwards? a snippet with braces might benefit from placing the cursor inside...)
- [ ] Regex capabilities - at least on selection.
- [ ] A backup for snippets - or a way of specifying snippets in a file, to modularize snippets. May be useful if regex snippets need to be defined separately from text snippets.
- [ ] Bugfixes and auditing.
- [ ] AUTOMATED TESTING. Use Jest.
- [ ] Do the things liamcain suggested in my plugin PR to the Obsidian plugin repo. (one half done)
- [ ] Working with Obsidians template stuff.
- [ ] REWRITE: CodeMirror editor extension. [See here on state fields.](https://docs.obsidian.md/Plugins/Editor/State+fields) This should also make things such as tabstops and working in live preview possible, but I don't know for sure. This would mark a version upgrade.
### Other caveats
- The lhs of a snippet (currently) cannot have newlines in it. Snippets of that sort will simply fail to work; they'll register but never trigger. The reason why is because I only search from cursor to start of line for snippet text; this is efficient and simple. To fix, we'd have to look backwards from the cursor position and match with the lhs of the snippet. Probably not a hard update, but given that this use case is somewhat rare (imo), I have neglected it. Do submit an issue. EDIT: the issue was submitted. Time to get to work.
- None right now, except that the maintainer of this plugin (me) is inexperienced and may break something. Keep your snippets safe if you write a lot of them.

186
main.ts
View file

@ -8,19 +8,17 @@ import {
Setting,
} from "obsidian";
import { EditorView } from "@codemirror/view";
import { Symbol } from "src/symbol";
import { LHS, RHS, Snippet, SnippetType } from "src/snippet";
enum AutoTriggerOptions {
Disabled = "disabled",
EnabledNoWS = "n-ws",
EnabledYesWS = "y-ws",
}
enum SnippetType {
SLSR = 0,
SLMR = 1,
MLSR = 2,
MLMR = 3,
}
interface JellySnippetsSettings {
snippetsFile: string;
triggerOnSpace: AutoTriggerOptions;
@ -28,10 +26,7 @@ interface JellySnippetsSettings {
triggerOnTab: AutoTriggerOptions;
snippetPartDivider: string;
snippetDivider: string;
// postSnippetCursorSymbol: string; // TODO
}
// TODO: implement cursor move after snippet replace.
// ? TODO: Can we implement growable lists in settings?
const DEFAULT_SETTINGS: JellySnippetsSettings = {
snippetsFile: String.raw`Snip me! |+| Snippet successfully replaced.
@ -46,26 +41,14 @@ const DEFAULT_SETTINGS: JellySnippetsSettings = {
triggerOnTab: AutoTriggerOptions.Disabled,
snippetPartDivider: " |+| ",
snippetDivider: "-==-",
// postSnippetCursorSymbol: "%move%", // TODO: Actually implement this symbol.
};
// TODO: Add semantic symbols to represent certain special characters.
// TODO: Also implement those semantic symbols for control characters.
// Specifically, allow me to add a "whitespace" symbol so that I can put newlines in snippets if I need.
// TODO: I should use regexable snippets. Or at least implement it somehow somewhere.
// regex: ^.*(all the whitespace, word delimiters)<snippet regex>
/*
The thing about regex snippets is that the more power we want to add, the harder it is to implement.
! Also, need to test safety of regex... use safe-regex (npm)
*/
export default class JellySnippets extends Plugin {
settings: JellySnippetsSettings;
private multilineSnippets: { [key: string]: string } = {};
private multilineSnippets: { [key: LHS]: RHS } = {};
async onload() {
await this.loadSettings();
console.log("load");
// Check settings and load snippets in.
this.reloadSnippets();
@ -97,7 +80,6 @@ export default class JellySnippets extends Plugin {
})
);
}
console.log("load 2");
this.addCommand({
id: "trigger-snippet",
@ -133,44 +115,47 @@ export default class JellySnippets extends Plugin {
}
reloadSnippets(): void {
console.log("reloading");
console.log("Jelly Snippets: Reloading snippets.");
this.multilineSnippets = {};
this.parseSnippets();
}
parseSnippets(): void {
// If they specified newline control character, split snippets by newline.
// ! If so, there can be no multiline snippets!
// TODO: Can we make it so that leaving the setting blank is newline separation?
let snippetDivider =
this.settings.snippetDivider == "\\n"
? "\n"
: this.settings.snippetDivider;
: this.settings.snippetDivider + "\n";
// go through the snippets file, split by the snippet divider, split by the part divider, put in map
let snippetLines = this.settings.snippetsFile.split(snippetDivider);
for (let snippet of snippetLines) {
// trim() is used so that each snippet line does not retain newlines.
// TODO: Again, control characters or semantic symbols in lhs.
// Trim newlines. Instead, use symbols to let people insert whitespace.
// This split means only the first division of the part divider is the LHS.
let snippetParts = snippet
.trim()
.trimEnd()
.split(this.settings.snippetPartDivider);
if (snippetParts.length !== 2) {
// probably an incomplete snippet
continue;
}
// Produce lhs. Continue if undefined.
let lhs = snippetParts.shift();
console.log(lhs);
let rhs = snippetParts.join(this.settings.snippetPartDivider);
if (lhs === undefined) {
console.log("Failed to register snippet: ", snippet);
} else {
this.multilineSnippets[lhs] = rhs;
continue;
}
// Produce rhs (raw data).
// * This is a join in case they used their snippetPartDivider too many times.
let rhsData = snippetParts.join(this.settings.snippetPartDivider);
// Scan rhs for symbols and perform the replacements; acquire RHS.
let rhs = Symbol.replaceSymbolsOnParse(rhsData);
this.multilineSnippets[lhs] = rhs;
}
console.log(this.multilineSnippets);
}
triggerSnippet(
editor: Editor,
pos?: EditorPosition
): [string, string] | undefined {
triggerSnippet(editor: Editor, pos?: EditorPosition): Snippet | undefined {
let curpos = pos ? pos : editor.getCursor();
return this.triggerMultilineSnippet(editor, curpos);
}
@ -183,7 +168,6 @@ export default class JellySnippets extends Plugin {
this.settings.triggerOnSpace !== AutoTriggerOptions.Disabled
) {
if (this.triggerSnippet(editor)) {
// TODO: actually provide autotriggeroptions for Space.
// Currently impossible to undo the space because the entire snippet
// and all this code triggers before the space actually happens.
return true;
@ -224,7 +208,7 @@ export default class JellySnippets extends Plugin {
) {
return false;
}
// Since the newline comes out first, we need to track where our old position was before newline.
// Since the newline comes out first, we need to find our old position before newline (peekPos).
let curpos = editor.getCursor();
let aboveline = curpos.line - 1;
let abovelineEnd = editor.getLine(aboveline).length;
@ -232,45 +216,43 @@ export default class JellySnippets extends Plugin {
line: aboveline,
ch: abovelineEnd,
};
// Try to trigger the snippet at the old position.
let maybeSnippet = this.triggerSnippet(editor, peekPos);
// If the snippet triggered, clean up the newline.
if (maybeSnippet) {
let snippetType = this.getSnippetType(maybeSnippet);
// delete newline at the end of snippet
let curpos = editor.getCursor();
let curoffset = editor.posToOffset(curpos);
// get end of replacement part
let rhsEndOffset =
curoffset + maybeSnippet.rhs.info.cursorEnd;
let rhsEndPos = editor.offsetToPos(rhsEndOffset);
// the line after that (works at document end)
let afterSnippetLinePos = {
line: rhsEndPos.line + 1,
ch: 0,
};
editor.replaceRange("", rhsEndPos, afterSnippetLinePos);
// If they want the newline added where the cursor is at (works weird for cursorEnd snippets but eh)
// TODO: maybe make another setting where add newline on enter is disabled if snippet has a cursorEnd?
if (
this.settings.triggerOnEnter ===
AutoTriggerOptions.EnabledNoWS
AutoTriggerOptions.EnabledYesWS
) {
// NoWS
if (snippetType === SnippetType.MLSR) {
// RCNN - do nothing
} else {
// RCNDMU - delete from curpos to start of next line
let curpos = editor.getCursor();
let nextLine = curpos.line + 1;
let nextLineStartPos: EditorPosition = {
line: nextLine,
// Insert newline where we are at.
editor.exec("newlineAndIndent");
curpos = editor.getCursor();
// To "de-indent", delete until start of line.
editor.replaceRange(
"",
{
line: curpos.line,
ch: 0,
};
editor.replaceRange("", curpos, nextLineStartPos);
}
} else {
// YesWS
let curpos = editor.getCursor();
if (snippetType === SnippetType.MLSR) {
// RCNN - insert newline ("repeat enter") / replace curpos with newline
editor.exec("newlineAndIndent");
editor.exec("indentLess");
} else {
// RCNDMU - move to start of next line / to the right
editor.exec("goRight");
// * To calculate it:
// let nextLine = curpos.line + 1;
// let nextLineStartPos: EditorPosition = {
// line: nextLine,
// ch: 0,
// };
// editor.setCursor(nextLineStartPos);
}
},
curpos
);
}
return true;
@ -290,35 +272,41 @@ export default class JellySnippets extends Plugin {
triggerMultilineSnippet(
editor: Editor,
pos?: EditorPosition
): [string, string] | undefined {
): Snippet | undefined {
const curpos = pos ? pos : editor.getCursor();
const curoffset = editor.posToOffset(curpos);
// @ts-expect-error
const view = editor.cm as EditorView;
for (let [lhs, rhs] of Object.entries(this.multilineSnippets)) {
if (!this.selectBackN(editor, lhs.length, curpos)) {
// console.log(
// "Error: failed to select back N at: " +
// pos +
// " with lhs: " +
// lhs
// );
continue;
}
const from = curoffset - lhs.length;
const to = curoffset;
// Get the text just before the cursor
let selected = view.state.sliceDoc(from, to);
// If the selected string is the LHS, replace it!
let selected = editor.getSelection();
if (lhs === selected) {
editor.replaceSelection(rhs);
// Dispatch the replacement and move the cursor to where it should be.
view.dispatch({
changes: [
{
from,
to,
insert: rhs.data,
},
],
selection: {
anchor: from + rhs.data.length - rhs.info.cursorEnd,
},
});
// Reset selection to where the cursor is *after* replacement.
// Allows "enabled-with-whitespace" auto replacements to work.
this.unselect(editor);
return [lhs, rhs];
// Return what you replaced.
return { lhs, rhs };
}
// Reset selection to where the cursor is.
this.unselect(editor, curpos);
}
// No replace - return undefined
return;
}
@ -326,12 +314,12 @@ export default class JellySnippets extends Plugin {
// Single snippets (no newlines) and multi snippets (newlines)
// have different, weird interaction with Obsidian. Sometimes the whitespace goes through, sometimes not.
// There needs to be a way of determining what type (mlhs->srhs? mlhs->mrhs? slhs? srhs? etc.) a snippet is.
getSnippetType(snippet: [string, string]): SnippetType {
let [lhs, rhs] = snippet;
getSnippetType(snippet: Snippet): SnippetType {
let { lhs, rhs } = snippet;
let type = SnippetType.SLSR;
// Compiler doesn't complain if we convert boolean to number with unary '+'.
type |= +lhs.contains("\n") && SnippetType.MLSR;
type |= +rhs.contains("\n") && SnippetType.SLMR;
type |= +lhs.includes("\n") ? SnippetType.MLSR : 0;
type |= +rhs.info.hasNewline ? SnippetType.SLMR : 0;
return type;
}

View file

@ -1,10 +1,10 @@
{
"id": "jelly-snippets",
"name": "Jelly Snippets",
"version": "0.1.6",
"version": "0.2.0",
"minAppVersion": "0.15.0",
"description": "A simple plugin for text snippets, with auto replacement",
"author": "Spencer Gouw",
"authorUrl": "github.com/rabirabirara",
"isDesktopOnly": false
}
}

20
src/snippet.ts Normal file
View file

@ -0,0 +1,20 @@
export type LHS = string;
export interface RHS {
data: string;
info: {
hasNewline: boolean;
cursorEnd: number;
};
}
export type Snippet = {
lhs: LHS;
rhs: RHS;
};
export enum SnippetType {
SLSR = 0,
SLMR = 1,
MLSR = 2,
MLMR = 3,
}

81
src/symbol.ts Normal file
View file

@ -0,0 +1,81 @@
import { RHS } from "src/snippet";
export enum Symbol {
// on parse
Newline = "%\\n",
Tab = "%\\t",
Space = "%\\s",
// on replace
CursorEnd = "%\\e",
}
export namespace Symbol {
const REPLACEABLE: Record<string, string> = {
[Symbol.Newline]: "\n",
[Symbol.Tab]: "\t",
[Symbol.Space]: " ",
};
// const ON_REPLACE: Record<string, string> = {
// [Symbol.CursorEnd]: "",
// };
export function replaceSymbolsOnParse(inputStr: string): RHS {
const result: string[] = [];
const len = inputStr.length;
let i = 0;
let cursor = 0;
let endFoundIdx = -1;
let hasNewline = false;
let found;
while (i < inputStr.length) {
found = false;
for (const symbol in REPLACEABLE) {
if (inputStr.startsWith(symbol, i)) {
result.push(REPLACEABLE[symbol]);
cursor += REPLACEABLE[symbol].length;
i += symbol.length;
found = true;
if (symbol == Symbol.Newline) {
hasNewline = true;
}
break;
}
}
// Check for cursorEnd symbol.
if (inputStr.startsWith(Symbol.CursorEnd, i)) {
i += Symbol.CursorEnd.length;
// cursor marks the index in final string, we derive the "how far to move left" from it
endFoundIdx = cursor;
found = true;
}
if (!found) {
result.push(inputStr[i]);
cursor++;
i++;
}
}
let data = result.join("");
let info = {
hasNewline,
cursorEnd: endFoundIdx < 0 ? 0 : data.length - endFoundIdx,
};
return { data, info };
}
}
// Some tests!
// let rhs1 = Symbol.replaceSymbolsOnParse("123%\\e45678");
// console.log(rhs1);
// let rhs2 = Symbol.replaceSymbolsOnParse("12345678%\\e");
// console.log(rhs2);
// let rhs3 = Symbol.replaceSymbolsOnParse("%\\e12345678");
// console.log(rhs3);
// let rhs4 = Symbol.replaceSymbolsOnParse("12%\\e3");
// console.log(rhs4);
// let rhs5 = Symbol.replaceSymbolsOnParse("1%\\e23");
// console.log(rhs5);
// let rhs6 = Symbol.replaceSymbolsOnParse("%\\e123");
// console.log(rhs6);
// let rhs7 = Symbol.replaceSymbolsOnParse("123%\\n4%\\e56");
// console.log(rhs7);
// console.log(rhs7.data);