Bläddra i källkod

Enhance/plugin APIs (#7555)

* feat: WIP native cli command support

* Add :shell/command-whitelist option

* Integrate cli to code block

* Add :code-block/command-whitelist option

* fix: size of icon

* improve(shell): cache user shell whitelist on application configures file

* improve(electron): promisify run cli command

* chore(libs): update version

* fix(plugin): incorrect payload of pdf highlights section hook

* improve(plugin): block renderer with specific block uuid

* improve(plugin): expose logger for user lib

* improve(plugin): block hooks type

* improve(plugin): block slot hook with specific block

* improve(plugin): auto generate key for provide UI options

* improve(plugin): style of injected ui container

* improve(plugin): types

* improve(plugin): async messaging api from host to plugin

* improve(plugin): add types

* improve(apis): get external plugin metadata

* improve(apis): invoke external plugin impls

* improve(apis): call external plugin impls for simple commands

* enhance(apis): datascript query api for predicate inputs

* enhance(apis): datascript query api for predicate inputs

* fix(apis): redundant args of datascript query api

* enhance(plugins): position of float ui container

* enhance(plugins): style of setting options

* enhance(plugins): layouts data for float ui

* chore(plugins): update CHANGELOG.md

* improve(apis): add types

* chore: fix some inclusive terms

* improve(apis): types

* chore(plugins): update CHANGELOG.md

* chore(plugins): build libs

* chore: update CHANGELOG.md

* chore: remove experiemental alda integration

* fix(lint): remove unused methods

Co-authored-by: Tienson Qin <[email protected]>
Co-authored-by: Andelf <[email protected]>
Charlie 2 år sedan
förälder
incheckning
020317911f

+ 20 - 1
libs/CHANGELOG.md

@@ -3,8 +3,27 @@
 All notable changes to this project will be documented in this file.
 
 ## [Unreleased]
+## [0.0.13]
 
-## [0.0.11]
+### Added
+- Support block content slot hook `App.onBlockRendererSlotted` with a specific block UUID
+- Support plugins calling each other `App.invokeExternalPlugin` with key of models & commands.  
+  E.g. (It is recommended that the caller plugin upgrade the SDK to the latest.)
+  ```typescript
+  // Defined at https://github.com/xyhp915/logseq-journals-calendar/blob/main/src/main.js#L74
+  await logseq.App.invokeExternalPlugin('logseq-journals-calendar.models.goToToday')
+  
+  // Defined at https://github.com/vipzhicheng/logseq-plugin-vim-shortcuts/blob/bec05aeee8/src/keybindings/down.ts#L20
+  await logseq.App.invokeExternalPlugin('logseq-plugin-vim-shortcuts.commands.vim-shortcut-down-0')
+  ```
+- Support api of `Editor.saveFocusedCodeEditorContent` [#FQ](https://github.com/logseq/logseq/issues/7714)
+- Support predicate for `DB.datascriptQuery` inputs
+
+### Fixed
+- Incorrect hook payload from `Editor.registerHighlightContextMenuItem`
+- Auto generate key if not exist for `provideUI` options
+
+## [0.0.12]
 
 ### Added
 

+ 1 - 1
libs/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@logseq/libs",
-  "version": "0.0.11",
+  "version": "0.0.13",
   "description": "Logseq SDK libraries",
   "main": "dist/lsplugin.user.js",
   "typings": "index.d.ts",

+ 27 - 10
libs/src/LSPlugin.caller.ts

@@ -38,7 +38,7 @@ class LSPluginCaller extends EventEmitter {
     payload: any,
     actor?: DeferredActor
   ) => Promise<any>
-  private _callUserModel?: (type: string, payload: any) => Promise<any>
+  private _callUserModel?: (type: string, ...payloads: any[]) => Promise<any>
 
   private _debugTag = ''
 
@@ -205,8 +205,13 @@ class LSPluginCaller extends EventEmitter {
     return this._call?.call(this, type, payload, actor)
   }
 
-  async callUserModel(type: string, payload: any = {}) {
-    return this._callUserModel?.call(this, type, payload)
+  async callUserModel(type: string, ...args: any[]) {
+    return this._callUserModel?.apply(this, [type, ...args])
+  }
+
+  async callUserModelAsync(type: string, ...args: any[]) {
+    type = AWAIT_LSPMSGFn(type)
+    return this._callUserModel?.apply(this, [type, ...args])
   }
 
   // run in host
@@ -235,12 +240,21 @@ class LSPluginCaller extends EventEmitter {
       const mainLayoutInfo = (await this._pluginLocal._loadLayoutsData())?.$$0
       if (mainLayoutInfo) {
         cnt.dataset.inited_layout = 'true'
-        const { width, height, left, top } = mainLayoutInfo
+        let { width, height, left, top, vw, vh } = mainLayoutInfo
+
+        left = Math.max(left, 0)
+        left = (typeof vw === 'number') ?
+          `${Math.min(left * 100 / vw, 99)}%` : `${left}px`
+
+        // 45 is height of headbar
+        top = Math.max(top, 45)
+        top = (typeof vh === 'number') ?
+          `${Math.min(top * 100 / vh, 99)}%` : `${top}px`
+
         Object.assign(cnt.style, {
           width: width + 'px',
           height: height + 'px',
-          left: left + 'px',
-          top: top + 'px',
+          left, top
         })
       }
     } catch (e) {
@@ -292,12 +306,15 @@ class LSPluginCaller extends EventEmitter {
             })
           }
 
-          this._callUserModel = async (type, payload: any) => {
+          this._callUserModel = async (type, ...payloads: any[]) => {
             if (type.startsWith(FLAG_AWAIT)) {
-              // TODO: attach payload with method call
-              return await refChild.get(type.replace(FLAG_AWAIT, ''))
+              // TODO: attach arguments with method call
+              return await refChild.get(
+                type.replace(FLAG_AWAIT, ''),
+                ...payloads
+              )
             } else {
-              refChild.call(type, payload)
+              refChild.call(type, payloads?.[0])
             }
           }
 

+ 20 - 57
libs/src/LSPlugin.core.ts

@@ -19,7 +19,7 @@ import {
   cleanInjectedScripts,
   safeSnakeCase,
   injectTheme,
-  cleanInjectedUI,
+  cleanInjectedUI, PluginLogger,
 } from './helpers'
 import * as pluginHelpers from './helpers'
 import Debug from 'debug'
@@ -132,59 +132,6 @@ class PluginSettings extends EventEmitter<'change' | 'reset'> {
   }
 }
 
-class PluginLogger extends EventEmitter<'change'> {
-  private _logs: Array<[type: string, payload: any]> = []
-
-  constructor(private readonly _tag: string) {
-    super()
-  }
-
-  write(type: string, payload: any[], inConsole?: boolean) {
-    if (payload?.length && (true === payload[payload.length - 1])) {
-      inConsole = true
-      payload.pop()
-    }
-
-    const msg = payload.reduce((ac, it) => {
-      if (it && it instanceof Error) {
-        ac += `${it.message} ${it.stack}`
-      } else {
-        ac += it.toString()
-      }
-      return ac
-    }, `[${this._tag}][${new Date().toLocaleTimeString()}] `)
-
-    this._logs.push([type, msg])
-
-    if (inConsole) {
-      console?.['ERROR' === type ? 'error' : 'debug'](`${type}: ${msg}`)
-    }
-
-    this.emit('change')
-  }
-
-  clear() {
-    this._logs = []
-    this.emit('change')
-  }
-
-  info(...args: any[]) {
-    this.write('INFO', args)
-  }
-
-  error(...args: any[]) {
-    this.write('ERROR', args)
-  }
-
-  warn(...args: any[]) {
-    this.write('WARN', args)
-  }
-
-  toJSON() {
-    return this._logs
-  }
-}
-
 interface UserPreferences {
   theme: LegacyTheme
   themes: {
@@ -204,7 +151,6 @@ interface PluginLocalOptions {
   mode: 'shadow' | 'iframe'
   settingsSchema?: SettingSchemaDesc[]
   settings?: PluginSettings
-  logger?: PluginLogger
   effect?: boolean
   theme?: boolean
 
@@ -460,6 +406,7 @@ class PluginLocal extends EventEmitter<'loaded'
   private _localRoot?: string
   private _dotSettingsFile?: string
   private _caller?: LSPluginCaller
+  private _logger?: PluginLogger
 
   /**
    * @param _options
@@ -483,7 +430,7 @@ class PluginLocal extends EventEmitter<'loaded'
 
   async _setupUserSettings(reload?: boolean) {
     const { _options } = this
-    const logger = (_options.logger = new PluginLogger('Loader'))
+    const logger = (this._logger = new PluginLogger('Loader'))
 
     if (_options.settings && !reload) {
       return
@@ -1056,13 +1003,17 @@ class PluginLocal extends EventEmitter<'loaded'
   }
 
   get logger() {
-    return this.options.logger
+    return this._logger
   }
 
   get disabled() {
     return this.settings?.get('disabled')
   }
 
+  get theme() {
+    return this.options.theme
+  }
+
   get caller() {
     return this._caller
   }
@@ -1123,6 +1074,8 @@ class PluginLocal extends EventEmitter<'loaded'
     json.usf = this.dotSettingsFile
     json.iir = this.isInstalledInDotRoot
     json.lsr = this._resolveResourceFullUrl('/')
+    json.settings = json.settings?.toJSON()
+
     return json
   }
 }
@@ -1535,6 +1488,16 @@ class LSPluginCore
     return this._registeredThemes
   }
 
+  get enabledPlugins() {
+    return [...this.registeredPlugins.entries()].reduce((a, b) => {
+      let p = b?.[1]
+      if (p?.disabled !== true) {
+        a.set(b?.[0], p)
+      }
+      return a
+    }, new Map())
+  }
+
   async registerTheme(id: PluginLocalIdentity, opt: Theme): Promise<void> {
     debug('Register theme #', id, opt)
 

+ 36 - 2
libs/src/LSPlugin.ts

@@ -102,6 +102,10 @@ export type IUserHook<E = any, R = IUserOffHook> = (
 export type IUserSlotHook<E = any> = (
   callback: (e: IHookEvent & UISlotIdentity & E) => void
 ) => void
+export type IUserConditionSlotHook<C = any, E = any> = (
+  condition: C,
+  callback: (e: IHookEvent & UISlotIdentity & E) => void
+) => void
 
 export type EntityID = number
 export type BlockUUID = string
@@ -366,11 +370,33 @@ export interface IAppProxy {
     action: SimpleCommandCallback
   ) => void
 
+  /**
+   * Supported all registered palette commands
+   * @param type
+   * @param args
+   */
   invokeExternalCommand: (
     type: ExternalCommandType,
     ...args: Array<any>
   ) => Promise<void>
 
+  /**
+   * Call external plugin command provided by models or registerd commands
+   * @added 0.0.13
+   * @param type `xx-plugin-id.commands.xx-key`, `xx-plugin-id.models.xx-key`
+   * @param args
+   */
+  invokeExternalPlugin: (
+    type: string,
+    ...args: Array<any>
+  ) => Promise<unknown>
+
+  /**
+   * @added 0.0.13
+   * @param pid
+   */
+  getExternalPlugin: (pid: string) => Promise<{} | null>
+
   /**
    * Get state from app store
    * valid state is here
@@ -455,7 +481,13 @@ export interface IAppProxy {
   onGraphAfterIndexed: IUserHook<{ repo: string }>
   onThemeModeChanged: IUserHook<{ mode: 'dark' | 'light' }>
   onThemeChanged: IUserHook<Partial<{ name: string, mode: string, pid: string, url: string }>>
-  onBlockRendererSlotted: IUserSlotHook<{ uuid: BlockUUID }>
+
+  /**
+   * provide ui slot to specific block with UUID
+   *
+   * @added 0.0.13
+   */
+  onBlockRendererSlotted: IUserConditionSlotHook<BlockUUID, Omit<BlockEntity, 'children' | 'page'>>
 
   /**
    * provide ui slot to block `renderer` macro for `{{renderer arg1, arg2}}`
@@ -636,7 +668,7 @@ export interface IEditorProxy extends Record<string, any> {
 
   /**
    * @example https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-reddit-hot-news
-   * 
+   *
    * `keepUUID` will allow you to set a custom UUID for blocks by setting their properties.id
    */
   insertBatchBlock: (
@@ -722,6 +754,8 @@ export interface IEditorProxy extends Record<string, any> {
   editBlock: (srcBlock: BlockIdentity, opts?: { pos: number }) => Promise<void>
   selectBlock: (srcBlock: BlockIdentity) => Promise<void>
 
+  saveFocusedCodeEditorContent: () => Promise<void>
+
   upsertBlockProperty: (
     block: BlockIdentity,
     key: string,

+ 91 - 5
libs/src/LSPlugin.user.ts

@@ -1,6 +1,8 @@
 import {
+  isValidUUID,
   deepMerge,
   mergeSettingsWithSchema,
+  PluginLogger,
   safeSnakeCase,
   safetyPathJoin,
 } from './helpers'
@@ -55,6 +57,7 @@ type callableMethods =
 
 const PROXY_CONTINUE = Symbol.for('proxy-continue')
 const debug = Debug('LSPlugin:user')
+const logger = new PluginLogger('', { console: true })
 
 /**
  * @param type (key of group commands)
@@ -87,12 +90,26 @@ function registerSimpleCommand(
     method: 'register-plugin-simple-command',
     args: [
       this.baseInfo.id,
+      // [cmd, action]
       [{ key, label, type, desc, keybinding, extras }, ['editor/hook', eventKey]],
       palette,
     ],
   })
 }
 
+function shouldValidUUID(uuid: string) {
+  if (!isValidUUID(uuid)) {
+    logger.error(`#${uuid} is not a valid UUID string.`)
+    return false
+  }
+
+  return true
+}
+
+function checkEffect(p: LSPluginUser) {
+  return p && (p.baseInfo?.effect || !p.baseInfo?.iir)
+}
+
 let _appBaseInfo: AppInfo = null
 let _searchServices: Map<string, LSPluginSearchService> = new Map()
 
@@ -188,6 +205,45 @@ const app: Partial<IAppProxy> = {
     )
   },
 
+  onBlockRendererSlotted(
+    uuid,
+    callback: (payload: any) => void) {
+    if (!shouldValidUUID(uuid)) return
+
+    const pid = this.baseInfo.id
+    const hook = `hook:editor:${safeSnakeCase(`slot:${uuid}`)}`
+
+    this.caller.on(hook, callback)
+    this.App._installPluginHook(pid, hook)
+
+    return () => {
+      this.caller.off(hook, callback)
+      this.App._uninstallPluginHook(pid, hook)
+    }
+  },
+
+  invokeExternalPlugin(
+    this: LSPluginUser,
+    type: string,
+    ...args: Array<any>
+  ) {
+    type = type?.trim()
+    if (!type) return
+    let [pid, group] = type.split('.')
+    if (!['models', 'commands'].includes(group?.toLowerCase())) {
+      throw new Error(`Type only support '.models' or '.commands' currently.`)
+    }
+    const key = type.replace(`${pid}.${group}.`, '')
+
+    if (!pid || !group || !key) {
+      throw new Error(`Illegal type of #${type} to invoke external plugin.`)
+    }
+    return this._execCallableAPIAsync(
+      'invoke_external_plugin_cmd',
+      pid, group.toLowerCase(), key, args
+    )
+  },
+
   setFullScreen(flag) {
     const sf = (...args) => this._callWin('setFullScreen', ...args)
 
@@ -328,6 +384,8 @@ const db: Partial<IDBProxy> = {
       txMeta?: { outlinerOp: string; [p: string]: any }
     ) => void
   ): IUserOffHook {
+    if (!shouldValidUUID(uuid)) return
+
     const pid = this.baseInfo.id
     const hook = `hook:db:${safeSnakeCase(`block:${uuid}`)}`
     const aBlockChange = ({ block, txData, txMeta }) => {
@@ -346,6 +404,24 @@ const db: Partial<IDBProxy> = {
       this.App._uninstallPluginHook(pid, hook)
     }
   },
+
+  datascriptQuery<T = any>(
+    this: LSPluginUser,
+    query: string,
+    ...inputs: Array<any>
+  ): Promise<T> {
+    inputs.pop()
+
+    if (inputs?.some(it => (typeof it === 'function'))) {
+      const host = this.Experiments.ensureHostScope()
+      return host.logseq.api.datascript_query(query, ...inputs)
+    }
+
+    return this._execCallableAPIAsync(
+      `datascript_query`,
+      ...inputs
+    )
+  }
 }
 
 const git: Partial<IGitProxy> = {}
@@ -454,6 +530,13 @@ export class LSPluginUser
 
       baseInfo = deepMerge(this._baseInfo, baseInfo)
 
+      if (baseInfo?.id) {
+        this._debugTag =
+          this._caller.debugTag = `#${baseInfo.id} [${baseInfo.name}]`
+
+        this.logger.setTag(this._debugTag)
+      }
+
       if (this._settingsSchema) {
         baseInfo.settings = mergeSettingsWithSchema(
           baseInfo.settings,
@@ -464,11 +547,6 @@ export class LSPluginUser
         await this.useSettingsSchema(this._settingsSchema)
       }
 
-      if (baseInfo?.id) {
-        this._debugTag =
-          this._caller.debugTag = `#${baseInfo.id} [${baseInfo.name}]`
-      }
-
       try {
         await this._execCallableAPIAsync('setSDKMetadata', {
           version: this._version,
@@ -605,6 +683,14 @@ export class LSPluginUser
     return this._baseInfo
   }
 
+  get effect(): Boolean {
+    return checkEffect(this)
+  }
+
+  get logger() {
+    return logger
+  }
+
   get settings() {
     return this.baseInfo?.settings
   }

+ 99 - 29
libs/src/helpers.ts

@@ -5,6 +5,7 @@ import DOMPurify from 'dompurify'
 import { merge } from 'lodash-es'
 import { snakeCase } from 'snake-case'
 import * as callables from './callable.apis'
+import EventEmitter from 'eventemitter3'
 
 declare global {
   interface Window {
@@ -53,6 +54,74 @@ export function isObject(item: any) {
 
 export const deepMerge = merge
 
+export class PluginLogger extends EventEmitter<'change'> {
+  private _logs: Array<[type: string, payload: any]> = []
+
+  constructor(
+    private _tag?: string,
+    private _opts?: {
+      console: boolean
+    }
+  ) {
+    super()
+  }
+
+  write(type: string, payload: any[], inConsole?: boolean) {
+    if (payload?.length && (true === payload[payload.length - 1])) {
+      inConsole = true
+      payload.pop()
+    }
+
+    const msg = payload.reduce((ac, it) => {
+      if (it && it instanceof Error) {
+        ac += `${it.message} ${it.stack}`
+      } else {
+        ac += it.toString()
+      }
+      return ac
+    }, `[${this._tag}][${new Date().toLocaleTimeString()}] `)
+
+    this._logs.push([type, msg])
+
+    if (inConsole || this._opts?.console) {
+      console?.['ERROR' === type ? 'error' : 'debug'](`${type}: ${msg}`)
+    }
+
+    this.emit('change')
+  }
+
+  clear() {
+    this._logs = []
+    this.emit('change')
+  }
+
+  info(...args: any[]) {
+    this.write('INFO', args)
+  }
+
+  error(...args: any[]) {
+    this.write('ERROR', args)
+  }
+
+  warn(...args: any[]) {
+    this.write('WARN', args)
+  }
+
+  setTag(s: string) {
+    this._tag = s
+  }
+
+  toJSON() {
+    return this._logs
+  }
+}
+
+export function isValidUUID(s: string) {
+  return (typeof s === 'string' &&
+    (s.length === 36) &&
+    (/^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/gi).test(s))
+}
+
 export function genID() {
   // Math.random should be unique because of its seeding algorithm.
   // Convert it to base 36 (numbers + letters), and grab the first 9 characters
@@ -190,9 +259,9 @@ export function setupInjectedStyle(
   el.textContent = style
 
   attrs &&
-    Object.entries(attrs).forEach(([k, v]) => {
-      el.setAttribute(k, v)
-    })
+  Object.entries(attrs).forEach(([k, v]) => {
+    el.setAttribute(k, v)
+  })
 
   document.head.append(el)
 
@@ -227,7 +296,7 @@ export function setupInjectedUI(
     float = true
   }
 
-  const id = `${pl.id}--${ui.key}`
+  const id = `${pl.id}--${ui.key || genID()}`
   const key = id
 
   const target = float
@@ -268,22 +337,22 @@ export function setupInjectedUI(
 
     // update attributes
     attrs &&
-      Object.entries(attrs).forEach(([k, v]) => {
-        el.setAttribute(k, v)
-      })
+    Object.entries(attrs).forEach(([k, v]) => {
+      el.setAttribute(k, v)
+    })
 
     let positionDirty = el.dataset.dx != null
     ui.style &&
-      Object.entries(ui.style).forEach(([k, v]) => {
-        if (
-          positionDirty &&
-          ['left', 'top', 'bottom', 'right', 'width', 'height'].includes(k)
-        ) {
-          return
-        }
-
-        el.style[k] = v
-      })
+    Object.entries(ui.style).forEach(([k, v]) => {
+      if (
+        positionDirty &&
+        ['left', 'top', 'bottom', 'right', 'width', 'height'].includes(k)
+      ) {
+        return
+      }
+
+      el.style[k] = v
+    })
     return
   }
 
@@ -303,18 +372,19 @@ export function setupInjectedUI(
   content.innerHTML = ui.template
 
   attrs &&
-    Object.entries(attrs).forEach(([k, v]) => {
-      el.setAttribute(k, v)
-    })
+  Object.entries(attrs).forEach(([k, v]) => {
+    el.setAttribute(k, v)
+  })
 
   ui.style &&
-    Object.entries(ui.style).forEach(([k, v]) => {
-      el.style[k] = v
-    })
+  Object.entries(ui.style).forEach(([k, v]) => {
+    el.style[k] = v
+  })
 
   let teardownUI: () => void
   let disposeFloat: () => void
 
+  // seu up float container
   if (float) {
     el.setAttribute('draggable', 'true')
     el.setAttribute('resizable', 'true')
@@ -322,11 +392,11 @@ export function setupInjectedUI(
     el.classList.add('lsp-ui-float-container', 'visible')
     disposeFloat =
       (pl._setupResizableContainer(el, key),
-      pl._setupDraggableContainer(el, {
-        key,
-        close: () => teardownUI(),
-        title: attrs?.title,
-      }))
+        pl._setupDraggableContainer(el, {
+          key,
+          close: () => teardownUI(),
+          title: attrs?.title,
+        }))
   }
 
   if (!!slot && ui.reset) {
@@ -364,7 +434,7 @@ export function setupInjectedUI(
 
         const msgType = trigger.dataset[`on${ucFirst(type)}`]
         msgType &&
-          pl.caller?.callUserModel(msgType, transformableEvent(trigger, e))
+        pl.caller?.callUserModel(msgType, transformableEvent(trigger, e))
       },
       false
     )

+ 6 - 6
libs/src/postmate/index.ts

@@ -79,9 +79,9 @@ export const sanitize = (message, allowedOrigin) => {
  *                            passed to functions in the child model
  * @return {Promise}
  */
-export const resolveValue = (model, property) => {
+export const resolveValue = (model, property, args) => {
   const unwrappedContext =
-    typeof model[property] === 'function' ? model[property]() : model[property]
+    typeof model[property] === 'function' ? model[property].apply(null, args) : model[property]
   return Promise.resolve(unwrappedContext)
 }
 
@@ -134,7 +134,7 @@ export class ParentAPI {
     }
   }
 
-  get(property) {
+  get(property, ...args) {
     return new Promise((resolve) => {
       // Extract data from response and kill listeners
       const uid = generateNewMessageId()
@@ -154,6 +154,7 @@ export class ParentAPI {
           postmate: 'request',
           type: messageType,
           property,
+          args,
           uid,
         },
         this.childOrigin
@@ -218,7 +219,7 @@ export class ChildAPI {
         log('Child: Received request', e.data)
       }
 
-      const { property, uid, data } = e.data
+      const { property, uid, data, args } = e.data
 
       if (e.data.postmate === 'call') {
         if (
@@ -231,7 +232,7 @@ export class ChildAPI {
       }
 
       // Reply to Parent
-      resolveValue(this.model, property).then((value) => {
+      resolveValue(this.model, property, args).then((value) => {
         ;(e.source as WindowProxy).postMessage(
           {
             property,
@@ -375,7 +376,6 @@ export class Postmate {
     })
   }
 
-
   destroy() {
     if (process.env.NODE_ENV !== 'production') {
       log('Postmate: Destroying Postmate instance')

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
resources/js/lsplugin.core.js


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 0
resources/js/lsplugin.user.js


+ 2 - 1
resources/package.json

@@ -39,7 +39,8 @@
     "posthog-js": "1.10.2",
     "@logseq/rsapi": "0.0.57",
     "electron-deeplink": "1.0.10",
-    "abort-controller": "3.0.0"
+    "abort-controller": "3.0.0",
+    "command-exists": "1.2.9"
   },
   "devDependencies": {
     "@electron-forge/cli": "^6.0.0-beta.57",

+ 4 - 5
src/electron/electron/configs.cljs

@@ -17,8 +17,7 @@
     (let [body (.toString (.readFileSync fs cfg-path))]
       (if (seq body) (reader/read-string body) {}))
     (catch :default e
-      (js/console.error :cfg-error e)
-      {})))
+      (js/console.error :cfg-error e))))
 
 (defn- write-cfg!
   [cfg]
@@ -29,9 +28,9 @@
 
 (defn set-item!
   [k v]
-  (let [cfg (ensure-cfg)
-        cfg (assoc cfg k v)]
-    (write-cfg! cfg)))
+  (when-let [cfg (ensure-cfg)]
+    (some->> (assoc cfg k v)
+             (write-cfg!))))
 
 (defn get-item
   [k]

+ 24 - 4
src/electron/electron/handler.cljs

@@ -11,6 +11,7 @@
             ["diff-match-patch" :as google-diff]
             ["/electron/utils" :as js-utils]
             ["abort-controller" :as AbortController]
+            [electron.shell :as shell]
             [electron.fs-watcher :as watcher]
             [electron.configs :as cfgs]
             [promesa.core :as p]
@@ -410,11 +411,12 @@
 
 (defmethod handle :userAppCfgs [_window [_ k v]]
   (let [config (cfgs/get-config)]
-    (if-not k
-      config
+    (if-let [k (and k (keyword k))]
       (if-not (nil? v)
-        (cfgs/set-item! (keyword k) v)
-        (cfgs/get-item (keyword k))))))
+        (do (cfgs/set-item! k v)
+            (state/set-state! [:config k] v))
+        (cfgs/get-item k))
+     config)))
 
 (defmethod handle :getDirname [_]
   js/__dirname)
@@ -457,6 +459,24 @@
     (git/init!)
     (git/run-git2! (clj->js args))))
 
+(defmethod handle :runCli [window [_ {:keys [command args returnResult]}]]
+  (try
+    (let [on-data-handler (fn [message]
+                            (let [result (str "Running " command ": " message)]
+                              (when returnResult
+                                (utils/send-to-renderer window "notification"
+                                                        {:type    "success"
+                                                         :payload result}))))
+          deferred        (p/deferred)
+          on-exit-handler (fn [code]
+                            (p/resolve! deferred code))
+          _job            (shell/run-command-safety! command args on-data-handler on-exit-handler)]
+      deferred)
+    (catch js/Error e
+      (utils/send-to-renderer window "notification"
+                              {:type    "error"
+                               :payload (.-message e)}))))
+
 (defmethod handle :gitCommitAll [_ [_ message]]
   (git/add-all-and-commit! message))
 

+ 53 - 0
src/electron/electron/shell.cljs

@@ -0,0 +1,53 @@
+(ns electron.shell
+  (:require
+   [clojure.string :as string]
+   [electron.state :as state]
+   [clojure.set :as set]
+   [electron.logger :as logger]
+   ["child_process" :as child-process]
+   ["command-exists" :as command-exists]))
+
+(def commands-allowlist
+  #{"git" "pandoc" "ag" "grep" "alda"})
+
+;(def commands-denylist
+;  #{"rm" "mv" "rename" "dd" ">" "command" "sudo"})
+
+(defn- get-commands-allowlist
+  []
+  (set/union (set (some->> (map #(some-> % str string/trim string/lower-case)
+                                (get-in @state/state [:config :commands-allowlist]))
+                           (remove nil?)))
+             commands-allowlist))
+
+(defn- run-command!
+  [command args on-data on-exit]
+  (logger/debug "Shell: " (str command " " args))
+  (let [job (child-process/spawn (str command " " args)
+                                 #js []
+                                 #js {:shell true :detached false})]
+
+    (.on (.-stderr job) "data" on-data)
+    (.on (.-stdout job) "data" on-data)
+    (.on job "close" on-exit)
+
+    job))
+
+(defn- ensure-command-exists
+  [command]
+  (when-not
+   (some->> command (.sync command-exists))
+    (throw (js/Error. (str "Shell: " command " not exist!")))) command)
+
+(defn- ensure-command-in-allowlist
+  [command]
+  (when-not
+   (some->> command (contains? (get-commands-allowlist)))
+    (throw (js/Error. (str "Shell: " command " not be allowed!")))) command)
+
+(defn run-command-safety!
+  [command args on-data on-exit]
+  (when (some-> command str string/trim string/lower-case
+                (ensure-command-exists)
+                (ensure-command-in-allowlist))
+    (run-command! command args on-data on-exit)))

+ 1 - 1
src/electron/electron/utils.cljs

@@ -3,10 +3,10 @@
             ["electron" :refer [app BrowserWindow]]
             ["fs-extra" :as fs]
             ["path" :as path]
-            [cljs-bean.core :as bean]
             [clojure.string :as string]
             [electron.configs :as cfgs]
             [electron.logger :as logger]
+            [cljs-bean.core :as bean]
             [promesa.core :as p]))
 
 (defonce *win (atom nil)) ;; The main window

+ 1 - 1
src/main/frontend/commands.cljs

@@ -294,7 +294,7 @@
     ;; Allow user to modify or extend, should specify how to extend.
 
     (state/get-commands)
-    (state/get-plugins-commands))
+    (state/get-plugins-slash-commands))
    (remove nil?)
    (util/distinct-by-last-wins first)))
 

+ 28 - 19
src/main/frontend/components/block.cljs

@@ -2236,6 +2236,7 @@
   (let [{:block/keys [title body] :as block} (if (:block/title block) block
                                                  (merge block (block/parse-title-and-body uuid format pre-block? content)))
         collapsed? (util/collapsed? block)
+        plugin-slotted? (and config/lsp-enabled? (state/slot-hook-exist? uuid))
         block-ref? (:block-ref? config)
         stop-events? (:stop-events? config)
         block-ref-with-title? (and block-ref? (seq title))
@@ -2274,13 +2275,14 @@
         [:div.warning.text-sm
          "Large block will not be editable or searchable to not slow down the app, please use another editor to edit this block."])
       [:div.flex.flex-row.justify-between.block-content-inner
-       [:div.flex-1.w-full
-        (cond
-          (seq title)
-          (build-block-title config block)
+       (when-not plugin-slotted?
+         [:div.flex-1.w-full
+          (cond
+            (seq title)
+            (build-block-title config block)
 
-          :else
-          nil)]
+            :else
+            nil)])
 
        (clock-summary-cp block body)]
 
@@ -2306,18 +2308,24 @@
                  (not= block-type :whiteboard-shape))
         (properties-cp config block))
 
-      (let [title-collapse-enabled? (:outliner/block-title-collapse-enabled? (state/get-config))]
-        (when (and (not block-ref-with-title?)
-                   (seq body)
-                   (or (not title-collapse-enabled?)
-                       (and title-collapse-enabled? (not collapsed?))))
-         [:div.block-body
-          ;; TODO: consistent id instead of the idx (since it could be changed later)
-          (let [body (block/trim-break-lines! (:block/body block))]
-            (for [[idx child] (medley/indexed body)]
-              (when-let [block (markup-element-cp config child)]
-                (rum/with-key (block-child block)
-                  (str uuid "-" idx)))))]))
+      (if plugin-slotted?
+        [:div.block-slotted-body
+         (plugins/hook-block-slot
+          :block-content-slotted
+          (-> block (dissoc :block/children :block/page)))]
+
+        (let [title-collapse-enabled? (:outliner/block-title-collapse-enabled? (state/get-config))]
+          (when (and (not block-ref-with-title?)
+                     (seq body)
+                     (or (not title-collapse-enabled?)
+                         (and title-collapse-enabled? (not collapsed?))))
+            [:div.block-body
+             ;; TODO: consistent id instead of the idx (since it could be changed later)
+             (let [body (block/trim-break-lines! (:block/body block))]
+               (for [[idx child] (medley/indexed body)]
+                 (when-let [block (markup-element-cp config child)]
+                   (rum/with-key (block-child block)
+                                 (str uuid "-" idx)))))])))
 
       (case (:block/warning block)
         :multiple-blocks
@@ -2378,7 +2386,8 @@
                                string/trim
                                block-ref/block-ref?)]
     (if (and edit? editor-box)
-      [:div.editor-wrapper {:id editor-id}
+      [:div.editor-wrapper
+       {:id editor-id}
        (ui/catch-error
         (ui/block-error "Something wrong in the editor" {})
         (editor-box {:block block

+ 7 - 5
src/main/frontend/components/plugins.cljs

@@ -934,17 +934,15 @@
   (state/set-sub-modal! installed-themes))
 
 (rum/defc hook-ui-slot
-  ([type payload] (hook-ui-slot type payload nil))
-  ([type payload opts]
+  ([type payload] (hook-ui-slot type payload nil #(plugin-handler/hook-plugin-app type % nil)))
+  ([type payload opts callback]
    (let [rs      (util/rand-str 8)
          id      (str "slot__" rs)
          *el-ref (rum/use-ref nil)]
 
      (rum/use-effect!
       (fn []
-        (let [timer (js/setTimeout
-                     #(plugin-handler/hook-plugin-app type {:slot id :payload payload} nil)
-                     100)]
+        (let [timer (js/setTimeout #(callback {:type type :slot id :payload payload}) 50)]
           #(js/clearTimeout timer)))
       [id])
 
@@ -962,6 +960,10 @@
                    :ref           *el-ref
                    :on-mouse-down (fn [e] (util/stop e))})])))
 
+(rum/defc hook-block-slot < rum/static
+  [type block]
+  (hook-ui-slot type {} nil #(plugin-handler/hook-plugin-block-slot block %)))
+
 (rum/defc ui-item-renderer
   [pid type {:keys [key template prefix]}]
   (let [*el    (rum/use-ref nil)

+ 14 - 0
src/main/frontend/components/plugins.css

@@ -559,10 +559,13 @@
           display: flex;
           align-items: center;
           padding-top: 3px;
+          flex-wrap: wrap;
 
           label {
             padding-right: 15px;
             user-select: none;
+            white-space: nowrap;
+            margin-top: 2px;
           }
 
           input {
@@ -895,6 +898,17 @@ html[data-theme='dark'] {
       display: inline-flex;
     }
   }
+
+  .block-slotted-body {
+    .lsp-hook-ui-slot {
+      display: block;
+
+      [data-injected-ui] {
+        display: flex;
+        flex-direction: column;
+      }
+    }
+  }
 }
 
 .ui__modal[label=plugins-dashboard] {

+ 1 - 1
src/main/frontend/components/shell.cljs

@@ -32,7 +32,7 @@
        [:div
         [:h1.title
          "Input command"]
-        [:div.mt-4.mb-4.relative.rounded-md.shadow-sm.max-w-xs
+        [:div.mt-4.mb-4.relative.rounded-md.shadow-sm
          [:input#run-command.form-input.font-mono.block.w-full.sm:text-sm.sm:leading-5
           {:autoFocus true
            :on-key-down util/stop-propagation

+ 44 - 42
src/main/frontend/extensions/pdf/highlights.cljs

@@ -115,46 +115,48 @@
         id          (:id highlight)
         new?        (nil? id)
         content     (:content highlight)
-        text?       (string/blank? (:image content))
+        area?       (not (string/blank? (:image content)))
         action-fn!  (fn [action clear?]
                       (when-let [action (and action (name action))]
-                        (case action
-                          "ref"
-                          (pdf-assets/copy-hl-ref! highlight)
-
-                          "copy"
-                          (do
-                            (util/copy-to-clipboard!
-                             (or (:text content) (pdf-utils/fix-selection-text-breakline (.toString selection))))
-                            (pdf-utils/clear-all-selection))
-
-                          "link"
-                          (pdf-assets/goto-block-ref! highlight)
-
-                          "del"
-                          (do
-                            (del-hl! highlight)
-                            (pdf-assets/del-ref-block! highlight)
-                            (pdf-assets/unlink-hl-area-image$ viewer (:pdf/current @state/state) highlight))
-
-                          "hook"
-                          :dune
-
-                          ;; colors
-                          (let [properties {:color action}]
-                            (if-not id
-                              ;; add highlight
-                              (let [highlight (merge (if (fn? highlight) (highlight) highlight)
-                                                     {:id         (pdf-utils/gen-uuid)
-                                                      :properties properties})]
-                                (add-hl! highlight)
-                                (pdf-utils/clear-all-selection)
-                                (pdf-assets/copy-hl-ref! highlight))
-
-                              ;; update highlight
-                              (upd-hl! (assoc highlight :properties properties)))
-
-                            (reset! *highlight-last-color (keyword action))))
+                        (let [highlight (if (fn? highlight) (highlight) highlight)
+                              content (:content highlight)]
+                          (case action
+                            "ref"
+                            (pdf-assets/copy-hl-ref! highlight)
+
+                            "copy"
+                            (do
+                              (util/copy-to-clipboard!
+                               (or (:text content) (pdf-utils/fix-selection-text-breakline (.toString selection))))
+                              (pdf-utils/clear-all-selection))
+
+                            "link"
+                            (pdf-assets/goto-block-ref! highlight)
+
+                            "del"
+                            (do
+                              (del-hl! highlight)
+                              (pdf-assets/del-ref-block! highlight)
+                              (pdf-assets/unlink-hl-area-image$ viewer (:pdf/current @state/state) highlight))
+
+                            "hook"
+                            :dune
+
+                            ;; colors
+                            (let [properties {:color action}]
+                              (if-not id
+                                ;; add highlight
+                                (let [highlight (merge highlight
+                                                       {:id         (pdf-utils/gen-uuid)
+                                                        :properties properties})]
+                                  (add-hl! highlight)
+                                  (pdf-utils/clear-all-selection)
+                                  (pdf-assets/copy-hl-ref! highlight))
+
+                                ;; update highlight
+                                (upd-hl! (assoc highlight :properties properties)))
+
+                              (reset! *highlight-last-color (keyword action)))))
 
                         (and clear? (js/setTimeout #(clear-ctx-tip!) 68))))]
 
@@ -185,20 +187,20 @@
 
      (and id [:li.item {:data-action "ref"} (t :pdf/copy-ref)])
 
-     (and (not (:image content)) [:li.item {:data-action "copy"} (t :pdf/copy-text)])
+     (and (not area?) [:li.item {:data-action "copy"} (t :pdf/copy-text)])
 
      (and id [:li.item {:data-action "link"} (t :pdf/linked-ref)])
 
      (and id [:li.item {:data-action "del"} (t :delete)])
 
-     (when (and config/lsp-enabled? text?)
+     (when (and config/lsp-enabled? (not area?))
        (for [[_ {:keys [key label extras] :as _cmd} action pid]
              (state/get-plugins-commands-with-type :highlight-context-menu-item)]
          [:li.item {:key         key
                     :data-action "hook"
-                    :on-click    #(do
+                    :on-click    #(let [highlight (if (fn? highlight) (highlight) highlight)]
                                     (commands/exec-plugin-simple-command!
-                                     pid {:key key :content content :point point} action)
+                                     pid {:key key :content (:content highlight) :point point} action)
 
                                     (when (true? (:clearSelection extras))
                                       (pdf-utils/clear-all-selection)))}

+ 5 - 0
src/main/frontend/handler/events.cljs

@@ -40,6 +40,7 @@
             [frontend.handler.search :as search-handler]
             [frontend.handler.ui :as ui-handler]
             [frontend.handler.user :as user-handler]
+            [frontend.handler.shell :as shell-handler]
             [frontend.handler.web.nfs :as nfs-handler]
             [frontend.mobile.core :as mobile]
             [frontend.mobile.util :as mobile-util]
@@ -918,6 +919,10 @@
                        [:p "Don't forget to re-index your graph when all the conflicts are resolved."]]
                       :status :error}]))
 
+(defmethod handle :run/cli-command [[_ command content]]
+  (when (and command (not (string/blank? content)))
+    (shell-handler/run-cli-command-wrapper! command content)))
+
 (defn run!
   []
   (let [chan (state/get-events-chan)]

+ 102 - 83
src/main/frontend/handler/plugin.cljs

@@ -24,11 +24,11 @@
   [input]
   (when input
     (walk/postwalk
-      (fn [a]
-        (cond
-          (keyword? a) (csk/->camelCase (name a))
-          (uuid? a) (str a)
-          :else a)) input)))
+     (fn [a]
+       (cond
+         (keyword? a) (csk/->camelCase (name a))
+         (uuid? a) (str a)
+         :else a)) input)))
 
 (defn invoke-exported-api
   [type & args]
@@ -71,42 +71,42 @@
   [refresh?]
   (if (or refresh? (nil? (:plugin/marketplace-pkgs @state/state)))
     (p/create
-      (fn [resolve reject]
-        (let [on-ok (fn [res]
-                      (if-let [res (and res (bean/->clj res))]
-                        (let [pkgs (:packages res)]
-                          (state/set-state! :plugin/marketplace-pkgs pkgs)
-                          (resolve pkgs))
-                        (reject nil)))]
-          (if (state/http-proxy-enabled-or-val?)
-            (-> (ipc/ipc :httpFetchJSON plugins-url)
-                (p/then on-ok)
-                (p/catch reject))
-            (util/fetch plugins-url on-ok reject)))))
+     (fn [resolve reject]
+       (let [on-ok (fn [res]
+                     (if-let [res (and res (bean/->clj res))]
+                       (let [pkgs (:packages res)]
+                         (state/set-state! :plugin/marketplace-pkgs pkgs)
+                         (resolve pkgs))
+                       (reject nil)))]
+         (if (state/http-proxy-enabled-or-val?)
+           (-> (ipc/ipc :httpFetchJSON plugins-url)
+               (p/then on-ok)
+               (p/catch reject))
+           (util/fetch plugins-url on-ok reject)))))
     (p/resolved (:plugin/marketplace-pkgs @state/state))))
 
 (defn load-marketplace-stats
   [refresh?]
   (if (or refresh? (nil? (:plugin/marketplace-stats @state/state)))
     (p/create
-      (fn [resolve reject]
-        (let [on-ok (fn [^js res]
-                      (if-let [res (and res (bean/->clj res))]
-                        (do
-                          (state/set-state!
-                           :plugin/marketplace-stats
-                           (into {} (map (fn [[k stat]]
-                                           [k (assoc stat
-                                                     :total_downloads
-                                                     (reduce (fn [a b] (+ a (get b 2))) 0 (:releases stat)))])
-                                         res)))
-                          (resolve nil))
-                        (reject nil)))]
-          (if (state/http-proxy-enabled-or-val?)
-            (-> (ipc/ipc :httpFetchJSON stats-url)
-                (p/then on-ok)
-                (p/catch reject))
-            (util/fetch stats-url on-ok reject)))))
+     (fn [resolve reject]
+       (let [on-ok (fn [^js res]
+                     (if-let [res (and res (bean/->clj res))]
+                       (do
+                         (state/set-state!
+                          :plugin/marketplace-stats
+                          (into {} (map (fn [[k stat]]
+                                          [k (assoc stat
+                                                    :total_downloads
+                                                    (reduce (fn [a b] (+ a (get b 2))) 0 (:releases stat)))])
+                                        res)))
+                         (resolve nil))
+                       (reject nil)))]
+         (if (state/http-proxy-enabled-or-val?)
+           (-> (ipc/ipc :httpFetchJSON stats-url)
+               (p/then on-ok)
+               (p/catch reject))
+           (util/fetch stats-url on-ok reject)))))
     (p/resolved nil)))
 
 (defn check-or-update-marketplace-plugin
@@ -114,32 +114,46 @@
   (when-not (and (:plugin/installing @state/state)
                  (not (plugin-common-handler/installed? id)))
     (p/catch
-      (p/then
-        (do (state/set-state! :plugin/installing pkg)
-            (p/catch
-              (load-marketplace-plugins false)
-              (fn [^js e]
-                (state/reset-all-updates-state)
-                (throw e))))
-        (fn [mfts]
-
-          (let [mft (some #(when (= (:id %) id) %) mfts)]
-            ;;TODO: (throw (js/Error. [:not-found-in-marketplace id]))
-            (ipc/ipc :updateMarketPlugin (merge (dissoc pkg :logger) mft)))
-          true))
-
-      (fn [^js e]
-        (error-handler e)
-        (state/set-state! :plugin/installing nil)
-        (js/console.error e)))))
+     (p/then
+      (do (state/set-state! :plugin/installing pkg)
+          (p/catch
+           (load-marketplace-plugins false)
+           (fn [^js e]
+             (state/reset-all-updates-state)
+             (throw e))))
+      (fn [mfts]
+
+        (let [mft (some #(when (= (:id %) id) %) mfts)]
+          ;;TODO: (throw (js/Error. [:not-found-in-marketplace id]))
+          (ipc/ipc :updateMarketPlugin (merge (dissoc pkg :logger) mft)))
+        true))
+
+     (fn [^js e]
+       (error-handler e)
+       (state/set-state! :plugin/installing nil)
+       (js/console.error e)))))
 
 (defn get-plugin-inst
-  [id]
+  [pid]
   (try
-    (js/LSPluginCore.ensurePlugin (name id))
+    (js/LSPluginCore.ensurePlugin (name pid))
     (catch :default _e
       nil)))
 
+(defn call-plugin-user-model!
+  [pid key args]
+  (when-let [^js pl (get-plugin-inst pid)]
+    (let [^js caller (.-caller pl)]
+      (.apply (.-callUserModelAsync caller) caller (bean/->js (list* (name key) args))))))
+
+(defn call-plugin-user-command!
+  [pid key args]
+  (when-let [commands (and key (seq (get (:plugin/simple-commands @state/state) (keyword pid))))]
+    (when-let [matched (medley/find-first #(= (:key (second %)) key) commands)]
+      (let [[_ cmd action pid] matched]
+        (state/pub-event!
+         [:exec-plugin-cmd {:type type :key key :pid pid :cmd (assoc cmd :args args) :action action}])))))
+
 (defn open-updates-downloading
   []
   (when (and (not (:plugin/updates-downloading? @state/state))
@@ -168,7 +182,7 @@
 
 (defn setup-install-listener!
   []
-  (let [channel (name :lsp-installed)
+  (let [channel  (name :lsp-installed)
         listener (fn [^js _ ^js e]
                    (js/console.debug :lsp-installed e)
 
@@ -205,7 +219,7 @@
                                           [(str (t :plugin/up-to-date) " :)") :success]
 
                                           [error-code :error])
-                             pending? (seq (:plugin/updates-pending @state/state))]
+                             pending?   (seq (:plugin/updates-pending @state/state))]
 
                          (if (and only-check pending?)
                            (state/consume-updates-coming-plugin payload false)
@@ -217,10 +231,10 @@
 
                              ;; notify human tips
                              (notification/show!
-                               (str
-                                 (if (= :error type) "[Error]" "")
-                                 (str "<" (:id payload) "> ")
-                                 msg) type)))
+                              (str
+                               (if (= :error type) "[Error]" "")
+                               (str "<" (:id payload) "> ")
+                               msg) type)))
 
                          (js/console.error payload))
 
@@ -272,18 +286,18 @@
                                    (get keybinding-mode-handler-map (keyword mode)))
                      :action     (fn []
                                    (state/pub-event!
-                                     [:exec-plugin-cmd {:type type :key key :pid pid :cmd cmd :action action}]))}]
+                                    [:exec-plugin-cmd {:type type :key key :pid pid :cmd cmd :action action}]))}]
     palette-cmd))
 
 (defn simple-cmd-keybinding->shortcut-args
   [pid key keybinding]
-  (let [id (keyword (str "plugin." pid "/" key))
+  (let [id      (keyword (str "plugin." pid "/" key))
         binding (:binding keybinding)
         binding (if util/mac?
                   (or (:mac keybinding) binding)
                   binding)
-        mode (or (:mode keybinding) :global)
-        mode (get keybinding-mode-handler-map (keyword mode))]
+        mode    (or (:mode keybinding) :global)
+        mode    (get keybinding-mode-handler-map (keyword mode))]
     [mode id {:binding binding}]))
 
 (defn register-plugin-simple-command
@@ -320,7 +334,7 @@
       (let [path [:plugin/installed-resources pid type]]
         (when (contains? #{:error nil} (get-in @state/state (conj path key)))
           (swap! state/state update-in path
-            (fnil assoc {}) key (merge opts {:pid pid}))
+                 (fnil assoc {}) key (merge opts {:pid pid}))
           true)))))
 
 (defn unregister-plugin-resources
@@ -350,7 +364,7 @@
   [pid type {:keys [before subs render edit] :as _opts}]
   (when-let [key (and type (keyword type))]
     (register-plugin-resources pid :fenced-code-renderers
-      {:key key :edit edit :before before :subs subs :render render})
+                               {:key key :edit edit :before before :subs subs :render render})
     (swap! *fenced-code-providers conj pid)
     #(swap! *fenced-code-providers disj pid)))
 
@@ -366,7 +380,7 @@
   [pid type {:keys [enhancer] :as _opts}]
   (when-let [key (and type (keyword type))]
     (register-plugin-resources pid :extensions-enhancers
-       {:key key :enhancer enhancer})
+                               {:key key :enhancer enhancer})
     (swap! *extensions-enhancer-providers conj pid)
     #(swap! *extensions-enhancer-providers disj pid)))
 
@@ -411,11 +425,11 @@
     (when-not (string/blank? content)
       (let [content (if-not (string/blank? url)
                       (string/replace
-                        content #"!\[[^\]]*\]\((.*?)\s*(\"(?:.*[^\"])\")?\s*\)"
-                        (fn [[matched link]]
-                          (if (and link (not (string/starts-with? link "http")))
-                            (string/replace matched link (util/node-path.join url link))
-                            matched)))
+                       content #"!\[[^\]]*\]\((.*?)\s*(\"(?:.*[^\"])\")?\s*\)"
+                       (fn [[matched link]]
+                         (if (and link (not (string/starts-with? link "http")))
+                           (string/replace matched link (util/node-path.join url link))
+                           matched)))
                       content)]
         (format/to-html content :markdown (gp-mldoc/default-config :markdown))))
     (catch :default e
@@ -429,9 +443,9 @@
       ;; local
       (-> (p/let [content (invoke-exported-api "load_plugin_readme" url)
                   content (parse-user-md-content content item)]
-                 (and (string/blank? (string/trim content)) (throw nil))
-                 (state/set-state! :plugin/active-readme [content item])
-                 (state/set-sub-modal! (fn [_] (display))))
+            (and (string/blank? (string/trim content)) (throw nil))
+            (state/set-state! :plugin/active-readme [content item])
+            (state/set-sub-modal! (fn [_] (display))))
           (p/catch #(do (js/console.warn %)
                         (notification/show! "No README content." :warn))))
       ;; market
@@ -441,8 +455,8 @@
   []
   (when util/electron?
     (p/let [path (ipc/ipc "openDialog")]
-           (when-not (:plugin/selected-unpacked-pkg @state/state)
-             (state/set-state! :plugin/selected-unpacked-pkg path)))))
+      (when-not (:plugin/selected-unpacked-pkg @state/state)
+        (state/set-state! :plugin/selected-unpacked-pkg path)))))
 
 (defn reset-unpacked-state
   []
@@ -482,6 +496,11 @@
                 type     (str "block:" (:block/uuid b))]]
     (hook-plugin-db type {:block b :tx-data (get tx-data' (:db/id b)) :tx-meta tx-meta})))
 
+(defn hook-plugin-block-slot
+  [block payload]
+  (when-let [type (and block (str "slot:" (:block/uuid block)))]
+    (hook-plugin-editor type (merge payload block) nil)))
+
 (defn get-ls-dotdir-root
   []
   (ipc/ipc "getLogseqDotDirRoot"))
@@ -533,11 +552,11 @@
 (defn- get-user-default-plugins
   []
   (p/catch
-    (p/let [files ^js (ipc/ipc "getUserDefaultPlugins")
-            files (js->clj files)]
-           (map #(hash-map :url %) files))
-    (fn [e]
-      (js/console.error e))))
+   (p/let [files ^js (ipc/ipc "getUserDefaultPlugins")
+           files (js->clj files)]
+     (map #(hash-map :url %) files))
+   (fn [e]
+     (js/console.error e))))
 
 (defn check-enabled-for-updates
   [theme?]

+ 35 - 18
src/main/frontend/handler/shell.cljs

@@ -7,7 +7,8 @@
             [promesa.core :as p]
             [frontend.db :as db]
             [frontend.state :as state]
-            [frontend.config :as config]))
+            [frontend.config :as config]
+            [frontend.util :as util]))
 
 (defn run-git-command!
   [command]
@@ -17,14 +18,15 @@
   [command]
   (ipc/ipc "runGitWithinCurrentGraph" command))
 
-;; TODO: export to pdf/html/word
-(defn run-pandoc-command!
-  [command]
-  (ipc/ipc "runPandoc" command))
+(defn run-cli-command!
+  [command args]
+  (ipc/ipc :runCli {:command      command
+                    :args         args
+                    :returnResult true}))
 
 (defn wrap-notification!
   [command f args]
-  (p/let [result (f args)]
+  (p/let [result (f command args)]
     (notification/show!
      (if (string/blank? result)
        [:p [:code.mr-1 (str command " " args) ]
@@ -33,22 +35,29 @@
      :success
      false)))
 
+(def commands-denylist
+  #{"rm" "mv" "rename" "dd" ">" "command" "sudo"})
+
 (defn run-command!
   [command]
-  (let [[command args] (gp-util/split-first " " command)
-        command (and command (string/lower-case command))]
-    (when (and (not (string/blank? command)) (not (string/blank? args)))
-      (let [args (string/trim args)]
-        (case (keyword command)
-         :git
-         (wrap-notification! command run-git-command! args)
+  (let [[command args]
+        (if (and (string? command) (string/includes? command " "))
+          (gp-util/split-first " " command)
+          [command ""])
+        command (and command (string/lower-case command))
+        args (-> args str string/trim)]
+    (when-not (string/blank? command)
+      (cond
+        (contains? commands-denylist command)
+        (notification/show!
+         [:div (str command " is too dangerous!")]
+         :error)
 
-         ;; :pandoc
-         ;; (wrap-notification! command run-pandoc-command! args)
+        (= "git" command)
+        (wrap-notification! command (fn [_ args] (run-git-command! args)) args)
 
-         (notification/show!
-          [:div (str command " is not supported yet!")]
-          :error))))))
+        :else
+        (run-cli-command! command args)))))
 
 ;; git show $REV:$FILE
 (defn- get-versioned-file-content
@@ -90,3 +99,11 @@
     (notification/show!
      [:div "git config successfully!"]
      :success)))
+
+(defn run-cli-command-wrapper!
+  [command content]
+  (let [args (case command
+               "alda" (util/format "play -c \"%s\"" content)
+               ;; TODO: plugin slot
+               content)]
+    (run-cli-command! command args)))

+ 5 - 1
src/main/frontend/modules/layout/core.cljs

@@ -1,12 +1,16 @@
 (ns frontend.modules.layout.core
   (:require [cljs-bean.core :as bean]
+            [goog.object :as gobj]
             [frontend.util :as util]))
 
 (defonce *movable-containers (atom {}))
 
 (defn- calc-layout-data
   [^js cnt ^js _evt]
-  (.toJSON (.getBoundingClientRect cnt)))
+  (let [^js o (.toJSON (.getBoundingClientRect cnt))]
+    (set! (.-vw o) (gobj/get js/visualViewport "width"))
+    (set! (.-vh o) (gobj/get js/visualViewport "height"))
+    o))
 
 (defn ^:export move-container-to-top
   [identity]

+ 7 - 1
src/main/frontend/state.cljs

@@ -1437,7 +1437,7 @@ Similar to re-frame subscriptions"
   [value]
   (set-state! :network/online? value))
 
-(defn get-plugins-commands
+(defn get-plugins-slash-commands
   []
   (mapcat seq (flatten (vals (:plugin/installed-slash-commands @state)))))
 
@@ -1542,6 +1542,12 @@ Similar to re-frame subscriptions"
         (set-state! [:plugin/installed-hooks hook-or-all] (disj coll pid))))
     true))
 
+(defn slot-hook-exist?
+  [uuid]
+  (when-let [type (and uuid (string/replace (str uuid) "-" "_"))]
+    (when-let [hooks (sub :plugin/installed-hooks)]
+      (contains? hooks (str "hook:editor:slot_" type)))))
+
 (defn active-tldraw-app
   []
   (when-let [tldraw-el (.closest js/document.activeElement ".logseq-tldraw[data-tlapp]")]

+ 151 - 132
src/main/logseq/api.cljs

@@ -118,25 +118,25 @@
   ;; get app base info
   []
   (bean/->js
-    (normalize-keyword-for-json
-      {:version fv/version})))
+   (normalize-keyword-for-json
+    {:version fv/version})))
 
 (def ^:export get_user_configs
   (fn []
     (bean/->js
-      (normalize-keyword-for-json
-        {:preferred-language    (:preferred-language @state/state)
-         :preferred-theme-mode  (:ui/theme @state/state)
-         :preferred-format      (state/get-preferred-format)
-         :preferred-workflow    (state/get-preferred-workflow)
-         :preferred-todo        (state/get-preferred-todo)
-         :preferred-date-format (state/get-date-formatter)
-         :preferred-start-of-week (state/get-start-of-week)
-         :current-graph         (state/get-current-repo)
-         :show-brackets         (state/show-brackets?)
-         :enabled-journals      (state/enable-journals?)
-         :enabled-flashcards    (state/enable-flashcards?)
-         :me                    (state/get-me)}))))
+     (normalize-keyword-for-json
+      {:preferred-language      (:preferred-language @state/state)
+       :preferred-theme-mode    (:ui/theme @state/state)
+       :preferred-format        (state/get-preferred-format)
+       :preferred-workflow      (state/get-preferred-workflow)
+       :preferred-todo          (state/get-preferred-todo)
+       :preferred-date-format   (state/get-date-formatter)
+       :preferred-start-of-week (state/get-start-of-week)
+       :current-graph           (state/get-current-repo)
+       :show-brackets           (state/show-brackets?)
+       :enabled-journals        (state/enable-journals?)
+       :enabled-flashcards      (state/enable-flashcards?)
+       :me                      (state/get-me)}))))
 
 (def ^:export get_current_graph_configs
   (fn []
@@ -188,7 +188,7 @@
           path (util/node-path.join path "package.json")]
       (fs/write-file! repo "" path (js/JSON.stringify data nil 2) {:skip-compare? true}))))
 
-(def ^:export save_focused_code_editor
+(def ^:export save_focused_code_editor_content
   (fn []
     (code-handler/save-code-editor!)))
 
@@ -277,7 +277,7 @@
 (def ^:export read_plugin_storage_file
   (fn [plugin-id file assets?]
     (let [plugin-id (util/node-path.basename plugin-id)
-          sub-root (util/node-path.join "storages" plugin-id)]
+          sub-root  (util/node-path.join "storages" plugin-id)]
       (if (true? assets?)
         (read_assetsdir_file file sub-root)
         (read_dotdir_file file sub-root)))))
@@ -285,7 +285,7 @@
 (def ^:export unlink_plugin_storage_file
   (fn [plugin-id file assets?]
     (let [plugin-id (util/node-path.basename plugin-id)
-          sub-root (util/node-path.join "storages" plugin-id)]
+          sub-root  (util/node-path.join "storages" plugin-id)]
       (if (true? assets?)
         (unlink_assetsdir_file! file sub-root)
         (unlink_dotdir_file! file sub-root)))))
@@ -327,7 +327,7 @@
     (p/let [repo ""
             path (plugin-handler/get-ls-dotdir-root)
             path (util/node-path.join path "preferences.json")
-            _ (fs/create-if-not-exists repo "" path)
+            _    (fs/create-if-not-exists repo "" path)
             json (fs/read-file "" path)
             json (if (string/blank? json) "{}" json)]
       (js/JSON.parse json))))
@@ -338,7 +338,7 @@
       (p/let [repo ""
               path (plugin-handler/get-ls-dotdir-root)
               path (util/node-path.join path "preferences.json")]
-             (fs/write-file! repo "" path (js/JSON.stringify data nil 2) {:skip-compare? true})))))
+        (fs/write-file! repo "" path (js/JSON.stringify data nil 2) {:skip-compare? true})))))
 
 (def ^:export load_plugin_user_settings
   ;; results [path data]
@@ -356,18 +356,18 @@
   (fn [pid ^js cmd-actions]
     (when-let [[cmd actions] (bean/->clj cmd-actions)]
       (plugin-handler/register-plugin-slash-command
-        pid [cmd (mapv #(into [(keyword (first %))]
-                              (rest %)) actions)]))))
+       pid [cmd (mapv #(into [(keyword (first %))]
+                             (rest %)) actions)]))))
 
 (def ^:export register_plugin_simple_command
   (fn [pid ^js cmd-action palette?]
     (when-let [[cmd action] (bean/->clj cmd-action)]
-      (let [action (assoc action 0 (keyword (first action)))
-            cmd (assoc cmd :key (string/replace (:key cmd) ":" "-"))
-            key (:key cmd)
-            keybinding (:keybinding cmd)
+      (let [action      (assoc action 0 (keyword (first action)))
+            cmd         (assoc cmd :key (string/replace (:key cmd) ":" "-"))
+            key         (:key cmd)
+            keybinding  (:keybinding cmd)
             palette-cmd (and palette? (plugin-handler/simple-cmd->palette-cmd pid cmd action))
-            action' #(state/pub-event! [:exec-plugin-cmd {:type type :key key :pid pid :cmd cmd :action action}])]
+            action'     #(state/pub-event! [:exec-plugin-cmd {:type type :key key :pid pid :cmd cmd :action action}])]
 
         ;; handle simple commands
         (plugin-handler/register-plugin-simple-command pid cmd action)
@@ -414,7 +414,7 @@
   (fn [pid type ^js opts]
     (when-let [opts (bean/->clj opts)]
       (plugin-handler/register-plugin-ui-item
-        pid (assoc opts :type type)))))
+       pid (assoc opts :type type)))))
 
 ;; app
 (def ^:export relaunch
@@ -465,16 +465,30 @@
 (def ^:export push_state
   (fn [^js k ^js params ^js query]
     (rfe/push-state
-      (keyword k)
-      (bean/->clj params)
-      (bean/->clj query))))
+     (keyword k)
+     (bean/->clj params)
+     (bean/->clj query))))
 
 (def ^:export replace_state
   (fn [^js k ^js params ^js query]
     (rfe/replace-state
-      (keyword k)
-      (bean/->clj params)
-      (bean/->clj query))))
+     (keyword k)
+     (bean/->clj params)
+     (bean/->clj query))))
+
+(defn ^:export get_external_plugin
+  [pid]
+  (when-let [^js pl (plugin-handler/get-plugin-inst pid)]
+    (.toJSON pl)))
+
+(defn ^:export invoke_external_plugin_cmd
+  [pid cmd-group cmd-key cmd-args]
+  (case (keyword cmd-group)
+    :models
+    (plugin-handler/call-plugin-user-model! pid cmd-key cmd-args)
+
+    :commands
+    (plugin-handler/call-plugin-user-command! pid cmd-key cmd-args)))
 
 ;; editor
 (def ^:export check_editing
@@ -543,13 +557,13 @@
               page
               (let [properties (bean/->clj properties)
                     {:keys [redirect createFirstBlock format journal]} (bean/->clj opts)
-                    name (page-handler/create!
-                           name
-                           {:redirect?           (if (boolean? redirect) redirect true)
-                            :journal?            journal
-                            :create-first-block? (if (boolean? createFirstBlock) createFirstBlock true)
-                            :format              format
-                            :properties          properties})]
+                    name       (page-handler/create!
+                                name
+                                {:redirect?           (if (boolean? redirect) redirect true)
+                                 :journal?            journal
+                                 :create-first-block? (if (boolean? createFirstBlock) createFirstBlock true)
+                                 :format              format
+                                 :properties          properties})]
                 (db-model/get-page name)))
             (:db/id)
             (db-utils/pull)
