Add link autocompletion (limited)
This commit is contained in:
parent
6db6a4b19d
commit
62c1ccf0b4
52
app.vue
52
app.vue
|
|
@ -191,6 +191,58 @@ iconify-icon
|
|||
@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 {
|
||||
@apply bg-transparent;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ export class Content
|
|||
private static root: FileSystemDirectoryHandle;
|
||||
|
||||
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();
|
||||
|
||||
static init(): Promise<boolean>
|
||||
|
|
@ -146,10 +146,6 @@ export class Content
|
|||
Content._overview = {};
|
||||
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;
|
||||
}
|
||||
|
|
@ -272,7 +268,12 @@ export class Content
|
|||
}
|
||||
|
||||
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.write('overview', JSON.stringify(Content._overview), { create: true });
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ import { Annotation, EditorState, SelectionRange, StateField, type Range } from
|
|||
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
|
||||
import { bracketMatching, HighlightStyle, indentOnInput, syntaxHighlighting, syntaxTree } from '@codemirror/language';
|
||||
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 { IterMode, Tree, type SyntaxNodeRef } from '@lezer/common';
|
||||
import { tags } from '@lezer/highlight';
|
||||
import { dom } from '#shared/dom.util';
|
||||
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 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';
|
||||
|
|
@ -236,14 +236,21 @@ export class MarkdownEditor
|
|||
syntaxHighlighting(highlight),
|
||||
bracketMatching(),
|
||||
closeBrackets(),
|
||||
autocompletion({
|
||||
icons: false,
|
||||
defaultKeymap: true,
|
||||
maxRenderedOptions: 10,
|
||||
activateOnTyping: true,
|
||||
override: [ wikilinkAutocompletion ]
|
||||
}),
|
||||
crosshairCursor(),
|
||||
EditorView.lineWrapping,
|
||||
keymap.of([
|
||||
...completionKeymap,
|
||||
...closeBracketsKeymap,
|
||||
...defaultKeymap,
|
||||
...searchKeymap,
|
||||
...historyKeymap,
|
||||
...completionKeymap
|
||||
]),
|
||||
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
|
||||
if (viewUpdate.docChanged && !viewUpdate.transactions.some(tr => tr.annotation(External)))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,25 @@
|
|||
import type { CompletionContext, CompletionResult } from '@codemirror/autocomplete';
|
||||
import type { Element, MarkdownConfig } from '@lezer/markdown';
|
||||
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 = {
|
||||
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-]*$/,
|
||||
}
|
||||
};
|
||||
Loading…
Reference in New Issue