Skip to content

Commit

Permalink
release: 0.2.0
Browse files Browse the repository at this point in the history
  • Loading branch information
RyotaUshio committed Dec 6, 2023
1 parent ef449e8 commit 3efa187
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 84 deletions.
8 changes: 4 additions & 4 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"id": "better-link-autocompletion",
"name": "Better Link Autocompletion",
"version": "0.1.2",
"id": "enhanced-link-suggestions",
"name": "Enhanced Link Suggestions",
"version": "0.2.0",
"minAppVersion": "1.3.5",
"description": "Enhance Obsidian's built-in link autocompletion.",
"description": "Upgrade Obsidian's built-in link suggestions with quick preview & block markdown rendering.",
"author": "Ryota Ushio",
"authorUrl": "https://github.com/RyotaUshio",
"fundingUrl": "https://www.buymeacoffee.com/ryotaushio",
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "obsidian-better-link-autocompletion",
"version": "0.1.2",
"description": "Enhance Obsidian's built-in link autocompletion.",
"name": "obsidian-enhanced-link-suggestions",
"version": "0.2.0",
"description": "Upgrade Obsidian's built-in link suggestions with quick preview & block markdown rendering.",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
Expand Down
79 changes: 30 additions & 49 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { MarkdownRenderer, finishRenderMath } from 'obsidian';
import { Component, EditorSuggest, HoverParent, HoverPopover, Keymap, MarkdownView, Plugin, SearchMatches, TFile, renderMath, stripHeadingForLink } from 'obsidian';
import { DEFAULT_SETTINGS, MyPluginSettings, SampleSettingTab } from './settings';
import { Component, MarkdownRenderer, EditorSuggest, HoverParent, HoverPopover, Keymap, Plugin, stripHeadingForLink } from 'obsidian';
import { around } from 'monkey-around';
import { BlockLinkInfo, CalloutLinkInfo, FileLinkInfo, HeadingLinkInfo, MathLinkInfo, MathNode } from 'typings/items';

type Item = FileLinkInfo | HeadingLinkInfo | MathLinkInfo | CalloutLinkInfo | BlockLinkInfo;
import { DEFAULT_SETTINGS, MyPluginSettings, SampleSettingTab } from 'settings';
import { BlockLinkInfo, FileLinkInfo, HeadingLinkInfo } from 'typings/items';
import { extractFirstNLines, render } from 'utils';

type Item = FileLinkInfo | HeadingLinkInfo | BlockLinkInfo;
type BuiltInAutocompletion = EditorSuggest<Item> & { component: Component };