@@ -585,47 +599,47 @@
 (def ^:export insert_block
   (fn [block-uuid-or-page-name content ^js opts]
     (let [{:keys [before sibling focus isPageBlock customUUID properties]} (bean/->clj opts)
-          page-name (and isPageBlock block-uuid-or-page-name)
-          custom-uuid (or customUUID (:id properties))
-          custom-uuid (when custom-uuid (uuid-or-throw-error custom-uuid))
-          edit-block? (if (nil? focus) true focus)
-          _ (when (and custom-uuid (db-model/query-block-by-uuid custom-uuid))
-              (throw (js/Error.
-                      (util/format "Custom block UUID already exists (%s)." custom-uuid))))
-          block-uuid (if isPageBlock nil (uuid block-uuid-or-page-name))
-          block-uuid' (if (and (not sibling) before block-uuid)
-                        (let [block (db/entity [:block/uuid block-uuid])
-                              first-child (db-model/get-by-parent-&-left (db/get-db)
-                                                                         (:db/id block)
-                                                                         (:db/id block))]
-                          (if first-child
-                            (:block/uuid first-child)
-                            block-uuid))
-                        block-uuid)
+          page-name              (and isPageBlock block-uuid-or-page-name)
+          custom-uuid            (or customUUID (:id properties))
+          custom-uuid            (when custom-uuid (uuid-or-throw-error custom-uuid))
+          edit-block?            (if (nil? focus) true focus)
+          _                      (when (and custom-uuid (db-model/query-block-by-uuid custom-uuid))
+                                   (throw (js/Error.
+                                           (util/format "Custom block UUID already exists (%s)." custom-uuid))))
+          block-uuid             (if isPageBlock nil (uuid block-uuid-or-page-name))
+          block-uuid'            (if (and (not sibling) before block-uuid)
+                                   (let [block       (db/entity [:block/uuid block-uuid])
+                                         first-child (db-model/get-by-parent-&-left (db/get-db)
+                                                                                    (:db/id block)
+                                                                                    (:db/id block))]
+                                     (if first-child
+                                       (:block/uuid first-child)
+                                       block-uuid))
+                                   block-uuid)
           insert-at-first-child? (not= block-uuid' block-uuid)
           [sibling? before?] (if insert-at-first-child?
                                [true true]
                                [sibling before])
-          before? (if (and (false? sibling?) before? (not insert-at-first-child?))
-                    false
-                    before?)
-          new-block (editor-handler/api-insert-new-block!
-                      content
-                      {:block-uuid  block-uuid'
-                       :sibling?    sibling?
-                       :before?     before?
-                       :edit-block? edit-block?
-                       :page        page-name
-                       :custom-uuid custom-uuid
-                       :properties  (merge properties
-                                           (when custom-uuid {:id custom-uuid}))})]
+          before?                (if (and (false? sibling?) before? (not insert-at-first-child?))
+                                   false
+                                   before?)
+          new-block              (editor-handler/api-insert-new-block!
+                                  content
+                                  {:block-uuid  block-uuid'
+                                   :sibling?    sibling?
+                                   :before?     before?
+                                   :edit-block? edit-block?
+                                   :page        page-name
+                                   :custom-uuid custom-uuid
+                                   :properties  (merge properties
+                                                       (when custom-uuid {:id custom-uuid}))})]
       (bean/->js (normalize-keyword-for-json new-block)))))
 
 (def ^:export insert_batch_block
   (fn [block-uuid ^js batch-blocks ^js opts]
     (when-let [block (db-model/query-block-by-uuid (uuid-or-throw-error block-uuid))]
       (when-let [bb (bean/->clj batch-blocks)]
-        (let [bb (if-not (vector? bb) (vector bb) bb)
+        (let [bb         (if-not (vector? bb) (vector bb) bb)
               {:keys [sibling keepUUID]} (bean/->clj opts)
               keep-uuid? (or keepUUID false)
               _ (when keep-uuid? (doseq
@@ -641,16 +655,16 @@
 (def ^:export remove_block
   (fn [block-uuid ^js _opts]
     (let [includeChildren true
-          repo (state/get-current-repo)]
+          repo            (state/get-current-repo)]
       (editor-handler/delete-block-aux!
        {:block/uuid (uuid-or-throw-error block-uuid) :repo repo} includeChildren)
       nil)))
 
 (def ^:export update_block
   (fn [block-uuid content ^js _opts]
-    (let [repo (state/get-current-repo)
+    (let [repo       (state/get-current-repo)
           edit-input (state/get-edit-input-id)
-          editing? (and edit-input (string/ends-with? edit-input (str block-uuid)))]
+          editing?   (and edit-input (string/ends-with? edit-input (str block-uuid)))]
       (if editing?
         (state/set-edit-content! edit-input content)
         (editor-handler/save-block! repo (uuid-or-throw-error block-uuid) content))
@@ -659,16 +673,16 @@
 (def ^:export move_block
   (fn [src-block-uuid target-block-uuid ^js opts]
     (let [{:keys [before children]} (bean/->clj opts)
-          move-to (cond
-                    (boolean before)
-                    :top
+          move-to      (cond
+                         (boolean before)
+                         :top
 
-                    (boolean children)
-                    :nested
+                         (boolean children)
+                         :nested
 
-                    :else
-                    nil)
-          src-block (db-model/query-block-by-uuid (uuid-or-throw-error src-block-uuid))
+                         :else
+                         nil)
+          src-block    (db-model/query-block-by-uuid (uuid-or-throw-error src-block-uuid))
           target-block (db-model/query-block-by-uuid (uuid-or-throw-error target-block-uuid))]
       (editor-dnd-handler/move-blocks nil [src-block] target-block move-to) nil)))
 
@@ -680,15 +694,15 @@
       (when-not (contains? block :block/name)
         (when-let [uuid (:block/uuid block)]
           (let [{:keys [includeChildren]} (bean/->clj opts)
-                repo (state/get-current-repo)
+                repo  (state/get-current-repo)
                 block (if includeChildren
                         ;; nested children results
                         (first (outliner-tree/blocks->vec-tree
-                                       (db-model/get-block-and-children repo uuid) uuid))
+                                (db-model/get-block-and-children repo uuid) uuid))
                         ;; attached shallow children
                         (assoc block :block/children
-                          (map #(list :uuid (get-in % [:data :block/uuid]))
-                            (db/get-block-immediate-children repo uuid))))]
+                               (map #(list :uuid (get-in % [:data :block/uuid]))
+                                    (db/get-block-immediate-children repo uuid))))]
             (bean/->js (normalize-keyword-for-json block))))))))
 
 (def ^:export get_current_block
@@ -718,15 +732,15 @@
   (fn [block-uuid ^js opts]
     (let [block-uuid (uuid-or-throw-error block-uuid)]
       (when-let [block (db-model/get-block-by-uuid block-uuid)]
-       (let [opts       (bean/->clj opts)
-             opts       (if (or (string? opts) (boolean? opts)) {:flag opts} opts)
-             {:keys [flag]} opts
-             flag       (if (= "toggle" flag)
-                          (not (util/collapsed? block))
-                          (boolean flag))]
-         (if flag (editor-handler/collapse-block! block-uuid)
-             (editor-handler/expand-block! block-uuid))
-         nil)))))
+        (let [opts (bean/->clj opts)
+              opts (if (or (string? opts) (boolean? opts)) {:flag opts} opts)
+              {:keys [flag]} opts
+              flag (if (= "toggle" flag)
+                     (not (util/collapsed? block))
+                     (boolean flag))]
+          (if flag (editor-handler/collapse-block! block-uuid)
+                   (editor-handler/expand-block! block-uuid))
+          nil)))))
 
 (def ^:export upsert_block_property
   (fn [block-uuid key value]
@@ -766,7 +780,7 @@
 (defn ^:export get_page_linked_references
   [page-name-or-uuid]
   (when-let [page (and page-name-or-uuid (db-model/get-page page-name-or-uuid))]
-    (let [page-name (:block/name page)
+    (let [page-name  (:block/name page)
           ref-blocks (if page-name
                        (db-model/get-page-referenced-blocks-full page-name)
                        (db-model/get-block-referenced-blocks (:block/uuid page)))
@@ -802,12 +816,12 @@
 
 (defn ^:export prepend_block_in_page
   [uuid-or-page-name content ^js opts]
-  (let [page? (not (util/uuid-string? uuid-or-page-name))
+  (let [page?           (not (util/uuid-string? uuid-or-page-name))
         page-not-exist? (and page? (nil? (db-model/get-page uuid-or-page-name)))
-        _ (and page-not-exist? (page-handler/create! uuid-or-page-name
-                                 {:redirect? false
-                                  :create-first-block? true
-                                  :format (state/get-preferred-format)}))]
+        _               (and page-not-exist? (page-handler/create! uuid-or-page-name
+                                                                   {:redirect?           false
+                                                                    :create-first-block? true
+                                                                    :format              (state/get-preferred-format)}))]
     (when-let [block (db-model/get-page uuid-or-page-name)]
       (let [block'   (if page? (second-child-of-block block) (first-child-of-block block))
             sibling? (and page? (not (nil? block')))
@@ -818,18 +832,18 @@
 
 (defn ^:export append_block_in_page
   [uuid-or-page-name content ^js opts]
-  (let [page? (not (util/uuid-string? uuid-or-page-name))
+  (let [page?           (not (util/uuid-string? uuid-or-page-name))
         page-not-exist? (and page? (nil? (db-model/get-page uuid-or-page-name)))
-        _ (and page-not-exist? (page-handler/create! uuid-or-page-name
-                                 {:redirect? false
-                                  :create-first-block? true
-                                  :format (state/get-preferred-format)}))]
+        _               (and page-not-exist? (page-handler/create! uuid-or-page-name
+                                                                   {:redirect?           false
+                                                                    :create-first-block? true
+                                                                    :format              (state/get-preferred-format)}))]
     (when-let [block (db-model/get-page uuid-or-page-name)]
       (let [block'   (last-child-of-block block)
             sibling? (not (nil? block'))
             opts     (bean/->clj opts)
             opts     (merge opts {:isPageBlock (and page? (not sibling?)) :sibling sibling?}
-                       (when sibling? {:before false}))
+                            (when sibling? {:before false}))
             src      (if sibling? (str (:block/uuid block')) uuid-or-page-name)]
         (insert_block src content (bean/->js opts))))))
 
@@ -855,13 +869,18 @@
   [query & inputs]
   (when-let [repo (state/get-current-repo)]
     (when-let [db (db/get-db repo)]
-      (let [query (cljs.reader/read-string query)
+      (let [query           (cljs.reader/read-string query)
             resolved-inputs (map #(cond
                                     (string? %)
                                     (some-> % (cljs.reader/read-string) (query-react/resolve-input))
+
+                                    (fn? %)
+                                    (fn [& args]
+                                      (.apply % nil (clj->js (mapv bean/->js args))))
+
                                     :else %)
                                  inputs)
-            result (apply d/q query db resolved-inputs)]
+            result          (apply d/q query db resolved-inputs)]
         (bean/->js (normalize-keyword-for-json result false))))))
 
 (defn ^:export custom_query
@@ -874,7 +893,7 @@
   []
   (when-let [repo (state/get-current-repo)]
     (when-let [db (db/get-db repo)]
-      (let [db-str (if db (db/db->string db) "")
+      (let [db-str   (if db (db/db->string db) "")
             data-str (str "data:text/edn;charset=utf-8," (js/encodeURIComponent db-str))]
         (when-let [anchor (gdom/getElement "download")]
           (.setAttribute anchor "href" data-str)
@@ -900,18 +919,18 @@
 (defn ^:export git_load_ignore_file
   []
   (when-let [repo (state/get-current-repo)]
-    (p/let [file ".gitignore"
-            dir (config/get-repo-dir repo)
-            _ (fs/create-if-not-exists repo dir file)
+    (p/let [file    ".gitignore"
+            dir     (config/get-repo-dir repo)
+            _       (fs/create-if-not-exists repo dir file)
             content (fs/read-file dir file)]
-           content)))
+      content)))
 
 (defn ^:export git_save_ignore_file
   [content]
   (when-let [repo (and (string? content) (state/get-current-repo))]
     (p/let [file ".gitignore"
-            dir (config/get-repo-dir repo)
-            _ (fs/write-file! repo dir file content {:skip-compare? true})])))
+            dir  (config/get-repo-dir repo)
+            _    (fs/write-file! repo dir file content {:skip-compare? true})])))
 
 ;; ui
 (defn ^:export show_msg
@@ -921,9 +940,9 @@
    (let [{:keys [key timeout]} (bean/->clj opts)
          hiccup? (and (string? content) (string/starts-with? (string/triml content) "[:"))
          content (if hiccup? (parse-hiccup-ui content) content)
-         uid (when (string? key) (keyword key))
-         clear? (not= timeout 0)
-         key' (notification/show! content (keyword status) clear? uid timeout)]
+         uid     (when (string? key) (keyword key))
+         clear?  (not= timeout 0)
+         key'    (notification/show! content (keyword status) clear? uid timeout)]
      (name key'))))
 
 (defn ^:export ui_show_msg
@@ -939,7 +958,7 @@
 (defn ^:export assets_list_files_of_current_graph
   [^js exts]
   (p/let [files (ipc/ipc :getAssetsFiles {:exts exts})]
-         (bean/->js files)))
+    (bean/->js files)))
 
 ;; experiments
 (defn ^:export exper_load_scripts
@@ -947,28 +966,28 @@
   (when-let [^js _pl (plugin-handler/get-plugin-inst pid)]
     (doseq [s scripts
             :let [upt-status #(state/upt-plugin-resource pid :scripts s :status %)
-                  init? (plugin-handler/register-plugin-resources pid :scripts {:key s :src s})]]
+                  init?      (plugin-handler/register-plugin-resources pid :scripts {:key s :src s})]]
       (when init?
         (p/catch
-          (p/then
-            (do
-              (upt-status :pending)
-              (loader/load s nil {:attributes {:data-ref (name pid)}}))
-            #(upt-status :done))
-          #(upt-status :error))))))
+         (p/then
+          (do
+            (upt-status :pending)
+            (loader/load s nil {:attributes {:data-ref (name pid)}}))
+          #(upt-status :done))
+         #(upt-status :error))))))
 
 (defn ^:export exper_register_fenced_code_renderer
   [pid type ^js opts]
   (when-let [^js _pl (plugin-handler/get-plugin-inst pid)]
     (plugin-handler/register-fenced-code-renderer
-      (keyword pid) type (reduce #(assoc %1 %2 (aget opts (name %2))) {}
-                                 [:edit :before :subs :render]))))
+     (keyword pid) type (reduce #(assoc %1 %2 (aget opts (name %2))) {}
+                                [:edit :before :subs :render]))))
 
 (defn ^:export exper_register_extensions_enhancer
   [pid type enhancer]
   (when-let [^js _pl (and (fn? enhancer) (plugin-handler/get-plugin-inst pid))]
     (plugin-handler/register-extensions-enhancer
-      (keyword pid) type {:enhancer enhancer})))
+     (keyword pid) type {:enhancer enhancer})))
 
 (defonce *request-k (volatile! 0))
 
@@ -1006,7 +1025,7 @@
 (defn ^:export force_save_graph
   []
   (p/let [_ (el/persist-dbs!)]
-         true))
+    true))
 
 (def ^:export make_asset_url editor-handler/make-asset-url)
 

+ 5 - 0
static/yarn.lock

@@ -1404,6 +1404,11 @@ combined-stream@^1.0.8:
   dependencies:
     delayed-stream "~1.0.0"
 
[email protected]:
+  version "1.2.9"
+  resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69"
+  integrity sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==
+
 [email protected]:
   version "2.9.0"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"

Vissa filer visades inte eftersom för många filer har ändrats