import { IDisposable } from "@common/types" ; import { MarkdownEditor } from "@renderer/editors/editor-markdown" ; import { ITagSearchResult } from "@main/plugins/crossref-plugin" ; import { MainIpcHandlers } from "@main/MainIPC" ; import { EditorView as ProseEditorView } from "prosemirror-view" ; import { suggest , Suggester , SuggestKeyBinding } from "prosemirror-suggest" ; import { render } from "solid-js/web" ; import { createSignal , For } from "solid-js" ; type GCommand = ( acceptedText : string ) => void ; export function makeSuggestionPlugin ( editor ) { let showSuggestions = false ; const acceptSuggestion = ( { command } ) => { if ( ! showSuggestions ) { return false ; } if ( editor . popup === null ) { return false ; } let selected = editor . popup . getPopupSelected ( ) ; if ( ! selected ) { return false ; } command ( selected . result ) ; return true ; } ; const suggestTags = { noDecorations : true , char : '[[' , name : 'emoji-suggestion' , appendText : '' , supportedCharacters : /[^\s]/i , keyBindings : { ArrowUp : ( ) => { editor . popup ?. cyclePopupEntries ( - 1 ) ; if ( showSuggestions ) { return true ; } } , ArrowDown : ( ) => { editor . popup ?. cyclePopupEntries ( + 1 ) ; if ( showSuggestions ) { return true ; } } , Enter : acceptSuggestion , Tab : acceptSuggestion , Esc : ( ) => { showSuggestions = false ; } , } , onChange : ( params ) => { showSuggestions = true ; let query = params . queryText . full ; editor . popup ?. showPopupAt ( params . range . from , query ) ; } , onExit : ( ) => { showSuggestions = false ; editor . popup ?. hidePopup ( ) ; } , createCommand : ( { match , view } ) => { return ( acceptedText ) => { if ( ! acceptedText ) { throw new Error ( "no replacement text provided!" ) ; } showSuggestions = false ; const tr = view . state . tr ; const { from , end : to } = match . range ; tr . insertText ( acceptedText , from + 2 , to ) ; view . dispatch ( tr ) ; } ; } , } ; return suggest ( suggestTags ) ; } export class SuggestionPopup implements IDisposable { private _proseEditorView : ProseEditorView ; private _editorElt : HTMLElement ; todo private _mainProxy : MainIpcHandlers ; private _popupElt : HTMLElement ; private _popupData ? : ( ) => { data : ITagSearchResult [ ] ; } ; private _setPopupData ? : ( v : { data : ITagSearchResult [ ] ; } ) => { data : ITagSearchResult [ ] ; } ; private _popupIndex ? : ( ) => number ; private _setPopupIndex ? : ( v : number ) => number ; constructor ( view , editorElt , mainProxy ) { this . _proseEditorView = view ; this . _editorElt = editorElt ; this . _mainProxy = mainProxy ; let [ popupData , setPopupData ] = createSignal < { data : ITagSearchResult [ ] ; } > ( { data : [ ] } ) ; let [ popupIndex , setPopupIndex ] = createSignal ( 0 ) ; this . _popupData = popupData ; this . _setPopupData = setPopupData ; this . _popupIndex = popupIndex ; this . _setPopupIndex = setPopupIndex ; const Popup = ( ) => { return ( < div > < For each = { popupData ( ) . data } fallback = { "" } > { ( data , idx ) => ( < div class = { ( idx ( ) == popupIndex ( ) ) ? "popup-item selected" : "popup-item" } innerHTML = { data . resultEmphasized } / > ) } < /For> < /div>); } ; this . _popupElt = document . createElement ( "div" ) ; this . _popupElt . setAttribute ( "class" , "popup" ) ; this . _editorElt . appendChild ( this . _popupElt ) ; render ( Popup , this . _popupElt ) ; } dispose ( ) { this . _popupElt . remove ( ) ; } showPopupAtCursor ( query ) { if ( ! this . _proseEditorView ) { return ; } this . showPopupAt ( this . _proseEditorView . state . selection . to , query ) ; } async showPopupAt ( pos , query ) { if ( ! this . _proseEditorView ) { return ; } todo let suggestions = await this . _mainProxy . tag . fuzzyTagSearch ( query ) ; let coords = this . _proseEditorView . coordsAtPos ( pos ) ; if ( this . _setPopupData ) { this . _setPopupData ( { data : suggestions } ) ; } let rect = this . _editorElt . getBoundingClientRect ( ) ; let win = this . _editorElt . ownerDocument . defaultView ; let editorX = rect . left + ( win ?. pageXOffset || 0 ) ; let editorY = rect . top + ( win ?. pageYOffset || 0 ) ; let x = coords . left - editorX + 1 ; let y = coords . bottom - editorY ; let popupElt = this . _popupElt ; popupElt . classList . add ( "visible" ) ; popupElt . style . left = `${ x | 0 }px` ; popupElt . style . top = `${ y | 0 }px` ; } hidePopup ( ) { this . _popupElt . classList . remove ( "visible" ) ; } cyclePopupEntries ( direction ) { if ( ! this . _popupIndex || ! this . _setPopupIndex || ! this . _popupData ) { return ; } let length = this . _popupData ( ) . data . length ; this . _setPopupIndex ( Math . min ( length - 1 , Math . max ( 0 , ( this . _popupIndex ( ) + direction ) ) ) ) ; } getPopupSelected ( ) { if ( ! this . _popupData || ! this . _popupIndex ) { return null ; } return this . _popupData ( ) . data [ this . _popupIndex ( ) ] ; } }