export default class MyPlugin extends Plugin {
Expand Down Expand Up @@ -42,12 +43,13 @@ export default class MyPlugin extends Plugin {
open(old) {
return function () {
old.call(this);
this.component.load();
(this.component as Component).registerDomEvent(window, 'keydown', (event) => {
const self = this as BuiltInAutocompletion;
self.component.load();
self.component.registerDomEvent(window, 'keydown', (event) => {
if (suggest.isOpen && Keymap.isModifier(event, plugin.settings.modifierToPreview)) {
const item = suggest.suggestions.values[suggest.suggestions.selectedItem];
const parent = new KeyupHandlingHoverParent(plugin, suggest);
this.component.addChild(parent);
const parent = new KeyEventAwareHoverParent(plugin, suggest);
self.component.addChild(parent);
if (item.type === 'file') {
app.workspace.trigger('link-hover', parent, null, item.file.path, "")
} else if (item.type === 'heading') {
Expand All @@ -72,34 +74,27 @@ export default class MyPlugin extends Plugin {
if (plugin.settings.dev) console.log(item);

if (item.type === "block") {
if (plugin.settings.math && item.node?.type === "math") {
renderInSuggestionTitleEl(el, (titleEl) => {
titleEl.replaceChildren(renderMath((item.node as MathNode).value, true));
});
finishRenderMath();
return;
}

if (plugin.settings.callout && item.node?.type === "callout") {
renderInSuggestionTitleEl(el, async (titleEl) => {
await MarkdownRenderer.render(
app,
extractCalloutTitle(item.content.slice(item.node.position.start.offset, item.node.position.end.offset)),
titleEl,
item.file.path,
plugin
);
});
return;
}
if (plugin.settings[item.node.type] === false) return;

let text = item.content.slice(item.node.position.start.offset, item.node.position.end.offset);
let limit: number | undefined = (plugin.settings as any)[item.node.type + 'Lines'];
if (limit) text = extractFirstNLines(text, limit);

render(el, async (containerEl) => {
containerEl.setAttribute('data-line', item.node.position.start.line.toString());
await MarkdownRenderer.render(
app, text, containerEl, item.file.path, this.component
);
containerEl.querySelectorAll('.copy-code-button').forEach((el) => el.remove());
});
}
}
}
}));
}
}

export class KeyupHandlingHoverParent extends Component implements HoverParent {
export class KeyEventAwareHoverParent extends Component implements HoverParent {
#hoverPopover: HoverPopover | null;

constructor(private plugin: MyPlugin, private suggest: BuiltInAutocompletion) {
Expand All @@ -112,6 +107,11 @@ export class KeyupHandlingHoverParent extends Component implements HoverParent {
this.hideChild();
}

hideChild() {
/// @ts-ignore
this.#hoverPopover?.hide();
}

get hoverPopover() {
return this.#hoverPopover;
}
Expand Down Expand Up @@ -139,23 +139,4 @@ export class KeyupHandlingHoverParent extends Component implements HoverParent {
})
}
}

hideChild() {
/// @ts-ignore
this.#hoverPopover?.hide();
}
}


function extractCalloutTitle(text: string) {
const lineBreak = text.indexOf('\n');
return lineBreak === -1 ? text : text.slice(0, lineBreak + 1);
}

function renderInSuggestionTitleEl(el: HTMLElement, cb: (suggestionTitleEl: HTMLElement) => void) {
const suggestionTitleEl = el.querySelector<HTMLElement>('.suggestion-title');
if (suggestionTitleEl) {
suggestionTitleEl.replaceChildren();
cb(suggestionTitleEl)
};
}
148 changes: 125 additions & 23 deletions src/settings.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,160 @@
import { Modifier, PluginSettingTab, Setting } from 'obsidian';
import MyPlugin from './main';
import { getModifierNameInPlatform } from 'utils';


// https://stackoverflow.com/a/50851710/13613783
export type BooleanKeys<T> = { [k in keyof T]: T[k] extends boolean ? k : never }[keyof T];
export type NumberKeys<T> = { [k in keyof T]: T[k] extends number ? k : never }[keyof T];
// Inspired by https://stackoverflow.com/a/50851710/13613783
export type KeysOfType<Obj, Type> = { [k in keyof Obj]: Obj[k] extends Type ? k : never }[keyof Obj];


export interface MyPluginSettings {
math: boolean;
// code: boolean;
code: boolean;
blockquote: boolean;
heading: boolean;
paragraph: boolean;
callout: boolean;
dev: boolean;
math: boolean;
listItem: boolean;
footnoteDefinition: boolean;
element: boolean;
table: boolean;
codeLines: number;
blockquoteLines: number;
paragraphLines: number;
calloutLines: number;
listItemLines: number;
footnoteDefinitionLines: number;
elementLines: number;
tableLines: number;
modifierToPreview: Modifier;
compactPreview: boolean;
dev: boolean;
}

export const DEFAULT_SETTINGS: MyPluginSettings = {
math: true,
// code: true,
code: true,
blockquote: true,
heading: true,
paragraph: true,
callout: true,
dev: false,
math: true,
listItem: true,
footnoteDefinition: true,
element: true,
table: true,
codeLines: 0,
blockquoteLines: 0,
paragraphLines: 0,
calloutLines: 0,
listItemLines: 0,
footnoteDefinitionLines: 0,
elementLines: 0,
tableLines: 0,
modifierToPreview: 'Alt',
compactPreview: false,
dev: false,
}

export class SampleSettingTab extends PluginSettingTab {
constructor(public plugin: MyPlugin) {
super(plugin.app, plugin);
}

addToggleSetting(name: string, settingName: BooleanKeys<MyPluginSettings>) {
addToggleSetting(settingName: KeysOfType<MyPluginSettings, boolean>) {
return new Setting(this.containerEl)
.setName(name)
.addToggle((toggle) => {
toggle.setValue(this.plugin.settings[settingName])
toggle.onChange(async (value) => {
this.plugin.settings[settingName] = value;
await this.plugin.saveSettings();
});
.onChange(async (value) => {
this.plugin.settings[settingName] = value;
await this.plugin.saveSettings();
});
});
}

addEnableSetting(name: string, settingName: BooleanKeys<MyPluginSettings>) {
return this.addToggleSetting(name, settingName).setHeading();
addDropdowenSetting(settingName: KeysOfType<MyPluginSettings, string>, options: string[], display?: (option: string) => string) {
return new Setting(this.containerEl)
.addDropdown((dropdown) => {
const displayNames = new Set<string>();
for (const option of options) {
const displayName = display?.(option) ?? option;
if (!displayNames.has(displayName)) {
dropdown.addOption(option, displayName);
displayNames.add(displayName);
}
};
dropdown.setValue(this.plugin.settings[settingName])
.onChange(async (value) => {
// @ts-ignore
this.plugin.settings[settingName] = value;
await this.plugin.saveSettings();
});
});
}

addSliderSetting(settingName: KeysOfType<MyPluginSettings, number>, min: number, max: number, step: number) {
return new Setting(this.containerEl)
.addSlider((slider) => {
slider.setLimits(min, max, step)
.setValue(this.plugin.settings[settingName])
.setDynamicTooltip()
.onChange(async (value) => {
// @ts-ignore
this.plugin.settings[settingName] = value;
await this.plugin.saveSettings();
});
});
}

display(): void {
const { containerEl } = this;
containerEl.empty();
this.containerEl.empty();

this.addDropdowenSetting('modifierToPreview', ['Mod', 'Ctrl', 'Meta', 'Shift', 'Alt'], getModifierNameInPlatform)
.setName('Modifier key for quick preview')
.setDesc('Hold down this key to preview the link without clicking.');
this.addToggleSetting('compactPreview')
.setName('Compact hover preview')
.setDesc('Use compact font size for the hover preview.');

new Setting(this.containerEl).setName('Block markdown rendering').setHeading();
this.addToggleSetting('paragraph').setName('Render paragraphs');
this.addSliderSetting('paragraphLines', 0, 10, 1)
.setName('Paragraph line limit')
.setDesc('Maximum number of lines to render. Set to 0 to disable line limit.');
this.addToggleSetting('heading').setName('Render headings');
this.addToggleSetting('callout').setName('Render callouts');
this.addSliderSetting('calloutLines', 0, 10, 1)
.setName('Callout line limit')
.setDesc('Maximum number of lines to render. Set to 0 to disable line limit.');
this.addToggleSetting('blockquote').setName('Render blockquotes');
this.addSliderSetting('blockquoteLines', 0, 10, 1)
.setName('Blockquote line limit')
.setDesc('Maximum number of lines to render. Set to 0 to disable line limit.');
this.addToggleSetting('code').setName('Render code blocks');
this.addSliderSetting('codeLines', 0, 10, 1)
.setName('Code block line limit')
.setDesc('Maximum number of lines to render. Set to 0 to disable line limit.');
this.addToggleSetting('math').setName('Render math blocks');
this.addToggleSetting('listItem').setName('Render list items');
this.addSliderSetting('listItemLines', 0, 10, 1)
.setName('List item line limit')
.setDesc('Maximum number of lines to render. Set to 0 to disable line limit.');
this.addToggleSetting('table').setName('Render tables');
this.addSliderSetting('tableLines', 0, 10, 1)
.setName('Table line limit')
.setDesc('Maximum number of lines to render. Set to 0 to disable line limit.');
this.addToggleSetting('footnoteDefinition').setName('Render footnote definitions');
this.addSliderSetting('footnoteDefinitionLines', 0, 10, 1)
.setName('Footnote definition line limit')
.setDesc('Maximum number of lines to render. Set to 0 to disable line limit.');
this.addToggleSetting('element').setName('Render elements');
this.addSliderSetting('elementLines', 0, 10, 1)
.setName('Element line limit')
.setDesc('Maximum number of lines to render. Set to 0 to disable line limit.');

new Setting(this.containerEl).setName('Advanced').setHeading();

this.addEnableSetting('Math blocks', 'math');
this.addEnableSetting('Callouts', 'callout');
this.addToggleSetting('Compact hover preview', 'compactPreview');
this.addToggleSetting('Dev mode', 'dev');
this.addToggleSetting('dev')
.setName('Dev mode')
.setDesc('Show metadata about suggestion items in the dev console.');
}
}
3 changes: 2 additions & 1 deletion src/typings/items.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ interface BlockLinkInfo extends LinkInfo {
type: "block";
idMatch: SearchMatches | null;
subpath: string;
node: CalloutNode | MathNode;
node: Node; // CalloutNode | MathNode;
display: string;
content: string;
}

interface Node {
type: 'code' | 'blockquote' | 'heading' | 'paragraph' | 'callout' | 'math' | 'listItem' | 'footnoteDefinition' | 'element' | 'table';
children: Node[];
position: {
start: Loc;
Expand Down
32 changes: 32 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Modifier, Platform } from "obsidian";


export function extractFirstNLines(text: string, n: number) {
const lines = text.split('\n');
return lines.slice(0, n).join('\n');
}

export function render(el: HTMLElement, cb: (containerEl: HTMLElement) => void) {
const titleEl = el.querySelector<HTMLElement>('.suggestion-title');
if (titleEl) {
const containerEl = createDiv({cls: ['markdown-rendered']});
titleEl.replaceChildren(containerEl);
cb(containerEl);
};
}

export function getModifierNameInPlatform(mod: Modifier): string {
if (mod == "Mod") {
return Platform.isMacOS || Platform.isIosApp ? "command" : "Ctrl";
}
if (mod == "Shift") {
return Platform.isMacOS || Platform.isIosApp ? "shift" : "Shift";
}
if (mod == "Alt") {
return Platform.isMacOS || Platform.isIosApp ? "option" : "Alt";
}
if (mod == "Meta") {
return Platform.isMacOS || Platform.isIosApp ? "command" : Platform.isWin ? "Win" : "Meta";
}
return "ctrl";
}
Loading

0 comments on commit 3efa187

Please sign in to comment.