LSPlugin.user.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907
  1. import {
  2. isValidUUID,
  3. deepMerge,
  4. mergeSettingsWithSchema,
  5. PluginLogger,
  6. safeSnakeCase,
  7. safetyPathJoin, normalizeKeyStr,
  8. } from './helpers'
  9. import { LSPluginCaller } from './LSPlugin.caller'
  10. import * as callableAPIs from './callable.apis'
  11. import {
  12. IAppProxy,
  13. IDBProxy,
  14. IEditorProxy,
  15. ILSPluginUser,
  16. LSPluginBaseInfo,
  17. LSPluginUserEvents,
  18. SlashCommandAction,
  19. BlockCommandCallback,
  20. StyleString,
  21. Theme,
  22. UIOptions,
  23. IHookEvent,
  24. BlockIdentity,
  25. BlockPageName,
  26. UIContainerAttrs,
  27. SimpleCommandCallback,
  28. SimpleCommandKeybinding,
  29. SettingSchemaDesc,
  30. IUserOffHook,
  31. IGitProxy,
  32. IUIProxy,
  33. UserProxyNSTags,
  34. BlockUUID,
  35. BlockEntity,
  36. IDatom,
  37. IAssetsProxy,
  38. AppInfo,
  39. IPluginSearchServiceHooks,
  40. PageEntity, IUtilsProxy,
  41. } from './LSPlugin'
  42. import Debug from 'debug'
  43. import * as CSS from 'csstype'
  44. import EventEmitter from 'eventemitter3'
  45. import { IAsyncStorage, LSPluginFileStorage } from './modules/LSPlugin.Storage'
  46. import { LSPluginExperiments } from './modules/LSPlugin.Experiments'
  47. import { LSPluginRequest } from './modules/LSPlugin.Request'
  48. import { LSPluginSearchService } from './modules/LSPlugin.Search'
  49. declare global {
  50. interface Window {
  51. __LSP__HOST__: boolean
  52. logseq: LSPluginUser
  53. }
  54. }
  55. type callableMethods = keyof typeof callableAPIs | string // host exported SDK apis & host platform related apis
  56. const PROXY_CONTINUE = Symbol.for('proxy-continue')
  57. const debug = Debug('LSPlugin:user')
  58. const logger = new PluginLogger('', { console: true })
  59. /**
  60. * @param type (key of group commands)
  61. * @param opts
  62. * @param action
  63. */
  64. function registerSimpleCommand(
  65. this: LSPluginUser,
  66. type: string,
  67. opts: {
  68. key: string
  69. label: string
  70. desc?: string
  71. palette?: boolean
  72. keybinding?: SimpleCommandKeybinding
  73. extras?: Record<string, any>
  74. },
  75. action: SimpleCommandCallback
  76. ) {
  77. const { key, label, desc, palette, keybinding, extras } = opts
  78. if (typeof action !== 'function') {
  79. this.logger.error(`${key || label}: command action should be function.`)
  80. return false
  81. }
  82. const normalizedKey = normalizeKeyStr(key)
  83. if (!normalizedKey) {
  84. this.logger.error(`${label}: command key is required.`)
  85. return false
  86. }
  87. const eventKey = `SimpleCommandHook${normalizedKey}${++registeredCmdUid}`
  88. this.Editor['on' + eventKey](action)
  89. this.caller?.call(`api:call`, {
  90. method: 'register-plugin-simple-command',
  91. args: [
  92. this.baseInfo.id,
  93. // [cmd, action]
  94. [
  95. { key: normalizedKey, label, type, desc, keybinding, extras },
  96. ['editor/hook', eventKey],
  97. ],
  98. palette,
  99. ],
  100. })
  101. }
  102. function shouldValidUUID(uuid: string) {
  103. if (!isValidUUID(uuid)) {
  104. logger.error(`#${uuid} is not a valid UUID string.`)
  105. return false
  106. }
  107. return true
  108. }
  109. function checkEffect(p: LSPluginUser) {
  110. return p && (p.baseInfo?.effect || !p.baseInfo?.iir)
  111. }
  112. let _appBaseInfo: AppInfo = null
  113. let _searchServices: Map<string, LSPluginSearchService> = new Map()
  114. const app: Partial<IAppProxy> = {
  115. async getInfo(this: LSPluginUser, key) {
  116. if (!_appBaseInfo) {
  117. _appBaseInfo = await this._execCallableAPIAsync('get-app-info')
  118. }
  119. return typeof key === 'string' ? _appBaseInfo[key] : _appBaseInfo
  120. },
  121. registerCommand: registerSimpleCommand,
  122. registerSearchService<T extends IPluginSearchServiceHooks>(
  123. this: LSPluginUser,
  124. s: T
  125. ) {
  126. if (_searchServices.has(s.name)) {
  127. throw new Error(`SearchService: #${s.name} has registered!`)
  128. }
  129. _searchServices.set(s.name, new LSPluginSearchService(this, s))
  130. },
  131. registerCommandPalette(
  132. opts: { key: string; label: string; keybinding?: SimpleCommandKeybinding },
  133. action: SimpleCommandCallback
  134. ) {
  135. const { key, label, keybinding } = opts
  136. const group = '$palette$'
  137. return registerSimpleCommand.call(
  138. this,
  139. group,
  140. { key, label, palette: true, keybinding },
  141. action
  142. )
  143. },
  144. registerCommandShortcut(
  145. keybinding: SimpleCommandKeybinding | string,
  146. action: SimpleCommandCallback,
  147. opts: Partial<{
  148. key: string
  149. label: string
  150. desc: string
  151. extras: Record<string, any>
  152. }> = {}
  153. ) {
  154. if (typeof keybinding == 'string') {
  155. keybinding = {
  156. mode: 'global',
  157. binding: keybinding,
  158. }
  159. }
  160. const { binding } = keybinding
  161. const group = '$shortcut$'
  162. const key = opts.key || (group + safeSnakeCase(binding?.toString()))
  163. return registerSimpleCommand.call(
  164. this,
  165. group,
  166. { ...opts, key, palette: false, keybinding },
  167. action
  168. )
  169. },
  170. registerUIItem(
  171. type: 'toolbar' | 'pagebar',
  172. opts: { key: string; template: string }
  173. ) {
  174. const pid = this.baseInfo.id
  175. // opts.key = `${pid}_${opts.key}`
  176. this.caller?.call(`api:call`, {
  177. method: 'register-plugin-ui-item',
  178. args: [pid, type, opts],
  179. })
  180. },
  181. registerPageMenuItem(
  182. this: LSPluginUser,
  183. tag: string,
  184. action: (e: IHookEvent & { page: string }) => void
  185. ) {
  186. if (typeof action !== 'function') {
  187. return false
  188. }
  189. const key = tag + '_' + this.baseInfo.id
  190. const label = tag
  191. const type = 'page-menu-item'
  192. registerSimpleCommand.call(
  193. this,
  194. type,
  195. {
  196. key,
  197. label,
  198. },
  199. action
  200. )
  201. },
  202. onBlockRendererSlotted(uuid, callback: (payload: any) => void) {
  203. if (!shouldValidUUID(uuid)) return
  204. const pid = this.baseInfo.id
  205. const hook = `hook:editor:${safeSnakeCase(`slot:${uuid}`)}`
  206. this.caller.on(hook, callback)
  207. this.App._installPluginHook(pid, hook)
  208. return () => {
  209. this.caller.off(hook, callback)
  210. this.App._uninstallPluginHook(pid, hook)
  211. }
  212. },
  213. invokeExternalPlugin(this: LSPluginUser, type: string, ...args: Array<any>) {
  214. type = type?.trim()
  215. if (!type) return
  216. let [pid, group] = type.split('.')
  217. if (!['models', 'commands'].includes(group?.toLowerCase())) {
  218. throw new Error(`Type only support '.models' or '.commands' currently.`)
  219. }
  220. const key = type.replace(`${pid}.${group}.`, '')
  221. if (!pid || !group || !key) {
  222. throw new Error(`Illegal type of #${type} to invoke external plugin.`)
  223. }
  224. return this._execCallableAPIAsync(
  225. 'invoke_external_plugin_cmd',
  226. pid,
  227. group.toLowerCase(),
  228. key,
  229. args
  230. )
  231. },
  232. setFullScreen(flag) {
  233. const sf = (...args) => this._callWin('setFullScreen', ...args)
  234. if (flag === 'toggle') {
  235. this._callWin('isFullScreen').then((r) => {
  236. r ? sf() : sf(true)
  237. })
  238. } else {
  239. flag ? sf(true) : sf()
  240. }
  241. },
  242. }
  243. let registeredCmdUid = 0
  244. const editor: Partial<IEditorProxy> = {
  245. newBlockUUID(this: LSPluginUser): Promise<string> {
  246. return this._execCallableAPIAsync('new_block_uuid')
  247. },
  248. isPageBlock(
  249. this: LSPluginUser,
  250. block: BlockEntity | PageEntity
  251. ): Boolean {
  252. return block.uuid && block.hasOwnProperty('name')
  253. },
  254. registerSlashCommand(
  255. this: LSPluginUser,
  256. tag: string,
  257. actions: BlockCommandCallback | Array<SlashCommandAction>
  258. ) {
  259. debug('Register slash command #', this.baseInfo.id, tag, actions)
  260. if (typeof actions === 'function') {
  261. actions = [
  262. ['editor/clear-current-slash', false],
  263. ['editor/restore-saved-cursor'],
  264. ['editor/hook', actions],
  265. ]
  266. }
  267. actions = actions.map((it) => {
  268. const [tag, ...args] = it
  269. switch (tag) {
  270. case 'editor/hook':
  271. let key = args[0]
  272. let fn = () => {
  273. this.caller?.callUserModel(key)
  274. }
  275. if (typeof key === 'function') {
  276. fn = key
  277. }
  278. const eventKey = `SlashCommandHook${tag}${++registeredCmdUid}`
  279. it[1] = eventKey
  280. // register command listener
  281. this.Editor['on' + eventKey](fn)
  282. break
  283. default:
  284. }
  285. return it
  286. })
  287. this.caller?.call(`api:call`, {
  288. method: 'register-plugin-slash-command',
  289. args: [this.baseInfo.id, [tag, actions]],
  290. })
  291. },
  292. registerBlockContextMenuItem(
  293. this: LSPluginUser,
  294. label: string,
  295. action: BlockCommandCallback
  296. ) {
  297. if (typeof action !== 'function') {
  298. return false
  299. }
  300. const key = label + '_' + this.baseInfo.id
  301. const type = 'block-context-menu-item'
  302. registerSimpleCommand.call(
  303. this,
  304. type,
  305. {
  306. key,
  307. label,
  308. },
  309. action
  310. )
  311. },
  312. registerHighlightContextMenuItem(
  313. this: LSPluginUser,
  314. label: string,
  315. action: SimpleCommandCallback,
  316. opts?: { clearSelection: boolean }
  317. ) {
  318. if (typeof action !== 'function') {
  319. return false
  320. }
  321. const key = label + '_' + this.baseInfo.id
  322. const type = 'highlight-context-menu-item'
  323. registerSimpleCommand.call(
  324. this,
  325. type,
  326. {
  327. key,
  328. label,
  329. extras: opts,
  330. },
  331. action
  332. )
  333. },
  334. scrollToBlockInPage(
  335. this: LSPluginUser,
  336. pageName: BlockPageName,
  337. blockId: BlockIdentity,
  338. opts?: { replaceState: boolean }
  339. ) {
  340. const anchor = `block-content-` + blockId
  341. if (opts?.replaceState) {
  342. this.App.replaceState('page', { name: pageName }, { anchor })
  343. } else {
  344. this.App.pushState('page', { name: pageName }, { anchor })
  345. }
  346. },
  347. }
  348. const db: Partial<IDBProxy> = {
  349. onBlockChanged(
  350. this: LSPluginUser,
  351. uuid: BlockUUID,
  352. callback: (
  353. block: BlockEntity,
  354. txData: Array<IDatom>,
  355. txMeta?: { outlinerOp: string; [p: string]: any }
  356. ) => void
  357. ): IUserOffHook {
  358. if (!shouldValidUUID(uuid)) return
  359. const pid = this.baseInfo.id
  360. const hook = `hook:db:${safeSnakeCase(`block:${uuid}`)}`
  361. const aBlockChange = ({ block, txData, txMeta }) => {
  362. if (block.uuid !== uuid) {
  363. return
  364. }
  365. callback(block, txData, txMeta)
  366. }
  367. this.caller.on(hook, aBlockChange)
  368. this.App._installPluginHook(pid, hook)
  369. return () => {
  370. this.caller.off(hook, aBlockChange)
  371. this.App._uninstallPluginHook(pid, hook)
  372. }
  373. },
  374. datascriptQuery<T = any>(
  375. this: LSPluginUser,
  376. query: string,
  377. ...inputs: Array<any>
  378. ): Promise<T> {
  379. // force remove proxy ns flag `db`
  380. inputs.pop()
  381. if (inputs?.some((it) => typeof it === 'function')) {
  382. const host = this.Experiments.ensureHostScope()
  383. return host.logseq.api.datascript_query(query, ...inputs)
  384. }
  385. return this._execCallableAPIAsync(`datascript_query`, ...[query, ...inputs])
  386. },
  387. }
  388. const git: Partial<IGitProxy> = {}
  389. const ui: Partial<IUIProxy> = {}
  390. const utils: Partial<IUtilsProxy> = {}
  391. const assets: Partial<IAssetsProxy> = {
  392. makeSandboxStorage(this: LSPluginUser): IAsyncStorage {
  393. return new LSPluginFileStorage(this, { assets: true })
  394. },
  395. }
  396. type uiState = {
  397. key?: number
  398. visible: boolean
  399. }
  400. const KEY_MAIN_UI = 0
  401. /**
  402. * User plugin instance from global namespace `logseq`.
  403. * @example
  404. * ```ts
  405. * logseq.UI.showMsg('Hello, Logseq')
  406. * ```
  407. * @public
  408. */
  409. export class LSPluginUser
  410. extends EventEmitter<LSPluginUserEvents>
  411. implements ILSPluginUser {
  412. // @ts-ignore
  413. private _version: string = LIB_VERSION
  414. private _debugTag: string = ''
  415. private _settingsSchema?: Array<SettingSchemaDesc>
  416. private _connected: boolean = false
  417. /**
  418. * ui frame identities
  419. * @private
  420. */
  421. private _ui = new Map<number, uiState>()
  422. private _mFileStorage: LSPluginFileStorage
  423. private _mRequest: LSPluginRequest
  424. private _mExperiments: LSPluginExperiments
  425. /**
  426. * handler of before unload plugin
  427. * @private
  428. */
  429. private _beforeunloadCallback?: (e: any) => Promise<void>
  430. /**
  431. * @param _baseInfo
  432. * @param _caller
  433. */
  434. constructor(
  435. private _baseInfo: LSPluginBaseInfo,
  436. private _caller: LSPluginCaller
  437. ) {
  438. super()
  439. _caller.on('sys:ui:visible', (payload) => {
  440. if (payload?.toggle) {
  441. this.toggleMainUI()
  442. }
  443. })
  444. _caller.on('settings:changed', (payload) => {
  445. const b = Object.assign({}, this.settings)
  446. const a = Object.assign(this._baseInfo.settings, payload)
  447. this.emit('settings:changed', { ...a }, b)
  448. })
  449. _caller.on('beforeunload', async (payload) => {
  450. const { actor, ...rest } = payload
  451. const cb = this._beforeunloadCallback
  452. try {
  453. cb && (await cb(rest))
  454. actor?.resolve(null)
  455. } catch (e) {
  456. this.logger.error(`[beforeunload] `, e)
  457. actor?.reject(e)
  458. }
  459. })
  460. }
  461. // Life related
  462. async ready(model?: any, callback?: any) {
  463. if (this._connected) return
  464. try {
  465. if (typeof model === 'function') {
  466. callback = model
  467. model = {}
  468. }
  469. let baseInfo = await this._caller.connectToParent(model)
  470. this._connected = true
  471. baseInfo = deepMerge(this._baseInfo, baseInfo)
  472. this._baseInfo = baseInfo
  473. if (baseInfo?.id) {
  474. this._debugTag =
  475. this._caller.debugTag = `#${baseInfo.id} [${baseInfo.name}]`
  476. this.logger.setTag(this._debugTag)
  477. }
  478. if (this._settingsSchema) {
  479. baseInfo.settings = mergeSettingsWithSchema(
  480. baseInfo.settings,
  481. this._settingsSchema
  482. )
  483. // TODO: sync host settings schema
  484. await this.useSettingsSchema(this._settingsSchema)
  485. }
  486. try {
  487. await this._execCallableAPIAsync('setSDKMetadata', {
  488. version: this._version,
  489. })
  490. } catch (e) {
  491. console.warn(e)
  492. }
  493. callback && callback.call(this, baseInfo)
  494. } catch (e) {
  495. console.error(`${this._debugTag} [Ready Error]`, e)
  496. }
  497. }
  498. ensureConnected() {
  499. if (!this._connected) {
  500. throw new Error('not connected')
  501. }
  502. }
  503. beforeunload(callback: (e: any) => Promise<void>): void {
  504. if (typeof callback !== 'function') return
  505. this._beforeunloadCallback = callback
  506. }
  507. provideModel(model: Record<string, any>) {
  508. this.caller._extendUserModel(model)
  509. return this
  510. }
  511. provideTheme(theme: Theme) {
  512. this.caller.call('provider:theme', theme)
  513. return this
  514. }
  515. provideStyle(style: StyleString) {
  516. this.caller.call('provider:style', style)
  517. return this
  518. }
  519. provideUI(ui: UIOptions) {
  520. this.caller.call('provider:ui', ui)
  521. return this
  522. }
  523. // Settings related
  524. useSettingsSchema(schema: Array<SettingSchemaDesc>) {
  525. if (this.connected) {
  526. this.caller.call('settings:schema', {
  527. schema,
  528. isSync: true,
  529. })
  530. }
  531. this._settingsSchema = schema
  532. return this
  533. }
  534. updateSettings(attrs: Record<string, any>) {
  535. this.caller.call('settings:update', attrs)
  536. // TODO: update associated baseInfo settings
  537. }
  538. onSettingsChanged<T = any>(cb: (a: T, b: T) => void): IUserOffHook {
  539. const type = 'settings:changed'
  540. this.on(type, cb)
  541. return () => this.off(type, cb)
  542. }
  543. showSettingsUI() {
  544. this.caller.call('settings:visible:changed', { visible: true })
  545. }
  546. hideSettingsUI() {
  547. this.caller.call('settings:visible:changed', { visible: false })
  548. }
  549. // UI related
  550. setMainUIAttrs(attrs: Partial<UIContainerAttrs>): void {
  551. this.caller.call('main-ui:attrs', attrs)
  552. }
  553. setMainUIInlineStyle(style: CSS.Properties): void {
  554. this.caller.call('main-ui:style', style)
  555. }
  556. hideMainUI(opts?: { restoreEditingCursor: boolean }): void {
  557. const payload = {
  558. key: KEY_MAIN_UI,
  559. visible: false,
  560. cursor: opts?.restoreEditingCursor,
  561. }
  562. this.caller.call('main-ui:visible', payload)
  563. this.emit('ui:visible:changed', payload)
  564. this._ui.set(payload.key, payload)
  565. }
  566. showMainUI(opts?: { autoFocus: boolean }): void {
  567. const payload = {
  568. key: KEY_MAIN_UI,
  569. visible: true,
  570. autoFocus: opts?.autoFocus,
  571. }
  572. this.caller.call('main-ui:visible', payload)
  573. this.emit('ui:visible:changed', payload)
  574. this._ui.set(payload.key, payload)
  575. }
  576. toggleMainUI(): void {
  577. const payload = { key: KEY_MAIN_UI, toggle: true }
  578. const state = this._ui.get(payload.key)
  579. if (state && state.visible) {
  580. this.hideMainUI()
  581. } else {
  582. this.showMainUI()
  583. }
  584. }
  585. // Getters
  586. get version(): string {
  587. return this._version
  588. }
  589. get isMainUIVisible(): boolean {
  590. const state = this._ui.get(KEY_MAIN_UI)
  591. return Boolean(state && state.visible)
  592. }
  593. get connected(): boolean {
  594. return this._connected
  595. }
  596. get baseInfo(): LSPluginBaseInfo {
  597. return this._baseInfo
  598. }
  599. get effect(): Boolean {
  600. return checkEffect(this)
  601. }
  602. get logger() {
  603. return logger
  604. }
  605. get settings() {
  606. return this.baseInfo?.settings
  607. }
  608. get caller(): LSPluginCaller {
  609. return this._caller
  610. }
  611. resolveResourceFullUrl(filePath: string) {
  612. this.ensureConnected()
  613. if (!filePath) return
  614. filePath = filePath.replace(/^[.\\/]+/, '')
  615. return safetyPathJoin(this._baseInfo.lsr, filePath)
  616. }
  617. /**
  618. * @internal
  619. */
  620. _makeUserProxy(target: any, nstag?: UserProxyNSTags) {
  621. const that = this
  622. const caller = this.caller
  623. return new Proxy(target, {
  624. get(target: any, propKey, _receiver) {
  625. const origMethod = target[propKey]
  626. return function (this: any, ...args: any) {
  627. if (origMethod) {
  628. if (args?.length !== 0) args.concat(nstag)
  629. const ret = origMethod.apply(that, args)
  630. if (ret !== PROXY_CONTINUE) return ret
  631. }
  632. // Handle hook
  633. if (nstag) {
  634. const hookMatcher = propKey.toString().match(/^(once|off|on)/i)
  635. if (hookMatcher != null) {
  636. const f = hookMatcher[0].toLowerCase()
  637. const s = hookMatcher.input!
  638. const isOff = f === 'off'
  639. const pid = that.baseInfo.id
  640. let type = s.slice(f.length)
  641. let handler = args[0]
  642. let opts = args[1]
  643. // condition mode
  644. if (typeof handler === 'string' && typeof opts === 'function') {
  645. handler = handler.replace(/^logseq./, ':')
  646. type = `${type}${handler}`
  647. handler = opts
  648. opts = args[2]
  649. }
  650. type = `hook:${nstag}:${safeSnakeCase(type)}`
  651. caller[f](type, handler)
  652. const unlisten = () => {
  653. caller.off(type, handler)
  654. if (!caller.listenerCount(type)) {
  655. that.App._uninstallPluginHook(pid, type)
  656. }
  657. }
  658. if (!isOff) {
  659. that.App._installPluginHook(pid, type, opts)
  660. } else {
  661. unlisten()
  662. return
  663. }
  664. return unlisten
  665. }
  666. }
  667. let method = propKey as string
  668. // TODO: refactor api call with the explicit tag
  669. if ((['git', 'ui', 'assets', 'utils'] as UserProxyNSTags[]).includes(nstag)) {
  670. method = nstag + '_' + method
  671. }
  672. // Call host
  673. return caller.callAsync(`api:call`, {
  674. tag: nstag,
  675. method,
  676. args: args,
  677. })
  678. }
  679. },
  680. })
  681. }
  682. _execCallableAPIAsync(method: callableMethods, ...args) {
  683. return this._caller.callAsync(`api:call`, {
  684. method,
  685. args,
  686. })
  687. }
  688. _execCallableAPI(method: callableMethods, ...args) {
  689. this._caller.call(`api:call`, {
  690. method,
  691. args,
  692. })
  693. }
  694. _callWin(...args) {
  695. return this._execCallableAPIAsync(`_callMainWin`, ...args)
  696. }
  697. // User Proxies
  698. #appProxy: IAppProxy
  699. #editorProxy: IEditorProxy
  700. #dbProxy: IDBProxy
  701. #uiProxy: IUIProxy
  702. #utilsProxy: IUtilsProxy
  703. get App(): IAppProxy {
  704. if (this.#appProxy) return this.#appProxy
  705. return (this.#appProxy = this._makeUserProxy(app, 'app'))
  706. }
  707. get Editor(): IEditorProxy {
  708. if (this.#editorProxy) return this.#editorProxy
  709. return (this.#editorProxy = this._makeUserProxy(editor, 'editor'))
  710. }
  711. get DB(): IDBProxy {
  712. if (this.#dbProxy) return this.#dbProxy
  713. return (this.#dbProxy = this._makeUserProxy(db, 'db'))
  714. }
  715. get UI(): IUIProxy {
  716. if (this.#uiProxy) return this.#uiProxy
  717. return (this.#uiProxy = this._makeUserProxy(ui, 'ui'))
  718. }
  719. get Utils(): IUtilsProxy {
  720. if (this.#utilsProxy) return this.#utilsProxy
  721. return (this.#utilsProxy = this._makeUserProxy(utils, 'utils'))
  722. }
  723. get Git(): IGitProxy {
  724. return this._makeUserProxy(git, 'git')
  725. }
  726. get Assets(): IAssetsProxy {
  727. return this._makeUserProxy(assets, 'assets')
  728. }
  729. get FileStorage(): LSPluginFileStorage {
  730. let m = this._mFileStorage
  731. if (!m) m = this._mFileStorage = new LSPluginFileStorage(this)
  732. return m
  733. }
  734. get Request(): LSPluginRequest {
  735. let m = this._mRequest
  736. if (!m) m = this._mRequest = new LSPluginRequest(this)
  737. return m
  738. }
  739. get Experiments(): LSPluginExperiments {
  740. let m = this._mExperiments
  741. if (!m) m = this._mExperiments = new LSPluginExperiments(this)
  742. return m
  743. }
  744. }
  745. export * from './LSPlugin'
  746. /**
  747. * @internal
  748. */
  749. export function setupPluginUserInstance(
  750. pluginBaseInfo: LSPluginBaseInfo,
  751. pluginCaller: LSPluginCaller
  752. ) {
  753. return new LSPluginUser(pluginBaseInfo, pluginCaller)
  754. }
  755. // entry of iframe mode
  756. if (window.__LSP__HOST__ == null) {
  757. const caller = new LSPluginCaller(null)
  758. window.logseq = setupPluginUserInstance({} as any, caller)
  759. }