Compare commits

...

2 Commits

Author SHA1 Message Date
Clément Pons 93eaa1e3e4 Merge branch 'dev' of https://git.peaceultime.com/Peaceultime/obsidian-visualiser into dev 2025-11-03 11:21:08 +01:00
Clément Pons 62c1ccf0b4 Add link autocompletion (limited) 2025-11-03 11:20:56 +01:00
5 changed files with 118 additions and 9 deletions

52
app.vue
View File

@ -191,6 +191,58 @@ iconify-icon
@apply font-sans; @apply font-sans;
} }
.cm-tooltip-autocomplete {
@apply max-w-[400px];
@apply !bg-light-20;
@apply dark:!bg-dark-20;
@apply !border-light-40;
@apply dark:!border-dark-40;
}
/* .cm-tooltip-autocomplete > ul {
@apply p-1;
} */
.cm-tooltip-autocomplete > ul > li {
@apply flex;
@apply flex-col;
@apply !py-1;
@apply hover:bg-light-30;
@apply dark:hover:bg-dark-30;
}
.cm-tooltip-autocomplete > ul > li[aria-selected] {
@apply !bg-light-35;
@apply dark:!bg-dark-35;
}
.cm-completionIcon {
@apply !hidden;
}
.cm-completionLabel {
@apply px-4;
@apply font-sans;
@apply font-normal;
@apply text-base;
@apply text-light-100;
@apply dark:text-dark-100;
}
.cm-completionMatchedText {
@apply font-bold;
@apply !no-underline;
}
.cm-completionDetail {
@apply font-sans;
@apply font-normal;
@apply text-sm;
@apply text-light-60;
@apply dark:text-dark-60;
@apply italic;
}
::-webkit-scrollbar-corner { ::-webkit-scrollbar-corner {
@apply bg-transparent; @apply bg-transparent;
} }

BIN
db.sqlite

Binary file not shown.

View File

