Skip to content

Commit

Permalink
Merge pull request #17 from hadynz/popup-as-menu
Browse files Browse the repository at this point in the history
Render suggestions in Menu. Support multiple suggestions for a keyword
  • Loading branch information
hadynz authored Feb 19, 2022
2 parents d979cd5 + 7104b5b commit 5fb9f47
Show file tree
Hide file tree
Showing 11 changed files with 75 additions and 68 deletions.
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"id": "obsidian-sidekick",
"name": "Sidekick",
"description": "A companion to identify hidden connections that match your tags and pages",
"version": "1.3.0",
"version": "1.4.0",
"minAppVersion": "0.13.8",
"author": "Hady Osman",
"authorUrl": "https://hady.geek.nz",
Expand Down
6 changes: 2 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "obsidian-sidekick",
"version": "1.3.0",
"version": "1.4.0",
"description": "A companion to identify hidden connections that match your tags and pages",
"main": "src/index.ts",
"repository": {
Expand Down Expand Up @@ -51,7 +51,6 @@
"lint-staged": "^11.2.4",
"obsidian": "^0.13.11",
"prettier": "^2.2.1",
"prettier-plugin-svelte": "^2.2.0",
"rimraf": "^3.0.2",
"style-loader": "^3.3.1",
"terser-webpack-plugin": "^5.2.5",
Expand All @@ -73,7 +72,6 @@
"@tanishiking/aho-corasick": "^0.0.1",
"lodash": "^4.17.21",
"lokijs": "^1.5.12",
"tiny-typed-emitter": "^2.1.0",
"tippy.js": "^6.3.7"
"tiny-typed-emitter": "^2.1.0"
}
}
36 changes: 19 additions & 17 deletions src/cmExtension/suggestionsExtension.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
import {
Decoration,
DecorationSet,
EditorView,
ViewPlugin,
ViewUpdate,
PluginValue,
type DecorationSet,
type PluginValue,
} from '@codemirror/view';
import { RangeSetBuilder } from '@codemirror/rangeset';
import { debounce, Debouncer } from 'obsidian';
import { App, debounce, type Debouncer } from 'obsidian';

import Search from '../search';
import { SuggestionsPopup } from '../components/suggestionsPopup';
import { showSuggestionsModal } from '../components/suggestionsPopup';
import type Search from '../search';

import './suggestionsExtension.css';

const SuggestionCandidateClass = 'cm-suggestion-candidate';

const underlineDecoration = (start: number, end: number, replaceText: string) =>
const underlineDecoration = (start: number, end: number, keyword: string) =>
Decoration.mark({
class: SuggestionCandidateClass,
attributes: {
'data-replace-text': replaceText,
'data-keyword': keyword,
'data-position-start': `${start}`,
'data-position-end': `${end}`,
},
});

