Compare commits

...

27 commits

Author SHA1 Message Date
Idan Liberman
309d6b95e1
Update README.md 2026-05-07 23:47:52 +03:00
idanl
aa1d2be2a3 Correct version in package.json 2026-04-29 01:09:13 +03:00
idanl
410283d24c small doc patches 2026-04-29 01:05:38 +03:00
Idan Liberman
2660b8c365
Add GitHub action (#27) 2026-04-29 00:43:42 +03:00
idanl
e510be4afb testing for authentication 2026-04-29 00:00:29 +03:00
idanl
91432db0ec Add release workflow 2026-04-28 23:59:47 +03:00
idanlib
e95bc5b1ba fix type in readme 2026-03-13 15:34:54 +02:00
Idan Liberman
d65390304b
Merge pull request #25 from IdanLib/code-review-2b
Code review 2b
2025-08-03 01:19:06 +03:00
idanlib
789c1a830e code review 2b - reorg, version 1.1 + update readme 2025-08-03 01:17:53 +03:00
idanlib
198807bfbc code review 2b - reorg, version 1.1 2025-08-03 01:12:48 +03:00
idanlib
4a297bc369 code review 2 - reorg file 2025-07-27 01:07:20 +03:00
idanlib
b1bc22c320 code review 2 - refactor redirect() 2025-07-27 00:51:16 +03:00
Idan Liberman
3eeee6f8fd
Merge pull request #23 from IdanLib/code-review-1
code review 1
2025-07-27 00:01:10 +03:00
idanlib
6b00b25ad8 code review 2 2025-07-26 23:45:24 +03:00
idanlib
e5fe53026c code review 1 2025-07-26 13:57:41 +03:00
IdanL
8d11eb166c small readme edits 2024-12-12 22:32:59 +02:00
Idan Liberman
b8c84add70
Merge pull request #22 from IdanLib/rewrite-move-to-forwards-folder-with-file-manager-api
wrap delete note in setTimeout
2024-12-11 23:05:46 +02:00
IdanL
6f34effaf5 wrap delete note in setTimeout 2024-12-11 23:04:25 +02:00
IdanL
3453fe0fa9 change command and setting to sentence case 2024-12-11 22:47:50 +02:00
Idan Liberman
98e43c3002
Merge pull request #21 from IdanLib/rewrite-move-to-forwards-folder-with-file-manager-api
improvements
2024-12-04 23:22:27 +02:00
IdanL
dd4989d8cd improvements - remove comments 2024-12-04 23:21:50 +02:00
IdanL
cd09e45843 improvements - remove log 2024-12-04 23:21:08 +02:00
IdanL
03f6af966f improvements 2024-12-04 23:20:25 +02:00
Idan Liberman
a07976b0f7
Merge pull request #20 from IdanLib/refine-code
fixing two required review comments
2024-12-04 22:39:07 +02:00
IdanL
51e7c7ba35 fixing two required review comments 2024-12-04 22:38:35 +02:00
Idan Liberman
677d5c5264
Create LICENSE 2024-11-09 00:30:54 +02:00
IdanL
8c1632f513 create tagged release 2024-11-08 23:35:21 +02:00
8 changed files with 280 additions and 146 deletions

35
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,35 @@
name: Release Obsidian plugin
on:
push:
tags:
- "*"
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: "18.x"
- name: Build plugin
run: |
npm install
npm run build
- name: Create release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
tag="${GITHUB_REF#refs/tags/}"
gh release create "$tag" \
--title="$tag" \
--draft \
main.js manifest.json styles.css

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Idan Liberman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

119
README.md
View file

@ -1,79 +1,128 @@
# FastForwardLink
Fast-forward from multiple links to a single target note.
Obsidian lets you link notes. Great.
<img src="fastforwardlink.jpeg" alt="Girl in a jacket" width="400">
But real people don't always use the same terms consistently:
- abbreviations
- acronyms
- nicknames
- alternate spellings
- inside jokes
## Introduction
So you might write:
- `javascript`
- `js`
- `ecmascript`
FastForwardLink helps you write faster, keep your notes interconnected, and reduce navigation friction in your vault. This plugin allows you to set multiple links to open a single target note, effectively creating synonymous links. Use custom link abbreviations to type faster, establish conceptual connections between terms, navigate between notes quicker, and keep your vault organized.
...while meaning the exact same thing.
For example, when discussing photo editing software, `ps` is often shorthand for `photoshop`. Without FastForwardLink, the link `[[ps]]` would navigate to a note titled `ps`. But with FastForwardLink, the `[[ps]]` link forwards you directly to your target note `photoshop` (or any other specified note).
By default, Obsidian treats these as separate notes.
Here are some examples of how you might set up FastForwardLink:
Over time, that creates:
- duplicate notes
- fragmented knowledge base
- inconsistent linking
- friction while writing naturally
- `ps` > `photoshop`
- `js` > `javascript`
- `tay tay` > `taylor swift`
- `46` > `joe biden`
- `e=mc2` > `Einstein's special theory of relativity`
- `favorite film` > `bill and ted's bogus journey`
FastForwardLink solves this by turning alias notes into lightweight redirects.
> Press `Ctrl + Alt + R` to quickly create a forwarding link.
Write naturally. Land on the right note — fast.
### FastForwardLink Plugin Demo
<img src="fastforwardlink.jpeg" alt="FastForwardLink mascot" width="400">
## FastForwardLink Plugin Demo
![demo](./plugin_demo.gif)
FastForwardLink lets multiple note names point to the same canonical note. Write naturally without fragmenting your vault structure:
- `js``javascript`
- `ps``photoshop`
## How It Works
Suppose you have a note called `javascript`.
Elsewhere in your vault, you naturally write:
```text
[[js]]
```
Normally, Obsidian would create a brand-new js note. But with FastForwardLink:
- Create a note called `js`
- Add redirect syntax inside it
- Opening `js` automatically forwards to `javascript`
Here are some examples of how you might set up FastForwardLink:
- `ps` > `photoshop`
- `js` > `javascript`
- `tay tay` > `taylor swift`
- `e=mc2` > `Einstein's special theory of relativity`
- `favorite film` > `bill and ted's bogus journey`
## Features
- **Multiple Links, One Target**: Set multiple links to redirect to a single target note for quick navigation between related topics or abbreviations. Organize synonyms or alternate spellings for easier access.
- **Multiple Links, One Target**: Set multiple links to redirect to a single target note for quick navigation between related topics or abbreviations. Organize synonyms or alternate spellings for easier access.
- **Quick Keyboard Shortcut**: Press `Ctrl + Alt + R` to quickly create a forwarding link. Set target notes on the fly without breaking your writing flow.
- **Quick-Paste Command**: Set target notes on the fly without breaking your writing flow. In the Obsidian command palette, select **Paste redirect syntax onto selection** to quickly create a forwarding link.
- **Organized Vault**: Streamline vault navigation by unifying concepts, perfect for efficient, clutter-free notes.
- **Organized Vault**: Streamline vault navigation by unifying concepts, perfect for efficient, clutter-free notes.
- **Easy Management**: Forwarding notes are automatically moved to a designated folder for easy review, management, or removal.
- **Easy Management**: Forwarding notes are automatically moved to a designated folder for easy review, management, or removal.
- **Flexible Forwarding Options**:
- **Flexible Forwarding Options**:
- Open the target note in the same tab.
- Open the target note in a new tab while remaining on the original note.
- Open the target note in a new tab and switch to it automatically.
- Open the target note in the same tab.
- Open the target note in a new tab while remaining on the original note.
- Open the target note in a new tab and switch to it automatically.
- **Remove Forwarding Notes in One Click**: Easily delete all redirecting notes with a single click using plugin settings.
- **Remove Forwarding Notes in One Click**: Easily delete all redirecting notes with a single click using plugin settings.
## Installation
## Installation and Use
Download the plugin directly from Obsidian's community plugins. Just search for FastForwardLink.
### Installing FastForwardLink
To manually install FastForwardLink:
You can also install manually by doing the following:
1. [Download the following plugin files](https://github.com/IdanLib/ObsidianFastForwardLinkPlugin):
- `data.json`
- `main.js`
- `manifest.json`
2. Copy these file to your vault's plugins folder at `{VaultFolder}/.obsidian/plugins/FastForwardLink`.
2. Copy these files to your vault's plugins folder at `{VaultFolder}/.obsidian/plugins/FastForwardLink`.
3. In Obsidian, go to **Settings** > **Community Plugins** and enable **FastForwardLink**.
The plugin is now ready for use.
### Setting Up a Fast-Forward Link
## Create a Fast-Forward Link
To set up a fast-forwarding link:
To create a fast-forwarding link:
1. Create or open the note you want to fast-forward to a target note. For example, a note titled `ps`.
2. In the note, type the target note's title wrapped in the forwarding syntax: `::>[[target-note]]`. For example, to forward from `ps` to `photoshop`, include `::>[[photoshop]]` in the `ps` note.
Clicking the `ps` link in any note now opens the `photoshop` note.
### Keyboard Shortcut
## Quick-Paste Syntax
Yup, typing sucks. Fortunately, there's a keyboard shortcut to make it easier to create a forwarding note.
Yup, typing sucks. Fortunately, there's a command to help you quickly paste the redirect syntax into your code:
Press `Ctrl + Alt + R` (PC) or `Cmd + Opt + R` (macOS) to wrap selected text in the fast-forward syntax.
1. Press `Ctrl + P` to open the Obsidian command palette.
2. Search for and select the command **Paste Redirect Syntax onto Selection**.
The command wraps the selected text in the fast-forward syntax.
> [!TIP] > [Add a hotkey](https://help.obsidian.md/User+interface/Hotkeys#Setting+hotkeys) to trigger this command! We recommend `Ctrl + Alt + R` (PC) or `Cmd + Opt + R` (macOS).
## Version Updates
### Version 1.1
Some improvements include:
- You can now temporarily bypass forwarding with a designated command. In Obsidian's command palette, search for "FastForwardLink".
- Source code refactored and better organized for future contributors.
- Better robust event and error handling and messaging.
## Bugs and Contact
@ -83,7 +132,7 @@ Please open an issue in the [plugin's GitHub repository](https://github.com/Idan
### Known Bugs
- When "Open the target note in a new tab" is enabled and the fast-forwarding note has not yet been moved to the `_forwards_` folder, some additional tabs are opened. This is likely due to internal timing issues in the Obsidian-OS interaction.
- When "Open the target note in a new tab" is enabled and the fast-forwarding note has not yet been moved to the `_forwards_` folder, some additional tabs are opened. This is likely due to internal timing issues in the Obsidian-OS interaction.
When the fast-forwarding note is in the `_forwards` folder, plugin behavior is as expected.

View file

@ -41,7 +41,7 @@ export default class RedirectSettingsTab extends PluginSettingTab {
});
new Setting(containerEl)
.setName("Delete the _forwards Folder")
.setName("Delete the _forwards folder")
.setDesc(
"Before uninstalling the plugin, manually delete the `_forwards` folder to remove unnecessary files. This cannot be undone."
)
@ -49,7 +49,7 @@ export default class RedirectSettingsTab extends PluginSettingTab {
button.setButtonText("Delete");
button.setTooltip("Delete the _forwards folder.");
button.setWarning();
button.onClick((evt) => {
button.onClick(async (evt) => {
const forwardsFolder =
this.app.vault.getFolderByPath("_forwards");
if (!forwardsFolder) {
@ -61,7 +61,7 @@ export default class RedirectSettingsTab extends PluginSettingTab {
}
try {
this.app.vault.delete(forwardsFolder, true);
await this.app.fileManager.trashFile(forwardsFolder);
new Notice(
"_forwards folder deleted successfully.",
2000

236
main.ts
View file

@ -1,6 +1,7 @@
import { getLinkpath, Plugin, Notice, TFolder, TFile } from "obsidian";
import RedirectSettingsTab from "components/Settings";
// --- Setup ---
interface RedirectSettings {
openInNewTab: boolean;
switchToNewTab: boolean;
@ -11,10 +12,64 @@ const DEFAULT_SETTINGS: RedirectSettings = {
switchToNewTab: false,
};
// --- Main Plugin Class ---
export default class RedirectPlugin extends Plugin {
// --- Properties ---
settings: RedirectSettings;
redirectsFolder: TFolder | null;
redirectRef = async (file: TFile) => {
redirectsFolder: TFolder | null = null;
isMovingNote = false;
isBypassRedirect = false;
// --- Lifecycle methods and commands---
async onload() {
await this.loadSettings();
this.addSettingTab(new RedirectSettingsTab(this.app, this));
this.app.workspace.onLayoutReady(async () => {
await this.createRedirectsFolder();
await this.redirect();
});
this.app.workspace.on("file-open", this.handleFileOpen);
this.addCommand({
id: "paste-redirect-syntax",
name: "Paste redirect syntax onto selection",
editorCallback: (editor, view) => {
const selection = editor.getSelection();
editor.replaceSelection(`::>[[${selection}]]`);
const { ch, line } = editor.getCursor();
if (!selection) {
editor.setCursor({
ch: ch - 2,
line: line,
});
}
},
});
this.addCommand({
id: "bypass-forwarding",
name: "Bypass forwarding to target note",
callback: () => {
this.isBypassRedirect = true;
new Notice("Bypassing redirects.");
},
});
}
onunload() {
this.app.workspace.off("file-open", this.handleFileOpen);
}
// --- Event Handlers ---
handleFileOpen = async (file: TFile) => {
if (this.isMovingNote) return;
if (this.isBypassRedirect) {
this.isBypassRedirect = false;
return;
}
await this.redirect();
};
@ -24,51 +79,96 @@ export default class RedirectPlugin extends Plugin {
await this.saveSettings();
}
private async createRedirectsFolder() {
const currentRedirectsFolder =
this.app.vault.getAbstractFileByPath("_forwards");
// --- Core Logic ---
private async redirect(): Promise<void> {
const fileAndContent = await this.getCurrentFile();
if (!fileAndContent) return;
try {
this.redirectsFolder = await this.app.vault.createFolder(
"/_forwards"
);
} catch (error) {
console.warn(error);
}
const { currentFile, content } = fileAndContent;
const targetNoteFile = this.getTargetFile(content);
if (!targetNoteFile) return;
this.redirectsFolder = currentRedirectsFolder as TFolder;
await this.moveRedirectNote(currentFile);
await this.openTargetNote(targetNoteFile);
}
private async redirect() {
const currentFile = this.app.workspace.getActiveFile();
// --- Helper functions ---
if (!currentFile) {
private async moveRedirectNote(
redirectingNote: TFile | null
): Promise<TFile | void> {
if (
!redirectingNote ||
redirectingNote.path === `_forwards/${redirectingNote.name}`
) {
return;
}
const currentFileContent = (await this.getCurrentFileContent(
currentFile
)) as string;
new Notice(
`Moving ${redirectingNote.name} to the _forwards folder.`,
2000
);
const targetNoteFile = this.getTargetFile(currentFileContent);
if (!targetNoteFile) {
return;
if (!this.redirectsFolder) {
await this.createRedirectsFolder();
}
try {
this.isMovingNote = true;
const redirectingNoteInFolder = await this.app.vault.copy(
redirectingNote,
`/_forwards/${redirectingNote.name}`
);
if (this.settings.openInNewTab) {
await this.openTargetNote(redirectingNoteInFolder);
}
// Delay deletion to avoid race condition where the original note is deleted before the new note is fully opened in the UI.
setTimeout(async () => {
await this.deleteNote(redirectingNote);
this.isMovingNote = false;
}, 500);
} catch (error) {
this.isMovingNote = false;
console.error(error);
}
}
private async openTargetNote(targetNoteFile: TFile) {
await this.app.workspace.openLinkText(
targetNoteFile.name,
targetNoteFile.path,
this.settings.openInNewTab,
{ active: this.settings.switchToNewTab }
);
}
if (currentFile.path === `_forwards/${currentFile.name}`) {
new Notice(`${currentFile.name} is in the _forwards folder.`, 2000);
return;
private async createRedirectsFolder() {
try {
this.redirectsFolder = await this.app.vault.createFolder(
"/_forwards"
);
} catch (error) {
console.warn(
"`_forwards` folder not created or already exists.",
error
);
}
}
await this.moveRedirectNote(currentFile);
private async getCurrentFile(): Promise<{
currentFile: TFile;
content: string;
} | null> {
const currentFile = this.app.workspace.getActiveFile();
if (!currentFile) {
return null;
}
return {
currentFile: currentFile,
content: (await this.getCurrentFileContent(currentFile)) || "",
};
}
private async getCurrentFileContent(
@ -78,15 +178,13 @@ export default class RedirectPlugin extends Plugin {
return await this.app.vault.read(currentFile);
} catch (error) {
new Notice(`Cannot read ${currentFile}.`);
console.error(
console.error(`Failed to read ${currentFile} content: `, error)
);
console.error(`Failed to read ${currentFile} content: `, error);
return null;
}
}
private getTargetFile(currentFileContent: string): TFile | null {
const linkTextRegex = /::>\[\[(.*[\w\s]*)\]\]/i;
const linkTextRegex = /::>\[\[(.*?)\]\]/i;
const targetNoteName = currentFileContent.match(linkTextRegex)?.at(1);
if (!targetNoteName) {
@ -111,48 +209,9 @@ export default class RedirectPlugin extends Plugin {
return targetNoteFile;
}
private async moveRedirectNote(
redirectingNote: TFile | null
): Promise<TFile | void> {
let redirectingNoteInFolder = null;
if (!redirectingNote) {
return;
}
if (!this.redirectsFolder) {
await this.createRedirectsFolder();
}
try {
redirectingNoteInFolder = await this.app.vault.copy(
redirectingNote,
`/_forwards/${redirectingNote.name}`
);
if (this.settings.openInNewTab) {
// Turn off event handler to avoid opening the target note twice
this.app.workspace.off("file-open", this.redirectRef);
await this.app.workspace
.getLeaf(this.settings.openInNewTab)
.openFile(redirectingNoteInFolder, {
active: this.settings.switchToNewTab,
});
this.app.workspace.getLeaf().detach();
this.app.workspace.on("file-open", this.redirectRef);
}
await this.deleteNote(redirectingNote);
} catch (error) {
console.error(error);
}
}
private async deleteNote(orgRedirectingNote: TFile): Promise<void> {
try {
await this.app.vault.delete(orgRedirectingNote);
await this.app.fileManager.trashFile(orgRedirectingNote);
} catch (error) {
new Notice(
"Failed to delete directing note from original location. Please delete manually.",
@ -162,40 +221,7 @@ export default class RedirectPlugin extends Plugin {
}
}
async onload() {
await this.loadSettings();
this.addSettingTab(new RedirectSettingsTab(this.app, this));
this.app.workspace.onLayoutReady(async () => {
await this.createRedirectsFolder();
await this.redirect();
});
this.app.workspace.on("file-open", this.redirectRef);
this.addCommand({
id: "paste-redirect-syntax",
name: "Paste Redirect Syntax onto Selection",
hotkeys: [{ key: "r", modifiers: ["Ctrl", "Alt"] }],
editorCallback: (editor, view) => {
const selection = editor.getSelection();
editor.replaceSelection(`::>[[${selection}]]`);
const { ch, line } = editor.getCursor();
if (!selection) {
editor.setCursor({
ch: ch - 2,
line: line,
});
}
},
});
}
onunload() {
this.app.workspace.off("file-open", this.redirectRef);
}
// --- Settings Persistence ---
async loadSettings() {
try {
this.settings = Object.assign(

View file

@ -1,7 +1,7 @@
{
"id": "fast-forward-link",
"name": "FastForwardLink",
"version": "1.0.0",
"version": "1.1.2",
"minAppVersion": "0.15.0",
"description": "Fast-forward multiple links to a single target note. Create custom link shorthands (like `ps` > `photoshop`) to create synonyms, streamline navigation, and keep your vault organized.",
"author": "Idan Liberman",

View file

@ -1,6 +1,6 @@
{
"name": "fast-forward-link",
"version": "1.0.0",
"version": "1.1.2",
"description": "Forward multiple links to a single note with ease.",
"main": "main.js",
"scripts": {

View file

@ -1,3 +1,6 @@
{
"1.0.0": "0.15.0"
"1.0.0": "0.15.0",
"1.1.0": "0.15.0",
"1.1.1": "0.15.0",
"1.1.2": "0.15.0"
}