@ -116,7 +116,7 @@ export class Content
private static root: FileSystemDirectoryHandle; private static root: FileSystemDirectoryHandle;
private static _overview: Record<string, Omit<LocalContent, 'content'>>; private static _overview: Record<string, Omit<LocalContent, 'content'>>;
private static _reverseMapping: Record<string, string>; private static _reverseMapping: Record<string, string> = {};
private static queue = new AsyncQueue(); private static queue = new AsyncQueue();
static init(): Promise<boolean> static init(): Promise<boolean>
@ -146,10 +146,6 @@ export class Content
Content._overview = {}; Content._overview = {};
await Content.pull(true); await Content.pull(true);
} }
Content._reverseMapping = Object.values(Content._overview).reduce((p, v) => {
p[v.path] = v.id;
return p;
}, {} as Record<string, string>);
Content._ready = true; Content._ready = true;
} }
@ -272,7 +268,12 @@ export class Content
} }
for(const id of deletable) for(const id of deletable)
Content.queue.queue(() => Content.remove(id).then(e => delete Content._overview[id])); {
Content.queue.queue(() => Content.remove(id).then(e => {
delete Content._reverseMapping[Content._overview[id]!.path];
delete Content._overview[id];
}));
}
return Content.queue.queue(() => { return Content.queue.queue(() => {
return Content.write('overview', JSON.stringify(Content._overview), { create: true }); return Content.write('overview', JSON.stringify(Content._overview), { create: true });

View File

@ -3,13 +3,13 @@ import { Annotation, EditorState, SelectionRange, StateField, type Range } from
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands'; import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
import { bracketMatching, HighlightStyle, indentOnInput, syntaxHighlighting, syntaxTree } from '@codemirror/language'; import { bracketMatching, HighlightStyle, indentOnInput, syntaxHighlighting, syntaxTree } from '@codemirror/language';
import { search, searchKeymap } from '@codemirror/search'; import { search, searchKeymap } from '@codemirror/search';
import { closeBrackets, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete'; import { autocompletion, closeBrackets, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete';
import { markdown, markdownLanguage } from '@codemirror/lang-markdown'; import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
import { IterMode, Tree, type SyntaxNodeRef } from '@lezer/common'; import { IterMode, Tree, type SyntaxNodeRef } from '@lezer/common';
import { tags } from '@lezer/highlight'; import { tags } from '@lezer/highlight';
import { dom } from '#shared/dom.util'; import { dom } from '#shared/dom.util';
import { callout as calloutExtension } from '#shared/grammar/callout.extension'; import { callout as calloutExtension } from '#shared/grammar/callout.extension';
import { wikilink as wikilinkExtension } from '#shared/grammar/wikilink.extension'; import { wikilink as wikilinkExtension, autocompletion as wikilinkAutocompletion } from '#shared/grammar/wikilink.extension';
import { renderMarkdown } from '#shared/markdown.util'; import { renderMarkdown } from '#shared/markdown.util';
import prose, { a, blockquote, tag, h1, h2, h3, h4, h5, hr, li, small, table, td, th, callout } from "#shared/proses"; import prose, { a, blockquote, tag, h1, h2, h3, h4, h5, hr, li, small, table, td, th, callout } from "#shared/proses";
import { tagTag, tag as tagExtension } from './grammar/tag.extension'; import { tagTag, tag as tagExtension } from './grammar/tag.extension';
@ -236,14 +236,21 @@ export class MarkdownEditor
syntaxHighlighting(highlight), syntaxHighlighting(highlight),
bracketMatching(), bracketMatching(),
closeBrackets(), closeBrackets(),
autocompletion({
icons: false,
defaultKeymap: true,
maxRenderedOptions: 10,
activateOnTyping: true,
override: [ wikilinkAutocompletion ]
}),
crosshairCursor(), crosshairCursor(),
EditorView.lineWrapping, EditorView.lineWrapping,
keymap.of([ keymap.of([
...completionKeymap,
...closeBracketsKeymap, ...closeBracketsKeymap,
...defaultKeymap, ...defaultKeymap,
...searchKeymap, ...searchKeymap,
...historyKeymap, ...historyKeymap,
...completionKeymap
]), ]),
EditorView.updateListener.of((viewUpdate: ViewUpdate) => { EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
if (viewUpdate.docChanged && !viewUpdate.transactions.some(tr => tr.annotation(External))) if (viewUpdate.docChanged && !viewUpdate.transactions.some(tr => tr.annotation(External)))

View File

@ -1,5 +1,25 @@
import type { CompletionContext, CompletionResult } from '@codemirror/autocomplete';
import type { Element, MarkdownConfig } from '@lezer/markdown'; import type { Element, MarkdownConfig } from '@lezer/markdown';
import { styleTags, tags } from '@lezer/highlight'; import { styleTags, tags } from '@lezer/highlight';
import { Content } from '../content.util';
function fuzzyMatch(text: string, search: string): number {
const textLower = text.toLowerCase().normalize('NFC');
const searchLower = search.toLowerCase().normalize('NFC');
let searchIndex = 0;
let score = 0;
for (let i = 0; i < textLower.length && searchIndex < searchLower.length; i++) {
if (textLower[i] === searchLower[searchIndex]) {
score += 1;
if (i === searchIndex) score += 2; // Bonus for sequential match
searchIndex++;
}
}
return searchIndex === searchLower.length ? score : 0;
}
export const wikilink: MarkdownConfig = { export const wikilink: MarkdownConfig = {
defineNodes: [ defineNodes: [
@ -69,3 +89,32 @@ export const wikilink: MarkdownConfig = {
}) })
] ]
}; };
export const autocompletion = (context: CompletionContext): CompletionResult | null => {
const word = context.matchBefore(/\[\[[\w\s-]*/);
if (!word || (word.from === word.to && !context.explicit))
return null;
const searchTerm = word.text.slice(2).toLowerCase();
const options = Object.values(Content.files).filter(e => e.type !== 'folder').map(e => ({ ...e, score: fuzzyMatch(e.title, searchTerm) })).filter(e => e.score > 0).sort((a, b) => b.score - a.score).slice(0, 50);
return {
from: word.from + 2,
options: options.map(e => ({
label: e.title,
detail: e.path,
apply: (view, completion, from, to) => {
view.dispatch({
changes: {
from: word.from,
to: word.to,
insert: `[[${e.path}]]`
},
selection: { anchor: word.from + e.path.length + 2 }
});
},
type: 'text'
})),
validFor: /^[\[\w\s-]*$/,
}
};