import * as pathlib from "path" ; import { MainIpcHandlers , MainIpc_LifecycleHandlers , MainIpc_FileHandlers , MainIpc_ThemeHandlers , MainIpc_ShellHandlers , MainIpc_DialogHandlers , MainIpc_TagHandlers , MainIpc_OutlineHandlers , MainIpc_MetadataHandlers , MainIpc_NavigationHandlers } from "@main/MainIPC" ; import { RendererIpcEvents , RendererIpcHandlers } from "./RendererIPC" ; import { IPossiblyUntitledFile , IDirEntryMeta , IFileMeta } from "@common/files" ; import { invokerFor } from "@common/ipc" ; import { to } from "@common/util/to" ; import { IpcEvents } from "@common/events" ; import { MarkdownEditor } from "./editors/editor-markdown" ; import { Editor } from "./editors/editor" ; import { IFolderMarker , FileExplorer } from "./ui/explorer" ; import { TagSearch } from "./ui/tag-search" ; import { render } from "solid-js/web" ; import { State as SolidState , SetStateFunction , createState , Suspense , Switch , Match , For } from "solid-js" ; import { CalendarTab } from "./ui/calendarTab" ; import { OutlineTab } from "./ui/outlineTab" ; import { HistoryTab } from "./ui/historyTab" ; import { IOutline } from "@main/plugins/outline-plugin" ; import { ITagSearchResult , IFileSearchResult } from "@main/plugins/crossref-plugin" ; import { WindowAfterPreload } from "@renderer/preload_types" ; import { MouseButton } from "@common/inputEvents" ; declare let window ; const ipcRenderer = window . restrictedIpcRenderer ; export interface IRendererState { activeTab : number ; activeFile : null | IPossiblyUntitledFile ; fileTree : [ IFolderMarker , IDirEntryMeta [ ] ] [ ] ; navigationHistory : { history : IFileMeta [ ] ; currentIdx : number ; } ; themeCss : string ; } class Renderer { _mainProxy : MainIpcHandlers ; _eventHandlers : RendererIpcHandlers ; _ui : null | { titleElt : HTMLDivElement ; editorElt : HTMLDivElement ; } ; _react : null | { state : SolidState < IRendererState > ; setState : SetStateFunction < IRendererState > ; } ; _editor : Editor | null ; _currentFile : IPossiblyUntitledFile ; _fileTree : [ IFolderMarker , IDirEntryMeta [ ] ] [ ] ; constructor ( ) { const channel = "command" ; const logPrefix = "render->main" ; this . _mainProxy = { lifecycle : invokerFor < MainIpc_LifecycleHandlers > ( ipcRenderer , channel , logPrefix , "lifecycle" ) , file : invokerFor < MainIpc_FileHandlers > ( ipcRenderer , channel , logPrefix , "file" ) , theme : invokerFor < MainIpc_ThemeHandlers > ( ipcRenderer , channel , logPrefix , "theme" ) , shell : invokerFor < MainIpc_ShellHandlers > ( ipcRenderer , channel , logPrefix , "shell" ) , dialog : invokerFor < MainIpc_DialogHandlers > ( ipcRenderer , channel , logPrefix , "dialog" ) , tag : invokerFor < MainIpc_TagHandlers > ( ipcRenderer , channel , logPrefix , "tag" ) , outline : invokerFor < MainIpc_OutlineHandlers > ( ipcRenderer , channel , logPrefix , "outline" ) , metadata : invokerFor < MainIpc_MetadataHandlers > ( ipcRenderer , channel , logPrefix , "metadata" ) , navigation : invokerFor < MainIpc_NavigationHandlers > ( ipcRenderer , channel , logPrefix , "navigation" ) } ; this . _eventHandlers = new RendererIpcHandlers ( this ) ; todo ( window as any ) . renderer = this ; todo this . _currentFile = { type : "file" , contents : "" , modTime : - 1 , creationTime : - 1 } ; this . _fileTree = [ ] ; this . _editor = null ; this . _ui = null ; this . _react = null ; } init ( ) { console . log ( "render :: init()" ) ; ipcRenderer . receive ( IpcEvents . RENDERER_INVOKE , ( responseId , key , data ) => { console . log ( "render.on() :: RENDERER_INVOKE ::" , responseId , key , data ) ; this . handle ( key , data ) . then ( ( result ) => { ipcRenderer . send ( responseId , true , result ) ; } ) . catch ( ( reason ) => { ipcRenderer . send ( responseId , false , reason ) ; } ) ; } ) ; this . initUI ( ) ; this . initKeyboardEvents ( ) ; if ( this . _currentFile ) { this . setCurrentFile ( this . _currentFile ) ; } } initUI ( ) { const App = ( ) => { let [ state , setState ] = createState < IRendererState > ( { activeTab : 0 , activeFile : null , fileTree : [ ] , navigationHistory : { history : [ ] , currentIdx : 0 } , themeCss : "" } ) ; this . _react = { state , setState } ; const activeHash = ( ) => { if ( state . activeFile !== null && "hash" in state . activeFile ) { return state . activeFile . hash ; } else { return null ; } } ; const getOutline = async ( ) : Promise < IOutline | null > => { console . log ( "getOutline" ) ; let hash = activeHash ( ) ; if ( ! hash ) { return null ; } console . log ( "getOutline :: hash=" , hash ) ; let outline = await this . _mainProxy . outline . requestOutlineForHash ( hash ) ; console . log ( "outline found" , outline ) ; return outline ; } ; const getHistory = async ( ) => { return this . _mainProxy . navigation . getNavigationHistory ( ) ; } ; const Loading = ( ) => { return ( < div > loading ... < /div>); } ; const tabLabels = [ { title : "Workspace" , codicon : "codicon-folder-opened" } , { title : "Outline" , codicon : "codicon-book" } , { title : "Tags" , codicon : "codicon-symbol-numeric" } , { title : "Themes" , codicon : "codicon-symbol-color" } , { title : "Calendar" , codicon : "codicon-calendar" } , ] ; const handleFileClick = ( evt ) => { let target = evt . currentTarget as HTMLElement ; /** FIXME this is hacky, handle properly next time! */ if ( target . className == "folder" ) { let collapsed = target . getAttribute ( "data-collapsed" ) || "false" ; target . setAttribute ( "data-collapsed" , collapsed == "true" ? "false" : "true" ) ; return ; } let fileHash = target . getAttribute ( "data-filehash" ) ; if ( fileHash === null ) { return ; } console . log ( "explorer :: clicked" , fileHash ) ; this . _mainProxy . navigation . navigateToHash ( { hash : fileHash } ) ; } ; const handleTagClick = ( evt ) => { let target = evt . currentTarget as HTMLElement ; let tag = target . getAttribute ( "data-tag" ) ; if ( tag === null ) { return ; } this . _mainProxy . navigation . navigateToTag ( { tag , create : false } ) ; } ; const handleAppClicked = ( evt ) => { switch ( evt . button ) { case MouseButton . BACK : this . _mainProxy . navigation . navigatePrev ( ) ; break ; case MouseButton . FORWARD : this . _mainProxy . navigation . navigateNext ( ) ; break ; } } ; const handleHistoryClick = ( evt ) => { let target = evt . currentTarget as HTMLElement ; let historyIdxAttr = target . getAttribute ( "data-history-idx" ) ; if ( historyIdxAttr === null ) { return ; } let historyIdx = parseInt ( historyIdxAttr ) ; if ( isNaN ( historyIdx ) ) { return ; } this . _mainProxy . navigation . navigateToIndex ( historyIdx ) ; } ; const search = async ( query ) : Promise < ( ITagSearchResult | IFileSearchResult ) [ ] > => { console . log ( "searching..." , query ) ; let result = await this . _mainProxy . tag . fuzzyTagFileSearch ( query ) ; return result ; } ; const AppSidebar = ( ) => { return ( < div id = "sidebar" > { } < div class = "content" > < Suspense fallback = { < Loading />}> < Switch > < Match when = { state . activeTab == 0 } > < FileExplorer activeHash = { activeHash ( ) } fileTree = { state . fileTree } handleClick = { handleFileClick } / > < /Match> < Match when = { state . activeTab == 1 } > < OutlineTab getOutline = { getOutline } /> < /Match> < Match when = { state . activeTab == 2 } > < TagSearch getSearchResults = { search } handleTagClick = { handleTagClick } handleFileClick = { handleFileClick } / > < /Match> < Match when = { state . activeTab == 3 } > < HistoryTab navHistory = { state . navigationHistory } handleHistoryClick = { handleHistoryClick } / > < /Match> < Match when = { state . activeTab == 4 } > < CalendarTab /> < /Match> < /Switch> < /Suspense></div > { } < nav class = "tabs" > < For each = { tabLabels } > { ( tab , idx ) => { let active = ( ) => ( state . activeTab == idx ( ) ) ; return ( < a class = { `tab ${ active ( ) ? "active" : "" }` } title = { tab . title } onClick = { ( ) => setState ( { activeTab : idx ( ) } ) } > < span class = { `codicon ${ tab . codicon }` } > < /span> < /a>); } } < /For> < /nav> < /div>); } ; const AppContent = ( ) => { return ( < div id = "content" > < /div>); } ; const AppFooter = ( ) => { return ( < div id = "footer" > < div id = "title" > { state . activeFile ?. path || "(no file selected)" } < /div> < /div>); } ; return ( < > < style > { state . themeCss } < /style> < div id = "app" onMouseUp = { handleAppClicked } > < AppSidebar /> < AppContent / > < AppFooter /> < /div> < />); } ; let mainElt = document . getElementById ( "main" ) as HTMLElement ; render ( ( ) => < App />, mainElt); this . _mainProxy . theme . requestThemeRefresh ( ) ; let contentElt = document . getElementById ( "content" ) as HTMLElement ; let editorElt = document . createElement ( "div" ) ; editorElt . setAttribute ( "id" , "editor" ) ; editorElt . setAttribute ( "spellcheck" , "false" ) ; contentElt . appendChild ( editorElt ) ; this . _ui = { titleElt : document . getElementById ( "title" ) as HTMLDivElement , editorElt : editorElt } ; } initKeyboardEvents ( ) { todo const shiftHandler = ( evt ) => { if ( evt . ctrlKey ) { document . body . classList . add ( "user-ctrl" ) ; } else { document . body . classList . remove ( "user-ctrl" ) ; } } ; document . addEventListener ( "keydown" , shiftHandler ) ; document . addEventListener ( "keyup" , shiftHandler ) ; document . addEventListener ( "keypress" , shiftHandler ) ; } handle < T extends RendererIpcEvents > ( name , data ) { return this . _eventHandlers [ name ] ( data as any ) ; } applyThemeCss ( cssString ) { this . _react ?. setState ( { themeCss : cssString } ) ; } async setCurrentFile ( file ) { todo if ( this . _editor ) { todo let [ err , result ] = await to < string > ( this . _editor . closeAndDestroy ( ) ) ; if ( err == "Cancel" ) { return ; } else if ( err ) { return Promise . reject ( err ) ; } } console . log ( "render :: setCurrentFile" , file ) ; this . _currentFile = file ; if ( ! this . _ui ) { throw new Error ( "no user interface active!" ) ; } this . _react ?. setState ( { activeFile : file } ) ; let ext = pathlib . extname ( this . _currentFile . path || "" ) ; let editor ; const args = [ this . _currentFile , this . _ui . editorElt , this . _mainProxy ] as const ; switch ( ext ) { case ".md" : editor = new MarkdownEditor ( ... args ) ; break ; default : editor = new MarkdownEditor ( ... args ) ; break ; } this . _editor = editor ; this . _editor . init ( ) ; } setNavHistory ( navHistory ) { this . _react ?. setState ( { navigationHistory : navHistory } ) ; } setFileTree ( fileTree ) { console . log ( "render :: setFileTree" ) ; todo let sorted = fileTree . map ( entry => ( { entry , pathSplit : entry . path . split ( pathlib . sep ) } ) ) . sort ( ( a , b ) => { let na = a . pathSplit . length ; let nb = b . pathSplit . length ; for ( let i = 0 ; i < Math . max ( na , nb ) ; i ++ ) { if ( i >= na ) { return 1 ; } if ( i >= nb ) { return - 1 ; } if ( a . pathSplit [ i ] == b . pathSplit [ i ] ) continue ; let alast = ( i == na - 1 ) ; let blast = ( i == nb - 1 ) ; if ( alast && ! blast ) { return 1 ; } if ( ! alast && blast ) { return - 1 ; } return ( a . pathSplit [ i ] < b . pathSplit [ i ] ) ? - 1 : 1 ; } return ( a . entry . path < b . entry . path ) ? - 1 : 1 ; } ) ; fileTree = sorted . map ( val => val . entry ) ; let a1 = fileTree [ 0 ] . path ; let a2 = fileTree [ fileTree . length - 1 ] . path ; let i = 0 ; while ( i < a1 . length && a1 . charAt ( i ) === a2 . charAt ( i ) ) i ++ ; let prefix = a1 . substring ( 0 , i ) ; let directories = [ ] ; let folderMarker = null ; let fileList = [ ] ; let startIdx = 0 ; let prevDir = null ; for ( let idx = 0 ; idx < fileTree . length + 1 ; idx ++ ) { let lastIdx = ( idx === fileTree . length ) ; let dirPath = lastIdx ? ( prevDir || "" ) : pathlib . dirname ( fileTree [ idx ] . path ) ; if ( lastIdx || dirPath !== prevDir ) { if ( folderMarker && ( idx - startIdx > 0 ) ) { directories . push ( [ folderMarker , fileTree . slice ( startIdx , idx ) ] ) ; } folderMarker = { folderMarker : true , path : dirPath || "<root>" , pathSuffix : dirPath . substring ( prefix . length ) , name : pathlib . basename ( dirPath ) } ; prevDir = dirPath ; startIdx = idx ; } } this . _fileTree = directories ; this . _react ?. setState ( { fileTree : this . _fileTree } ) ; } } export default Renderer ;