export const suggestionsExtension = (search: Search): ViewPlugin<PluginValue> => {
export const suggestionsExtension = (search: Search, app: App): ViewPlugin<PluginValue> => {
return ViewPlugin.fromClass(
class {
decorations: DecorationSet;
Expand Down Expand Up @@ -68,7 +68,7 @@ export const suggestionsExtension = (search: Search): ViewPlugin<PluginValue> =>
const end = from + result.end;

// Add the decoration
builder.add(start, end, underlineDecoration(start, end, result.replaceText));
builder.add(start, end, underlineDecoration(start, end, result.keyword));
}
}

Expand All @@ -83,18 +83,20 @@ export const suggestionsExtension = (search: Search): ViewPlugin<PluginValue> =>
const target = e.target as HTMLElement;
const isCandidate = target.classList.contains(SuggestionCandidateClass);

if (!isCandidate) {
// Do nothing if user right-clicked or unrelated DOM element was clicked
if (!isCandidate || e.button !== 0) {
return;
}

// Extract position and replacement text from target element data attributes state
const { positionStart, positionEnd, replaceText } = target.dataset;

const popup = new SuggestionsPopup();
popup.show({
target,
text: replaceText,
onClick: () => {
const { positionStart, positionEnd, keyword } = target.dataset;

// Show suggestions modal
showSuggestionsModal({
app,
mouseEvent: e,
suggestions: search.getReplacementSuggestions(keyword),
onClick: (replaceText) => {
view.dispatch({
changes: {
from: +positionStart,
Expand Down
53 changes: 23 additions & 30 deletions src/components/suggestionsPopup.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,29 @@
import tippy from 'tippy.js';
import 'tippy.js/dist/tippy.css';
import { App, Menu, MenuItem } from 'obsidian';

import './suggestionsPopup.css';
type SuggestionsModalProps = {
app: App;
mouseEvent: MouseEvent;
suggestions: string[];
onClick: (replaceText: string) => void;
};

type SuggestionsPopupProps = {
target: HTMLElement;
text: string;
onClick: () => void;
const item = (icon, title, click) => {
return (item: MenuItem) => item.setIcon(icon).setTitle(title).onClick(click);
};

export class SuggestionsPopup {
public show(props: SuggestionsPopupProps): void {
const { text, onClick, target } = props;
export const showSuggestionsModal = (props: SuggestionsModalProps): void => {
const { app, mouseEvent, suggestions, onClick } = props;

const menu = new Menu(app);

const button = document.createElement('button');
button.innerText = text;
button.title = 'Suggestion';
button.onclick = () => {
onClick();
tippyInstance.hide();
};
suggestions.forEach((replaceText) => {
menu.addItem(
item('pencil', `Replace with ${replaceText}`, () => {
onClick(replaceText);
})
);
});

const tippyInstance = tippy(target, {
content: button,
trigger: 'click',
theme: 'obsidian',
interactive: true,
appendTo: document.body,
allowHTML: true,
onHidden: () => {
tippyInstance.destroy();
},
});
}
}
menu.addSeparator();
menu.showAtMouseEvent(mouseEvent);
};
11 changes: 8 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Plugin } from 'obsidian';
import { Extension } from '@codemirror/state';
import type { Extension } from '@codemirror/state';

import Search from './search';
import { PluginHelper } from './plugin-helper';
Expand All @@ -23,8 +23,13 @@ export default class TagsAutosuggestPlugin extends Plugin {
pluginHelper.onFileMetadataChanged((file) => indexer.replaceFileIndices(file));

// Re/load highlighting extension after any changes to index
indexer.on('indexRebuilt', () => this.updateEditorExtension(suggestionsExtension(search)));
indexer.on('indexUpdated', () => this.updateEditorExtension(suggestionsExtension(search)));
indexer.on('indexRebuilt', () =>
this.updateEditorExtension(suggestionsExtension(search, this.app))
);

indexer.on('indexUpdated', () =>
this.updateEditorExtension(suggestionsExtension(search, this.app))
);

// Build search index on startup (very expensive process)
pluginHelper.onLayoutReady(() => indexer.buildIndex());
Expand Down
4 changes: 2 additions & 2 deletions src/indexing/indexer.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import lokijs from 'lokijs';
import { TypedEmitter } from 'tiny-typed-emitter';
import { TFile } from 'obsidian';
import type { TFile } from 'obsidian';

import { PluginHelper } from '../plugin-helper';
import type { PluginHelper } from '../plugin-helper';

type Document = {
fileCreationTime: number;
Expand Down
18 changes: 14 additions & 4 deletions src/search/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
import _ from 'lodash';
import { Trie, Emit } from '@tanishiking/aho-corasick';

import { Indexer } from '../indexing/indexer';
import type { Indexer } from '../indexing/indexer';

type SearchResult = {
start: number;
end: number;
replaceText: string;
keyword: string;
};

const isEqual = (a: Emit, b: Emit) => {
return a.start === b.start && a.keyword === b.keyword;
};

export default class Search {
constructor(private indexer: Indexer) {}

public getReplacementSuggestions(keyword: string): string[] {
const keywords = this.indexer.getDocumentsByKeyword(keyword).map((doc) => doc.replaceText);
return _.uniq(keywords);
}

public find(text: string): SearchResult[] {
const keywords = this.indexer.getKeywords();

Expand All @@ -28,12 +38,12 @@ export default class Search {
}

private mapToSearchResults(results: Emit[]): SearchResult[] {
return results
return _.uniqWith(results, isEqual)
.filter((result) => this.keywordExistsInIndex(result.keyword))
.map((result) => ({
start: result.start,
end: result.end + 1,
replaceText: this.indexer.getDocumentsByKeyword(result.keyword)[0].replaceText,
keyword: result.keyword,
}))
.sort((a, b) => a.start - b.start); // Must sort by start position to prepare for highlighting
}
Expand Down
2 changes: 1 addition & 1 deletion src/utils/getAliases.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CachedMetadata, FrontMatterCache } from 'obsidian';
import type { CachedMetadata, FrontMatterCache } from 'obsidian';

import { getAliases } from './getAliases';

Expand Down
2 changes: 1 addition & 1 deletion src/utils/getAliases.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CachedMetadata } from 'obsidian';
import type { CachedMetadata } from 'obsidian';

export const getAliases = (metadata: CachedMetadata): string[] => {
const frontmatterAliases = metadata?.frontmatter?.['aliases'];
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"exclude": ["node_modules/*"],
"compilerOptions": {
"target": "es5",
"types": ["node", "svelte", "jest"],
"types": ["node", "jest"],
"baseUrl": ".",
"paths": {
"src": ["src/*", "tests/*"],
Expand Down
7 changes: 3 additions & 4 deletions webpack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import path from 'path';
import pack from './package.json';
import CopyPlugin from 'copy-webpack-plugin';
import TerserPlugin from 'terser-webpack-plugin';
import { Configuration, DefinePlugin } from 'webpack';
import { type Configuration, DefinePlugin } from 'webpack';

const isProduction = process.env.NODE_ENV === 'production';

Expand Down Expand Up @@ -62,11 +62,10 @@ const config: Configuration = {
],
resolve: {
alias: {
svelte: path.resolve('node_modules', 'svelte'),
'~': path.resolve(__dirname, 'src'),
},
extensions: ['.ts', '.tsx', '.js', '.svelte'],
mainFields: ['svelte', 'browser', 'module', 'main'],
extensions: ['.ts', '.tsx', '.js'],
mainFields: ['browser', 'module', 'main'],
},
externals: {
obsidian: 'commonjs2 obsidian',
Expand Down

0 comments on commit 5fb9f47

Please sign in to comment.