Merge pull request #5 from oeN/fix/use-new-suggest-api

Support the Live Preview editor
This commit is contained in:
Diomede 2022-02-16 10:39:29 +01:00 committed by GitHub
commit 6d35adb0a3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 488 additions and 437 deletions

View file

@ -1,8 +1,8 @@
{
"id": "liquid-templates",
"name": "Liquid Templates",
"version": "0.2.0",
"minAppVersion": "0.12.3",
"version": "0.3.0",
"minAppVersion": "0.13.10",
"description": "Empower your template with LiquidJS tags",
"author": "diomedet",
"authorUrl": "https://diomedet.com/",

View file

@ -1,41 +1,41 @@
import { addDays, format, subDays } from 'date-fns';
import { BaseFilter, BaseFilterProps } from './base_filter';
export default class DateFiler extends BaseFilter {
originalFilter: Function;
constructor(props: BaseFilterProps) {
super({ ...props, filterName: 'date' });
this.originalFilter = this.engine.filters.get('date');
}
handler = (givenValue: number | string, dateFormat?: string): string | number => {
const dateToFormat = (typeof givenValue === 'number')
? givenValue
: this.parseDate(givenValue)
const formatToUse = dateFormat || this.plugin.settings.dateFormat;
// TODO: improve or remove me: keep backwards compatibility with the current 0.1.5 version
if (formatToUse.includes('%')) {
// TODO: send a notification when using this function
return this.originalFilter(givenValue, dateFormat);
}
try {
return format(dateToFormat, formatToUse)
} catch (e) {
if (!(e instanceof RangeError)) throw e;
}
return givenValue;
}
parseDate = (stringToParse: string): Date => {
if(['now', 'today'].includes(stringToParse)) return new Date;
if(stringToParse === 'yesterday') return subDays(Date.now(), 1);
if(stringToParse === 'tomorrow') return addDays(Date.now(), 1);
// try to parse the given string
return new Date(Date.parse(stringToParse));
}
}
import { addDays, format, subDays } from 'date-fns';
import { BaseFilter, BaseFilterProps } from './base_filter';
export default class DateFiler extends BaseFilter {
originalFilter: Function;
constructor(props: BaseFilterProps) {
super({ ...props, filterName: 'date' });
this.originalFilter = this.engine.filters.get('date');
}
handler = (givenValue: number | string, dateFormat?: string): string | number => {
const dateToFormat = (typeof givenValue === 'number')
? givenValue
: this.parseDate(givenValue)
const formatToUse = dateFormat || this.plugin.settings.dateFormat;
// TODO: improve or remove me: keep backwards compatibility with the current 0.1.5 version
if (formatToUse.includes('%')) {
// TODO: send a notification when using this function
return this.originalFilter(givenValue, dateFormat);
}
try {
return format(dateToFormat, formatToUse)
} catch (e) {
if (!(e instanceof RangeError)) throw e;
}
return givenValue;
}
parseDate = (stringToParse: string): Date => {
if(['now', 'today'].includes(stringToParse)) return new Date;
if(stringToParse === 'yesterday') return subDays(Date.now(), 1);
if(stringToParse === 'tomorrow') return addDays(Date.now(), 1);
// try to parse the given string
return new Date(Date.parse(stringToParse));
}
}

View file

@ -1,11 +1,11 @@
import { addDays } from 'date-fns';
import { BaseFilter, BaseFilterProps } from './base_filter';
export default class DaysAfter extends BaseFilter {
constructor(props: BaseFilterProps) {
super({ ...props, filterName: 'days_after' });
}
handler = (daysToSub: number): Date => addDays(Date.now(), daysToSub);
}
import { addDays } from 'date-fns';
import { BaseFilter, BaseFilterProps } from './base_filter';
export default class DaysAfter extends BaseFilter {
constructor(props: BaseFilterProps) {
super({ ...props, filterName: 'days_after' });
}
handler = (daysToAdd: number): Date => addDays(Date.now(), daysToAdd);
}

View file

@ -5,40 +5,21 @@ import { LiquidTemplatesSettings, DEFAULT_SETTINGS, LiquidTemplatesSettingsTab }
import TemplateSuggest from "./suggest/template-suggest";
export default class LiquidTemplates extends Plugin {
private autosuggest: TemplateSuggest;
public settings: LiquidTemplatesSettings;
async onload(): Promise<void> {
console.log('loading liquid templates plugin');
await this.loadSettings();
this.setupAutosuggest();
this.addSettingTab(new LiquidTemplatesSettingsTab(this.app, this));
this.addSettingTab(new LiquidTemplatesSettingsTab(this.app, this))
this.registerEditorSuggest(new TemplateSuggest(this.app, this));
}
onunload(): void {
console.log('unloading liquid templates plugin');
// remove the autosuggest handler when unloading the plugin
this.app.workspace.iterateCodeMirrors((cm: CodeMirror.Editor) => {
cm.off("change", this.autosuggestHandler);
})
}
setupAutosuggest(): void {
this.autosuggest = new TemplateSuggest(this.app, this);
this.registerCodeMirror((cm: CodeMirror.Editor) => {
cm.on("change", this.autosuggestHandler);
});
}
autosuggestHandler = (
cmEditor: CodeMirror.Editor,
changeObj: CodeMirror.EditorChange
): boolean => {
return this.autosuggest?.update(cmEditor, changeObj);
};
async loadSettings(): Promise<void> {
this.settings = this.settingsWithDefault(await this.loadData());
@ -46,7 +27,7 @@ export default class LiquidTemplates extends Plugin {
async saveSettings(): Promise<void> {
// the templates folder can change
this.setupAutosuggest();
// this.setupAutosuggest();
await this.saveData(this.settings);
}

View file

@ -1,137 +1,146 @@
import { App, PluginSettingTab, Setting } from "obsidian";
import LiquidTemplates from "./main";
import { format } from 'date-fns';
export interface LiquidTemplatesSettings {
templatesFolder: string;
excludeFolders: string;
dateFormat: string;
timeFormat: string;
autocompleteTrigger: string;
}
export const DEFAULT_SETTINGS: LiquidTemplatesSettings = {
templatesFolder: 'templates',
excludeFolders: "",
dateFormat: 'yyyy-MM-dd',
timeFormat: 'k:m',
autocompleteTrigger: ';;'
};
export class LiquidTemplatesSettingsTab extends PluginSettingTab {
plugin: LiquidTemplates;
constructor(app: App, plugin: LiquidTemplates) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl('h2', {
text: 'Liquid Templates',
});
new Setting(containerEl)
.setName('Templates folder')
.setDesc('Folder where to find your templates, it will be used also as root folder for the liquid tags, like the include ones')
.addText((text) =>
text
.setPlaceholder(DEFAULT_SETTINGS.templatesFolder)
.setValue(this.plugin.settings.templatesFolder || DEFAULT_SETTINGS.templatesFolder)
.onChange(async (value) => {
this.plugin.settings.templatesFolder = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Exclude folders')
.setDesc('Name of the folders you want to exclude from the autosuggest menu, relative to the "Templates folder" above. Comma separated values. (useful if you have a "common" or "partial" folder where you store all the partial templates)')
.addText((text) =>
text
.setValue(this.plugin.settings.excludeFolders || DEFAULT_SETTINGS.excludeFolders)
.onChange(async (value) => {
this.plugin.settings.excludeFolders = value;
await this.plugin.saveSettings();
})
);
const dateFormatDesc = document.createDocumentFragment();
dateFormatDesc.append(
'Output format for render dates.',
dateFormatDesc.createEl('br'),
'For more syntax, refer to ',
dateFormatDesc.createEl("a", {
href: "https://date-fns.org/v2.21.3/docs/format",
text: "format reference",
}),
dateFormatDesc.createEl('br'),
'current format: ',
dateFormatDesc.createEl('b', {
cls: 'u-pop',
text: format(new Date, this.plugin.settings.dateFormat)
})
);
new Setting(containerEl)
.setName('Date format')
.setDesc(dateFormatDesc)
.addText((text) =>
text
.setPlaceholder(DEFAULT_SETTINGS.dateFormat)
.setValue(this.plugin.settings.dateFormat || DEFAULT_SETTINGS.dateFormat)
.onChange(async (value) => {
this.plugin.settings.dateFormat = value;
await this.plugin.saveSettings();
})
);
const timeFormatDesc = document.createDocumentFragment();
timeFormatDesc.append(
'Output format for render time.',
timeFormatDesc.createEl('br'),
'For more syntax, refer to ',
timeFormatDesc.createEl("a", {
href: "https://date-fns.org/v2.21.3/docs/format",
text: "format reference",
}),
timeFormatDesc.createEl('br'),
'current format: ',
timeFormatDesc.createEl('b', {
cls: 'u-pop',
text: format(new Date, this.plugin.settings.timeFormat)
})
);
new Setting(containerEl)
.setName('Time format')
.setDesc(timeFormatDesc)
.addText((text) =>
text
.setPlaceholder(DEFAULT_SETTINGS.timeFormat)
.setValue(this.plugin.settings.timeFormat || DEFAULT_SETTINGS.timeFormat)
.onChange(async (value) => {
this.plugin.settings.timeFormat = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Autocomplete trigger')
.setDesc('Character/s that trigger the autocomplete menu')
.addText((text) =>
text
.setPlaceholder(DEFAULT_SETTINGS.autocompleteTrigger)
.setValue(this.plugin.settings.autocompleteTrigger || DEFAULT_SETTINGS.autocompleteTrigger)
.onChange(async (value) => {
this.plugin.settings.autocompleteTrigger = value.trim();
await this.plugin.saveSettings();
})
);
}
}
import { App, PluginSettingTab, Setting } from "obsidian";
import LiquidTemplates from "./main";
import { format } from 'date-fns';
export interface LiquidTemplatesSettings {
templatesFolder: string;
excludeFolders: string;
dateFormat: string;
timeFormat: string;
autocompleteTrigger: string;
}
export const DEFAULT_SETTINGS: LiquidTemplatesSettings = {
templatesFolder: 'templates',
excludeFolders: "",
dateFormat: 'yyyy-MM-dd',
timeFormat: 'k:m',
autocompleteTrigger: ';;'
};
export class LiquidTemplatesSettingsTab extends PluginSettingTab {
plugin: LiquidTemplates;
constructor(app: App, plugin: LiquidTemplates) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl('h2', {
text: 'Liquid Templates',
});
new Setting(containerEl)
.setName('Templates folder')
.setDesc('Folder where to find your templates, it will be used also as root folder for the liquid tags, like the include ones')
.addText((text) =>
text
.setPlaceholder(DEFAULT_SETTINGS.templatesFolder)
.setValue(this.plugin.settings.templatesFolder || DEFAULT_SETTINGS.templatesFolder)
.onChange(async (value) => {
this.plugin.settings.templatesFolder = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Exclude folders')
.setDesc('Name of the folders you want to exclude from the autosuggest menu, relative to the "Templates folder" above. Comma separated values. (useful if you have a "common" or "partial" folder where you store all the partial templates)')
.addText((text) =>
text
.setValue(this.plugin.settings.excludeFolders || DEFAULT_SETTINGS.excludeFolders)
.onChange(async (value) => {
this.plugin.settings.excludeFolders = value;
await this.plugin.saveSettings();
})
);
const dateFormatDesc = document.createDocumentFragment();
dateFormatDesc.append(
'Output format for render dates.',
dateFormatDesc.createEl('br'),
'For more syntax, refer to ',
dateFormatDesc.createEl("a", {
href: "https://date-fns.org/v2.21.3/docs/format",
text: "format reference",
}),
dateFormatDesc.createEl('br'),
'current format: ',
dateFormatDesc.createEl('b', {
cls: 'u-pop',
text: format(new Date, this.plugin.settings.dateFormat)
})
);
new Setting(containerEl)
.setName('Date format')
.setDesc(dateFormatDesc)
.addText((text) =>
text
.setPlaceholder(DEFAULT_SETTINGS.dateFormat)
.setValue(this.plugin.settings.dateFormat || DEFAULT_SETTINGS.dateFormat)
.onChange(async (value) => {
this.plugin.settings.dateFormat = value;
await this.plugin.saveSettings();
})
);
const timeFormatDesc = document.createDocumentFragment();
timeFormatDesc.append(
'Output format for render time.',
timeFormatDesc.createEl('br'),
'For more syntax, refer to ',
timeFormatDesc.createEl("a", {
href: "https://date-fns.org/v2.21.3/docs/format",
text: "format reference",
}),
timeFormatDesc.createEl('br'),
'current format: ',
timeFormatDesc.createEl('b', {
cls: 'u-pop',
text: format(new Date, this.plugin.settings.timeFormat)
})
);
new Setting(containerEl)
.setName('Time format')
.setDesc(timeFormatDesc)
.addText((text) =>
text
.setPlaceholder(DEFAULT_SETTINGS.timeFormat)
.setValue(this.plugin.settings.timeFormat || DEFAULT_SETTINGS.timeFormat)
.onChange(async (value) => {
this.plugin.settings.timeFormat = value;
await this.plugin.saveSettings();
})
);
const triggerDesc = document.createDocumentFragment();
triggerDesc.append(
'Character/s that trigger the autocomplete menu.',
triggerDesc.createEl('br'),
triggerDesc.createEl('b', {
cls: 'u-pop',
text: 'NOTE: if you change this setting you need to reload Obsidian in order to take action.'
})
);
new Setting(containerEl)
.setName('Autocomplete trigger')
.setDesc(triggerDesc)
.addText((text) =>
text
.setPlaceholder(DEFAULT_SETTINGS.autocompleteTrigger)
.setValue(this.plugin.settings.autocompleteTrigger || DEFAULT_SETTINGS.autocompleteTrigger)
.onChange(async (value) => {
this.plugin.settings.autocompleteTrigger = value.trim();
await this.plugin.saveSettings();
})
);
}
}

View file

@ -1,129 +1,129 @@
import { App, ISuggestOwner, Scope } from "obsidian";
import Suggest from "./suggest";
function checkForInputPhrase(
cmEditor: CodeMirror.Editor,
pos: CodeMirror.Position,
phrase: string
): boolean {
const from = {
line: pos.line,
ch: pos.ch - phrase.length,
};
return cmEditor.getRange(from, pos) === phrase;
}
function isCursorBeforePos(
pos: CodeMirror.Position,
cursor: CodeMirror.Position
): boolean {
if (pos.line === cursor.line) {
return cursor.ch < pos.ch;
}
return cursor.line < pos.line;
}
export default abstract class CodeMirrorSuggest<T> implements ISuggestOwner<T> {
protected app: App;
protected cmEditor: CodeMirror.Editor;
private scope: Scope;
private suggestEl: HTMLElement;
private instructionsEl: HTMLElement;
private suggest: Suggest<T>;
private startPos: CodeMirror.Position;
private triggerPhrase: string;
constructor(app: App, triggerPhrase: string) {
this.triggerPhrase = triggerPhrase;
this.app = app;
this.scope = new Scope();
this.suggestEl = createDiv("suggestion-container");
const suggestion = this.suggestEl.createDiv("suggestion");
this.instructionsEl = this.suggestEl.createDiv("prompt-instructions");
this.suggest = new Suggest(this, suggestion, this.scope);
this.scope.register([], "Escape", this.close.bind(this));
}
public setInstructions(
createInstructionsFn: (containerEl: HTMLElement) => void
): void {
this.instructionsEl.empty();
createInstructionsFn(this.instructionsEl);
}
public update(
cmEditor: CodeMirror.Editor,
changeObj: CodeMirror.EditorChange
): boolean {
if (this.cmEditor !== cmEditor) {
this.suggestEl?.detach();
}
this.cmEditor = cmEditor;
const cursorPos = cmEditor.getCursor();
// autosuggest is open
if (this.suggestEl.parentNode) {
if (isCursorBeforePos(this.startPos, cursorPos)) {
this.close();
return false;
}
this.attachAtCursor();
} else {
if (
changeObj.text.length === 1 && // ignore multi-cursors
checkForInputPhrase(this.cmEditor, cursorPos, this.triggerPhrase) &&
!document.querySelector(".suggestion-container") // don't trigger multiple autosuggests
) {
this.startPos = cursorPos;
this.open();
this.attachAtCursor();
}
}
return false;
}
protected getStartPos(): CodeMirror.Position {
return {
line: this.startPos.line,
ch: this.startPos.ch - this.triggerPhrase.length,
};
}
protected getInputStr(): string {
// return string from / to cursor
const cursor = this.cmEditor.getCursor();
const line = this.cmEditor.getLine(cursor.line);
return line.substring(this.startPos.ch, cursor.ch);
}
private attachAtCursor() {
const inputStr = this.getInputStr();
const suggestions = this.getSuggestions(inputStr);
this.suggest.setSuggestions(suggestions);
this.cmEditor.addWidget(this.cmEditor.getCursor(), this.suggestEl, true);
}
open(): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(<any>this.app).keymap.pushScope(this.scope);
}
close(): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(<any>this.app).keymap.popScope(this.scope);
this.startPos = null;
this.suggest.setSuggestions([]);
this.suggestEl.detach();
}
abstract getSuggestions(inputStr: string): T[];
abstract renderSuggestion(item: T, el: HTMLElement): void;
abstract selectSuggestion(item: T, evt: MouseEvent | KeyboardEvent): void;
}
import { App, ISuggestOwner, Scope } from "obsidian";
import Suggest from "./suggest";
function checkForInputPhrase(
cmEditor: CodeMirror.Editor,
pos: CodeMirror.Position,
phrase: string
): boolean {
const from = {
line: pos.line,
ch: pos.ch - phrase.length,
};
return cmEditor.getRange(from, pos) === phrase;
}
function isCursorBeforePos(
pos: CodeMirror.Position,
cursor: CodeMirror.Position
): boolean {
if (pos.line === cursor.line) {
return cursor.ch < pos.ch;
}
return cursor.line < pos.line;
}
export default abstract class CodeMirrorSuggest<T> implements ISuggestOwner<T> {
protected app: App;
protected cmEditor: CodeMirror.Editor;
private scope: Scope;
private suggestEl: HTMLElement;
private instructionsEl: HTMLElement;
private suggest: Suggest<T>;
private startPos: CodeMirror.Position;
private triggerPhrase: string;
constructor(app: App, triggerPhrase: string) {
this.triggerPhrase = triggerPhrase;
this.app = app;
this.scope = new Scope();
this.suggestEl = createDiv("suggestion-container");
const suggestion = this.suggestEl.createDiv("suggestion");
this.instructionsEl = this.suggestEl.createDiv("prompt-instructions");
this.suggest = new Suggest(this, suggestion, this.scope);
this.scope.register([], "Escape", this.close.bind(this));
}
public setInstructions(
createInstructionsFn: (containerEl: HTMLElement) => void
): void {
this.instructionsEl.empty();
createInstructionsFn(this.instructionsEl);
}
public update(
cmEditor: CodeMirror.Editor,
changeObj: CodeMirror.EditorChange
): boolean {
if (this.cmEditor !== cmEditor) {
this.suggestEl?.detach();
}
this.cmEditor = cmEditor;
const cursorPos = cmEditor.getCursor();
// autosuggest is open
if (this.suggestEl.parentNode) {
if (isCursorBeforePos(this.startPos, cursorPos)) {
this.close();
return false;
}
this.attachAtCursor();
} else {
if (
changeObj.text.length === 1 && // ignore multi-cursors
checkForInputPhrase(this.cmEditor, cursorPos, this.triggerPhrase) &&
!document.querySelector(".suggestion-container") // don't trigger multiple autosuggests
) {
this.startPos = cursorPos;
this.open();
this.attachAtCursor();
}
}
return false;
}
protected getStartPos(): CodeMirror.Position {
return {
line: this.startPos.line,
ch: this.startPos.ch - this.triggerPhrase.length,
};
}
protected getInputStr(): string {
// return string from / to cursor
const cursor = this.cmEditor.getCursor();
const line = this.cmEditor.getLine(cursor.line);
return line.substring(this.startPos.ch, cursor.ch);
}
private attachAtCursor() {
const inputStr = this.getInputStr();
const suggestions = this.getSuggestions(inputStr);
this.suggest.setSuggestions(suggestions);
this.cmEditor.addWidget(this.cmEditor.getCursor(), this.suggestEl, true);
}
open(): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(<any>this.app).keymap.pushScope(this.scope);
}
close(): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(<any>this.app).keymap.popScope(this.scope);
this.startPos = null;
this.suggest.setSuggestions([]);
this.suggestEl.detach();
}
abstract getSuggestions(inputStr: string): T[];
abstract renderSuggestion(item: T, el: HTMLElement): void;
abstract selectSuggestion(item: T, evt: MouseEvent | KeyboardEvent): void;
}

View file

@ -1,95 +1,156 @@
import { App, TFile } from "obsidian";
import { Liquid } from "liquidjs";
import { format } from "date-fns";
import CodeMirrorSuggest from "./codemirror-suggest";
import type PoweredTemplates from "../main";
import { getTFilesFromFolder } from "../utils";
import { initEngine } from "src/engine";
interface ITemplateCompletion {
label: string;
file?: TFile;
}
// TODO: create a class for the suggestion that implements the ITemplateCompletion and have some
// utility method to render the template
// TODO: generate a proper context and use the defined settings for the date and time format
export default class TemplateSuggest extends CodeMirrorSuggest<ITemplateCompletion> {
plugin: PoweredTemplates;
engine: Liquid;
constructor(app: App, plugin: PoweredTemplates) {
super(app, plugin.settings.autocompleteTrigger);
this.plugin = plugin;
this.engine = initEngine(app, plugin);
}
open(): void {
super.open();
}
getSuggestions(inputStr: string): ITemplateCompletion[] {
// handle no matches
const suggestions = this.getTemplateSuggestions(inputStr);
if (suggestions.length) {
return suggestions;
} else {
return [{ label: inputStr }];
}
}
getTemplateSuggestions(inputStr: string): ITemplateCompletion[] {
// find the list of files
// TODO: filter before returning all the files
const {
templatesFolder,
excludeFolders
} = this.plugin.settings;
const templates = getTFilesFromFolder(this.app, templatesFolder, excludeFolders.split(','));
return templates
.map(file => ({ label: file.basename, file: file }))
.filter((items) => items.label.toLowerCase().startsWith(inputStr));
}
renderSuggestion(suggestion: ITemplateCompletion, el: HTMLElement): void {
el.setText(suggestion.label);
}
async generateContext() {
const {
dateFormat,
timeFormat
} = this.plugin.settings;
return {
// TODO: improve me
date: format(Date.now(), dateFormat),
time: format(Date.now(), timeFormat),
title: await this.app.workspace.getActiveFile().basename,
default_date_format: dateFormat,
default_time_format: timeFormat
}
}
async selectSuggestion(
suggestion: ITemplateCompletion,
_event: KeyboardEvent | MouseEvent
): Promise<void> {
const head = this.getStartPos();
const anchor = this.cmEditor.getCursor();
if (suggestion.file) {
const templateString = await this.app.vault.read(suggestion.file)
const rendered = await this.engine.parseAndRender(
templateString,
await this.generateContext()
);
this.cmEditor.replaceRange(rendered.trim(), head, anchor);
}
this.close()
}
}
import {
App,
TFile,
Editor,
EditorPosition,
EditorSuggest,
EditorSuggestContext,
EditorSuggestTriggerInfo,
MarkdownView,
} from "obsidian";
import { Liquid } from "liquidjs";
import { format } from "date-fns";
import type PoweredTemplates from "../main";
import { getTFilesFromFolder } from "../utils";
import { initEngine } from "src/engine";
interface ITemplateCompletion {
label: string;
file?: TFile;
inline?: string;
}
// TODO: create a class for the suggestion that implements the ITemplateCompletion and have some
// utility method to render the template
// TODO: generate a proper context and use the defined settings for the date and time format
export default class TemplateSuggest extends EditorSuggest<ITemplateCompletion> {
plugin: PoweredTemplates;
engine: Liquid;
app: App;
protected cmEditor: Editor;
private startPos: CodeMirror.Position;
private triggerPhrase: string;
constructor(app: App, plugin: PoweredTemplates) {
super(app);
this.app = app;
this.plugin = plugin;
this.engine = initEngine(app, plugin);
this.triggerPhrase = this.plugin.settings.autocompleteTrigger;
}
open(): void {
super.open();
}
getSuggestions(context: EditorSuggestContext): ITemplateCompletion[] {
const suggestions = this.getTemplateSuggestions(context.query);
if (suggestions.length) {
return suggestions;
} else {
return [{ label: context.query, inline: `{{${context.query}}}` }];
}
}
getTemplateSuggestions(inputStr: string): ITemplateCompletion[] {
// find the list of files
// TODO: filter before returning all the files
const {
templatesFolder,
excludeFolders
} = this.plugin.settings;
const templates = getTFilesFromFolder(this.app, templatesFolder, excludeFolders.split(','));
return templates
.map(file => ({ label: file.basename, file: file }))
.filter((items) => items.label.toLowerCase().startsWith(inputStr));
}
renderSuggestion(suggestion: ITemplateCompletion, el: HTMLElement): void {
el.setText(suggestion.label);
}
async generateContext() {
const {
dateFormat,
timeFormat
} = this.plugin.settings;
return {
// TODO: improve me
date: format(Date.now(), dateFormat),
time: format(Date.now(), timeFormat),
title: await this.app.workspace.getActiveFile().basename,
default_date_format: dateFormat,
default_time_format: timeFormat
}
}
async selectSuggestion(
suggestion: ITemplateCompletion,
_event: KeyboardEvent | MouseEvent
): Promise<void> {
const { workspace } = this.app;
const activeView = workspace.getActiveViewOfType(MarkdownView);
if (!activeView) return;
const editor = activeView.editor;
const head = this.startPos;
const anchor = editor.getCursor();
const rendered = await this.renderTemplateSuggestion(suggestion);
editor.replaceRange(rendered.trim(), head, anchor);
this.close()
}
async renderTemplateSuggestion(suggestion: ITemplateCompletion): Promise<string> {
let templateString = suggestion.inline
if (suggestion.file) templateString = await this.app.vault.read(suggestion.file!);
if (!templateString) return '';
return this.engine.parseAndRender(
templateString,
await this.generateContext()
);
}
onTrigger(cursor: EditorPosition, editor: Editor, file: TFile): EditorSuggestTriggerInfo {
const lineContents = editor.getLine(cursor.line);
const match = lineContents
.substring(0, cursor.ch)
.match(new RegExp(`(?:^|\s|\W)(${this.triggerPhrase}[^${this.triggerPhrase}]*$)`));
if (match === null) return null;
const triggerInfo = this.getTriggerInfo(match, cursor);
this.startPos = triggerInfo.start;
this.cmEditor = editor;
return triggerInfo;
}
protected getTriggerInfo(
match: RegExpMatchArray,
cursor: EditorPosition
): EditorSuggestTriggerInfo {
return {
start: this.getStartPos(match, cursor.line),
end: cursor,
query: match[1].substring(this.triggerPhrase.length),
};
}
protected getStartPos(match: RegExpMatchArray, line: number): EditorPosition {
return {
line: line,
ch: match.index + match[0].length - match[1].length,
};
}
}