Skip to content

Commit c6b9562

Browse files
feat(notes): add wiki-style internal links for notes and snippets (#721)
* feat(notes): add internal links parser and cache * feat(core): add internal link entity lookup and deeplinks * feat(notes): add internal link editor UI * fix(notes): defer internal link popup positioning * fix(notes): align internal link overlays and chips * fix(api): type internal link entity responses * feat(notes): add obsidian-style internal links * fix(notes): handle modifier click navigation * fix(notes): stabilize internal link interactions * fix(notes): require modifier key for internal link preview Preview popover now only appears on Cmd+hover (macOS) or Ctrl+hover (Win/Linux) instead of plain hover. Supports both hover-then-press and press-then-hover via document-level key listeners. * fix(notes): prevent hideMarkup from hiding internal link brackets The hideMarkup extension was hiding inner brackets of [[Target]] when the cursor entered from the left, causing a two-step reveal instead of showing all brackets at once. * fix(notes): eliminate flicker when navigating to snippets via internal links Deep link navigation to code space caused flickering because Library.vue's initApp() raced with openSnippetDeepLink(), briefly showing the previous selection before the target. Now openSnippetDeepLink() sets pendingCodeNavigation and isAppLoading before the route change, Library.vue skips its init when pending, and deep link handles all data loading with a fallback to default initCodeSpace() on error. * fix(notes): prevent preview popover from appearing at top-left corner After navigating to a snippet and returning to notes, the preview could render at (0,0) because hoveredLink retained a reference to a detached DOM element. Now destroy() always clears hoveredLink, and showPreview() checks target.isConnected before reading position. * feat: add internal link navigation history * fix: constrain header input with history controls * fix: reduce internal link preview text size * fix: use app scrollbar in link preview * docs: add notes internal links guide * fix: avoid notes flicker on history back navigation * fix: reduce internal link preview typography * feat: refine internal link chip colors * docs: update image
1 parent 3d646fa commit c6b9562

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+4691
-112
lines changed

docs/website/.vitepress/config.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ export default defineConfig({
122122
{ text: 'Library', link: '/documentation/notes/library' },
123123
{ text: 'Folders', link: '/documentation/notes/folders' },
124124
{ text: 'Tags', link: '/documentation/notes/tags' },
125+
{ text: 'Internal Links', link: '/documentation/notes/internal-links' },
125126
{ text: 'Mermaid', link: '/documentation/notes/mermaid' },
126127
{ text: 'Mindmap', link: '/documentation/notes/mindmap' },
127128
{ text: 'Presentation', link: '/documentation/notes/presentation' },

docs/website/documentation/notes/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,13 @@ The editor is built on CodeMirror 6 and includes:
4343
- Smart list indentation with automatic ordered list renumbering
4444
- Tab / Shift-Tab indentation
4545
- Table navigation between cells
46+
- Internal links to notes and snippets
4647
- Mermaid diagram support
4748
- Image embedding
4849
- Callout blocks
4950

5051
For visual diagrams in notes, see [Mermaid](/documentation/notes/mermaid).
52+
For wiki-style links between notes and snippets, see [Internal Links](/documentation/notes/internal-links).
5153

5254
## Editor Preferences
5355

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
---
2+
title: Internal Links
3+
description: "Link notes and snippets together inside massCode Notes with wiki-style links, hover previews, and back-forward navigation."
4+
---
5+
6+
# Internal Links
7+
8+
<AppVersion text=">=5.1.0" />
9+
10+
Internal Links let you connect notes and snippets with wiki-style links directly inside Notes. Use them to build lightweight documentation, link reference snippets from prose, and move through related material without leaving massCode.
11+
12+
<img :src="withBase('/notes-internal-links.png')">
13+
14+
## Link Syntax
15+
16+
Use double brackets around the target name:
17+
18+
```md
19+
[[API authentication]]
20+
[[Fetch helper]]
21+
```
22+
23+
You can also provide custom visible text with an alias:
24+
25+
```md
26+
[[API authentication|auth flow]]
27+
[[Fetch helper|request snippet]]
28+
```
29+
30+
massCode resolves internal links by item name. A target can be either a note or a snippet.
31+
32+
## Creating Links
33+
34+
Start typing `[[` in the Notes editor to open the internal links picker.
35+
36+
- The picker searches both notes and snippets.
37+
- Results show the item name and its current location.
38+
- Press <kbd>Enter</kbd> to insert the active result.
39+
- Use the arrow keys to move through the list.
40+
41+
The inserted link keeps the readable wiki-link format in your markdown.
42+
43+
## Opening Links
44+
45+
Hold <kbd>Cmd</kbd> on macOS or <kbd>Ctrl</kbd> on Windows or Linux, then click the link.
46+
47+
- If the target is a note, massCode opens it in Notes.
48+
- If the target is a snippet, massCode switches to Code and opens the snippet there.
49+
50+
Broken links stay visible, but they appear dimmed and struck through so you can spot missing targets.
51+
52+
## Preview
53+
54+
Hover an internal link while holding <kbd>Cmd</kbd> on macOS or <kbd>Ctrl</kbd> on Windows or Linux to open a preview popup.
55+
56+
- Note links show a text excerpt.
57+
- Snippet links show the first snippet fragment.
58+
59+
This helps you confirm the target before you navigate away from the current note.
60+
61+
## Navigation History
62+
63+
When you follow internal links, massCode keeps a small link navigation history for that session.
64+
65+
- Use the back and forward buttons in the editor header to move through link-based navigation.
66+
- Use <kbd>Cmd+[</kbd> / <kbd>Cmd+]</kbd> on macOS or <kbd>Ctrl+[</kbd> / <kbd>Ctrl+]</kbd> on Windows or Linux.
67+
- The same actions are also available from the **History** menu.
68+
69+
The history is specific to internal-link navigation. If you manually select another note or snippet from the list, that temporary link history is cleared.
70+
71+
## When to Use Internal Links
72+
73+
- Link architecture notes to implementation snippets
74+
- Connect meeting notes to reference code
75+
- Build personal knowledge-base pages that jump between notes and snippets
76+
- Keep long-form docs in Notes while linking reusable code examples from Code
77+
78+
<script setup>
79+
import { withBase } from 'vitepress'
80+
</script>
77.2 KB
Loading

src/main/api/dto/common/response.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,7 @@ import { t } from 'elysia'
33
export const commonAddResponse = t.Object({
44
id: t.Union([t.Number(), t.BigInt()]),
55
})
6+
7+
export const commonMessageResponse = t.Object({
8+
message: t.String(),
9+
})

src/main/api/dto/notes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export const notesDTO = new Elysia().model({
5252
notesAdd,
5353
notesContentUpdate,
5454
notesCountsResponse,
55+
noteItemResponse: noteItem,
5556
notesResponse,
5657
notesQuery: t.Object({
5758
...commonQuery.properties,
@@ -67,3 +68,4 @@ export const notesDTO = new Elysia().model({
6768
export type NotesAdd = typeof notesAdd.static
6869
export type NotesResponse = typeof notesResponse.static
6970
export type NotesCountsResponse = typeof notesCountsResponse.static
71+
export type NoteItemResponse = typeof noteItem.static

src/main/api/dto/snippets.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ const snippetsCountsResponse = t.Object({
6666
export const snippetsDTO = new Elysia().model({
6767
snippetContentsAdd,
6868
snippetContentsUpdate,
69+
snippetItemResponse: snippetItem,
6970
snippetsAdd,
7071
snippetsUpdate,
7172
snippetsCountsResponse,
@@ -83,3 +84,4 @@ export const snippetsDTO = new Elysia().model({
8384
export type SnippetsAdd = typeof snippetsAdd.static
8485
export type SnippetsResponse = typeof snippetsResponse.static
8586
export type SnippetsCountsResponse = typeof snippetsCountsResponse.static
87+
export type SnippetItemResponse = typeof snippetItem.static

src/main/api/routes/notes.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1-
import type { NotesResponse } from '../dto/notes'
1+
import type { NoteItemResponse, NotesResponse } from '../dto/notes'
22
import Elysia from 'elysia'
33
import { useNotesStorage } from '../../storage'
4-
import { commonAddResponse } from '../dto/common/response'
4+
import {
5+
commonAddResponse,
6+
commonMessageResponse,
7+
} from '../dto/common/response'
58
import { notesDTO } from '../dto/notes'
69

710
const app = new Elysia({ prefix: '/notes' })
@@ -89,6 +92,28 @@ app
8992
},
9093
},
9194
)
95+
.get(
96+
'/:id',
97+
({ params, status }) => {
98+
const storage = useNotesStorage()
99+
const note = storage.notes.getNoteById(Number(params.id))
100+
101+
if (!note) {
102+
return status(404, { message: 'Note not found' })
103+
}
104+
105+
return note as NoteItemResponse
106+
},
107+
{
108+
response: {
109+
200: 'noteItemResponse',
110+
404: commonMessageResponse,
111+
},
112+
detail: {
113+
tags: ['Notes'],
114+
},
115+
},
116+
)
92117
.post(
93118
'/',
94119
({ body, status }) => {

src/main/api/routes/snippets.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1-
import type { SnippetsCountsResponse, SnippetsResponse } from '../dto/snippets'
1+
import type {
2+
SnippetItemResponse,
3+
SnippetsCountsResponse,
4+
SnippetsResponse,
5+
} from '../dto/snippets'
26
import Elysia from 'elysia'
37
import { useStorage } from '../../storage'
4-
import { commonAddResponse } from '../dto/common/response'
8+
import {
9+
commonAddResponse,
10+
commonMessageResponse,
11+
} from '../dto/common/response'
512
import { snippetsDTO } from '../dto/snippets'
613

714
const app = new Elysia({ prefix: '/snippets' })
@@ -91,6 +98,28 @@ app
9198
},
9299
},
93100
)
101+
.get(
102+
'/:id',
103+
({ params, status }) => {
104+
const storage = useStorage()
105+
const snippet = storage.snippets.getSnippetById(Number(params.id))
106+
107+
if (!snippet) {
108+
return status(404, { message: 'Snippet not found' })
109+
}
110+
111+
return snippet as SnippetItemResponse
112+
},
113+
{
114+
response: {
115+
200: 'snippetItemResponse',
116+
404: commonMessageResponse,
117+
},
118+
detail: {
119+
tags: ['Snippets'],
120+
},
121+
},
122+
)
94123
// Создание сниппета
95124
.post(
96125
'/',

src/main/i18n/locales/en_US/ui.json

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,12 +135,27 @@
135135
"toHtml": "Export to HTML"
136136
},
137137
"copy": {
138-
"snippetLink": "Copy Snippet Link"
138+
"snippetLink": "Copy Snippet Link",
139+
"noteLink": "Copy Note Link"
139140
},
140141
"select": {
141142
"directory": "Select Directory"
142143
}
143144
},
145+
"internalLinks": {
146+
"picker": {
147+
"searchPlaceholder": "Search snippets and notes...",
148+
"emptyResults": "No results",
149+
"groups": {
150+
"snippets": "Snippets",
151+
"notes": "Notes"
152+
}
153+
},
154+
"missing": {
155+
"snippet": "Snippet not found",
156+
"note": "Note not found"
157+
}
158+
},
144159
"placeholder": {
145160
"emptyTagList": "Add tags to snippets to see them here",
146161
"emptyNotesTagList": "Add tags to notes to see them here",

0 commit comments

Comments
 (0)