118 lines
4.0 KiB
TypeScript
118 lines
4.0 KiB
TypeScript
/**
|
|
* Monaco Editor wiki-link autocomplete provider.
|
|
*
|
|
* Registers a completion provider for markdown that triggers on `[`
|
|
* and provides file suggestions when the user types `[[`.
|
|
*/
|
|
|
|
import type { FileNode } from '@/types/api';
|
|
|
|
interface FlatFile {
|
|
name: string;
|
|
path: string;
|
|
}
|
|
|
|
const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico']);
|
|
|
|
function isImage(name: string): boolean {
|
|
const dot = name.lastIndexOf('.');
|
|
return dot >= 0 && IMAGE_EXTENSIONS.has(name.substring(dot).toLowerCase());
|
|
}
|
|
|
|
function flattenFiles(nodes: FileNode[]): FlatFile[] {
|
|
const out: FlatFile[] = [];
|
|
for (const n of nodes) {
|
|
if (n.isDirectory) {
|
|
if (n.children) out.push(...flattenFiles(n.children));
|
|
} else {
|
|
out.push({ path: n.path, name: n.name });
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Register a wiki-link `[[` autocomplete provider for Monaco's markdown mode.
|
|
*
|
|
* @param monaco - The Monaco namespace (from onMount callback)
|
|
* @param getFileTree - Callback returning the current file tree
|
|
* @returns IDisposable to unregister the provider
|
|
*/
|
|
export function registerWikiLinkCompletion(
|
|
monaco: typeof import('monaco-editor'),
|
|
getFileTree: () => FileNode[],
|
|
): import('monaco-editor').IDisposable {
|
|
return monaco.languages.registerCompletionItemProvider('markdown', {
|
|
triggerCharacters: ['['],
|
|
provideCompletionItems(model, position) {
|
|
// Check that we're in a `[[` context
|
|
const lineContent = model.getLineContent(position.lineNumber);
|
|
const textBefore = lineContent.substring(0, position.column - 1);
|
|
|
|
// Must end with `[[` (possibly with partial text after it)
|
|
const wikiStart = textBefore.lastIndexOf('[[');
|
|
if (wikiStart < 0) return { suggestions: [] };
|
|
|
|
// Make sure there's no `]]` between wikiStart and cursor (not already closed)
|
|
const between = textBefore.substring(wikiStart + 2);
|
|
if (between.includes(']]')) return { suggestions: [] };
|
|
|
|
// Check if this is an image embed (![[)
|
|
const isEmbed = wikiStart > 0 && textBefore[wikiStart - 1] === '!';
|
|
|
|
// The partial query the user has typed after `[[`
|
|
const query = between.toLowerCase();
|
|
|
|
// Build the range from after `[[` to current cursor (or past any auto-closed `]]`)
|
|
const startCol = wikiStart + 3; // +2 for `[[`, +1 for 1-based
|
|
const textAfter = lineContent.substring(position.column - 1);
|
|
// If Monaco auto-closed brackets, there may be `]]` right after cursor — consume it
|
|
const closingMatch = textAfter.match(/^\]{1,2}/);
|
|
const extraClose = closingMatch ? closingMatch[0].length : 0;
|
|
const range = new monaco.Range(
|
|
position.lineNumber,
|
|
startCol,
|
|
position.lineNumber,
|
|
position.column + extraClose,
|
|
);
|
|
|
|
const files = flattenFiles(getFileTree());
|
|
const suggestions: import('monaco-editor').languages.CompletionItem[] = [];
|
|
|
|
for (const file of files) {
|
|
const fileIsImage = isImage(file.name);
|
|
|
|
// If user typed `![[`, prefer images; if `[[`, prefer docs
|
|
// But show all files regardless — just sort differently
|
|
const displayName = file.name.endsWith('.md')
|
|
? file.name.slice(0, -3)
|
|
: file.name;
|
|
|
|
// Filter by query
|
|
if (query && !displayName.toLowerCase().includes(query) && !file.path.toLowerCase().includes(query)) {
|
|
continue;
|
|
}
|
|
|
|
// For wiki-links, we insert just the name (hook resolves the path)
|
|
const insertName = file.name.endsWith('.md')
|
|
? file.name.slice(0, -3)
|
|
: file.name;
|
|
|
|
suggestions.push({
|
|
label: displayName,
|
|
kind: fileIsImage
|
|
? monaco.languages.CompletionItemKind.File
|
|
: monaco.languages.CompletionItemKind.Reference,
|
|
detail: file.path,
|
|
insertText: insertName + ']]',
|
|
range,
|
|
// Sort: prioritize images for ![[, docs for [[
|
|
sortText: (isEmbed ? (fileIsImage ? '0' : '1') : (fileIsImage ? '1' : '0')) + displayName,
|
|
});
|
|
}
|
|
|
|
return { suggestions };
|
|
},
|
|
});
|
|
}
|