From 1e76d5d2460b93837da9d05c76048c65b5d551f3 Mon Sep 17 00:00:00 2001 From: Hady Osman Date: Sat, 19 Feb 2022 23:13:38 +1300 Subject: [PATCH 1/2] Explicit typing for all type imports --- src/cmExtension/suggestionsExtension.ts | 8 ++++---- src/index.ts | 2 +- src/indexing/indexer.ts | 4 ++-- src/search/index.ts | 2 +- src/utils/getAliases.spec.ts | 2 +- src/utils/getAliases.ts | 2 +- tsconfig.json | 8 ++++---- webpack.config.ts | 2 +- 8 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/cmExtension/suggestionsExtension.ts b/src/cmExtension/suggestionsExtension.ts index abdc658..e1de7b7 100644 --- a/src/cmExtension/suggestionsExtension.ts +++ b/src/cmExtension/suggestionsExtension.ts @@ -1,15 +1,15 @@ 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 { debounce, type Debouncer } from 'obsidian'; -import Search from '../search'; +import type Search from '../search'; import { SuggestionsPopup } from '../components/suggestionsPopup'; import './suggestionsExtension.css'; diff --git a/src/index.ts b/src/index.ts index 75630fe..adb39ac 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/src/indexing/indexer.ts b/src/indexing/indexer.ts index 211ca21..4c85f24 100644 --- a/src/indexing/indexer.ts +++ b/src/indexing/indexer.ts @@ -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; diff --git a/src/search/index.ts b/src/search/index.ts index 4101dcd..a27e782 100644 --- a/src/search/index.ts +++ b/src/search/index.ts @@ -1,6 +1,6 @@ import { Trie, Emit } from '@tanishiking/aho-corasick'; -import { Indexer } from '../indexing/indexer'; +import type { Indexer } from '../indexing/indexer'; type SearchResult = { start: number; diff --git a/src/utils/getAliases.spec.ts b/src/utils/getAliases.spec.ts index be2df17..681b4b5 100644 --- a/src/utils/getAliases.spec.ts +++ b/src/utils/getAliases.spec.ts @@ -1,4 +1,4 @@ -import { CachedMetadata, FrontMatterCache } from 'obsidian'; +import type { CachedMetadata, FrontMatterCache } from 'obsidian'; import { getAliases } from './getAliases'; diff --git a/src/utils/getAliases.ts b/src/utils/getAliases.ts index 1d02181..111cdb3 100644 --- a/src/utils/getAliases.ts +++ b/src/utils/getAliases.ts @@ -1,4 +1,4 @@ -import { CachedMetadata } from 'obsidian'; +import type { CachedMetadata } from 'obsidian'; export const getAliases = (metadata: CachedMetadata): string[] => { const frontmatterAliases = metadata?.frontmatter?.['aliases']; diff --git a/tsconfig.json b/tsconfig.json index a725ead..72f77fc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,17 +1,17 @@ { + "extends": "@tsconfig/svelte/tsconfig.json", + "include": ["src/**/*", "tests/**/*", "webpack.config.ts"], "exclude": ["node_modules/*"], "compilerOptions": { - "target": "es5", + "target": "es6", "types": ["node", "svelte", "jest"], "baseUrl": ".", "paths": { "src": ["src/*", "tests/*"], "~/*": ["src/*"] }, - "resolveJsonModule": true, - "esModuleInterop": true, - "downlevelIteration": true + "resolveJsonModule": true }, // Fixes errors when changing `module` to ES in the above compiler options // See: https://github.com/webpack/webpack-cli/issues/2458#issuecomment-846635277 diff --git a/webpack.config.ts b/webpack.config.ts index 8c17122..0e32c72 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -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'; From 7104b5ba6dcf2a29e42c4fcf78c622a1f4ac37d0 Mon Sep 17 00:00:00 2001 From: Hady Osman Date: Sun, 20 Feb 2022 00:26:15 +1300 Subject: [PATCH 2/2] Render suggestions in Menu. Support multiple suggestions for a keyword --- manifest.json | 2 +- package.json | 6 +-- src/cmExtension/suggestionsExtension.ts | 30 +++++++------- src/components/suggestionsPopup.ts | 53 +++++++++++-------------- src/index.ts | 9 ++++- src/search/index.ts | 16 ++++++-- tsconfig.json | 10 ++--- webpack.config.ts | 5 +-- 8 files changed, 69 insertions(+), 62 deletions(-) diff --git a/manifest.json b/manifest.json index 9832d81..6fa9405 100644 --- a/manifest.json +++ b/manifest.json @@ -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", diff --git a/package.json b/package.json index 6856f24..a115769 100644 --- a/package.json +++ b/package.json @@ -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": { @@ -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", @@ -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" } } diff --git a/src/cmExtension/suggestionsExtension.ts b/src/cmExtension/suggestionsExtension.ts index e1de7b7..718e8de 100644 --- a/src/cmExtension/suggestionsExtension.ts +++ b/src/cmExtension/suggestionsExtension.ts @@ -7,26 +7,26 @@ import { type PluginValue, } from '@codemirror/view'; import { RangeSetBuilder } from '@codemirror/rangeset'; -import { debounce, type Debouncer } from 'obsidian'; +import { App, debounce, type Debouncer } from 'obsidian'; +import { showSuggestionsModal } from '../components/suggestionsPopup'; import type Search from '../search'; -import { SuggestionsPopup } from '../components/suggestionsPopup'; 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 => { +export const suggestionsExtension = (search: Search, app: App): ViewPlugin => { return ViewPlugin.fromClass( class { decorations: DecorationSet; @@ -68,7 +68,7 @@ export const suggestionsExtension = (search: Search): ViewPlugin => 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)); } } @@ -83,18 +83,20 @@ export const suggestionsExtension = (search: Search): ViewPlugin => 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, diff --git a/src/components/suggestionsPopup.ts b/src/components/suggestionsPopup.ts index 3133b06..7eee461 100644 --- a/src/components/suggestionsPopup.ts +++ b/src/components/suggestionsPopup.ts @@ -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); +}; diff --git a/src/index.ts b/src/index.ts index adb39ac..87a6654 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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()); diff --git a/src/search/index.ts b/src/search/index.ts index a27e782..e406c31 100644 --- a/src/search/index.ts +++ b/src/search/index.ts @@ -1,3 +1,4 @@ +import _ from 'lodash'; import { Trie, Emit } from '@tanishiking/aho-corasick'; import type { Indexer } from '../indexing/indexer'; @@ -5,12 +6,21 @@ 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(); @@ -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 } diff --git a/tsconfig.json b/tsconfig.json index 72f77fc..13bc40a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,17 +1,17 @@ { - "extends": "@tsconfig/svelte/tsconfig.json", - "include": ["src/**/*", "tests/**/*", "webpack.config.ts"], "exclude": ["node_modules/*"], "compilerOptions": { - "target": "es6", - "types": ["node", "svelte", "jest"], + "target": "es5", + "types": ["node", "jest"], "baseUrl": ".", "paths": { "src": ["src/*", "tests/*"], "~/*": ["src/*"] }, - "resolveJsonModule": true + "resolveJsonModule": true, + "esModuleInterop": true, + "downlevelIteration": true }, // Fixes errors when changing `module` to ES in the above compiler options // See: https://github.com/webpack/webpack-cli/issues/2458#issuecomment-846635277 diff --git a/webpack.config.ts b/webpack.config.ts index 0e32c72..1779028 100644 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -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',