LSPlugin.user.ts 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908
  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. runtime: 'js',
  490. })
  491. } catch (e) {
  492. console.warn(e)
  493. }
  494. callback && callback.call(this, baseInfo)
  495. } catch (e) {
  496. console.error(`${this._debugTag} [Ready Error]`, e)
  497. }
  498. }
  499. ensureConnected() {
  500. if (!this._connected) {
  501. throw new Error('not connected')
  502. }
  503. }
  504. beforeunload(callback: (e: any) => Promise<void>): void {
  505. if (typeof callback !== 'function') return
  506. this._beforeunloadCallback = callback
  507. }
  508. provideModel(model: Record<string, any>) {
  509. this.caller._extendUserModel(model)
  510. return this
  511. }
  512. provideTheme(theme: Theme) {
  513. this.caller.call('provider:theme', theme)
  514. return this
  515. }
  516. provideStyle(style: StyleString) {
  517. this.caller.call('provider:style', style)
  518. return this
  519. }
  520. provideUI(ui: UIOptions) {
  521. this.caller.call('provider:ui', ui)
  522. return this
  523. }
  524. // Settings related
  525. useSettingsSchema(schema: Array<SettingSchemaDesc>) {
  526. if (this.connected) {
  527. this.caller.call('settings:schema', {
  528. schema,
  529. isSync: true,
  530. })
  531. }
  532. this._settingsSchema = schema
  533. return this
  534. }
  535. updateSettings(attrs: Record<string, any>) {
  536. this.caller.call('settings:update', attrs)
  537. // TODO: update associated baseInfo settings
  538. }
  539. onSettingsChanged<T = any>(cb: (a: T, b: T) => void): IUserOffHook {
  540. const type = 'settings:changed'
  541. this.on(type, cb)
  542. return () => this.off(type, cb)
  543. }
  544. showSettingsUI() {
  545. this.caller.call('settings:visible:changed', { visible: true })
  546. }
  547. hideSettingsUI() {
  548. this.caller.call('settings:visible:changed', { visible: false })
  549. }
  550. // UI related
  551. setMainUIAttrs(attrs: Partial<UIContainerAttrs>): void {
  552. this.caller.call('main-ui:attrs', attrs)
  553. }
  554. setMainUIInlineStyle(style: CSS.Properties): void {
  555. this.caller.call('main-ui:style', style)
  556. }
  557. hideMainUI(opts?: { restoreEditingCursor: boolean }): void {
  558. const payload = {
  559. key: KEY_MAIN_UI,
  560. visible: false,
  561. cursor: opts?.restoreEditingCursor,
  562. }
  563. this.caller.call('main-ui:visible', payload)
  564. this.emit('ui:visible:changed', payload)
  565. this._ui.set(payload.key, payload)
  566. }
  567. showMainUI(opts?: { autoFocus: boolean }): void {
  568. const payload = {
  569. key: KEY_MAIN_UI,
  570. visible: true,
  571. autoFocus: opts?.autoFocus,
  572. }
  573. this.caller.call('main-ui:visible', payload)
  574. this.emit('ui:visible:changed', payload)
  575. this._ui.set(payload.key, payload)
  576. }
  577. toggleMainUI(): void {
  578. const payload = { key: KEY_MAIN_UI, toggle: true }
  579. const state = this._ui.get(payload.key)
  580. if (state && state.visible) {
  581. this.hideMainUI()
  582. } else {
  583. this.showMainUI()
  584. }
  585. }
  586. // Getters
  587. get version(): string {
  588. return this._version
  589. }
  590. get isMainUIVisible(): boolean {
  591. const state = this._ui.get(KEY_MAIN_UI)
  592. return Boolean(state && state.visible)
  593. }
  594. get connected(): boolean {
  595. return this._connected
  596. }
  597. get baseInfo(): LSPluginBaseInfo {
  598. return this._baseInfo
  599. }
  600. get effect(): Boolean {
  601. return checkEffect(this)
  602. }
  603. get logger() {
  604. return logger
  605. }
  606. get settings() {
  607. return this.baseInfo?.settings
  608. }
  609. get caller(): LSPluginCaller {
  610. return this._caller
  611. }
  612. resolveResourceFullUrl(filePath: string) {
  613. this.ensureConnected()
  614. if (!filePath) return
  615. filePath = filePath.replace(/^[.\\/]+/, '')
  616. return safetyPathJoin(this._baseInfo.lsr, filePath)
  617. }
  618. /**
  619. * @internal
  620. */
  621. _makeUserProxy(target: any, nstag?: UserProxyNSTags) {
  622. const that = this
  623. const caller = this.caller
  624. return new Proxy(target, {
  625. get(target: any, propKey, _receiver) {
  626. const origMethod = target[propKey]
  627. return function (this: any, ...args: any) {
  628. if (origMethod) {
  629. if (args?.length !== 0) args.concat(nstag)
  630. const ret = origMethod.apply(that, args)
  631. if (ret !== PROXY_CONTINUE) return ret
  632. }
  633. // Handle hook
  634. if (nstag) {
  635. const hookMatcher = propKey.toString().match(/^(once|off|on)/i)
  636. if (hookMatcher != null) {
  637. const f = hookMatcher[0].toLowerCase()
  638. const s = hookMatcher.input!
  639. const isOff = f === 'off'
  640. const pid = that.baseInfo.id
  641. let type = s.slice(f.length)
  642. let handler = args[0]
  643. let opts = args[1]
  644. // condition mode
  645. if (typeof handler === 'string' && typeof opts === 'function') {
  646. handler = handler.replace(/^logseq./, ':')
  647. type = `${type}${handler}`
  648. handler = opts
  649. opts = args[2]
  650. }
  651. type = `hook:${nstag}:${safeSnakeCase(type)}`
  652. caller[f](type, handler)
  653. const unlisten = () => {
  654. caller.off(type, handler)
  655. if (!caller.listenerCount(type)) {
  656. that.App._uninstallPluginHook(pid, type)
  657. }
  658. }
  659. if (!isOff) {
  660. that.App._installPluginHook(pid, type, opts)
  661. } else {
  662. unlisten()
  663. return
  664. }
  665. return unlisten
  666. }
  667. }
  668. let method = propKey as string
  669. // TODO: refactor api call with the explicit tag
  670. if ((['git', 'ui', 'assets', 'utils'] as UserProxyNSTags[]).includes(nstag)) {
  671. method = nstag + '_' + method
  672. }
  673. // Call host
  674. return caller.callAsync(`api:call`, {
  675. tag: nstag,
  676. method,
  677. args: args,
  678. })
  679. }
  680. },
  681. })
  682. }
  683. _execCallableAPIAsync(method: callableMethods, ...args) {
  684. return this._caller.callAsync(`api:call`, {
  685. method,
  686. args,
  687. })
  688. }
  689. _execCallableAPI(method: callableMethods, ...args) {
  690. this._caller.call(`api:call`, {
  691. method,
  692. args,
  693. })
  694. }
  695. _callWin(...args) {
  696. return this._execCallableAPIAsync(`_callMainWin`, ...args)
  697. }
  698. // User Proxies
  699. #appProxy: IAppProxy
  700. #editorProxy: IEditorProxy
  701. #dbProxy: IDBProxy
  702. #uiProxy: IUIProxy
  703. #utilsProxy: IUtilsProxy
  704. get App(): IAppProxy {
  705. if (this.#appProxy) return this.#appProxy
  706. return (this.#appProxy = this._makeUserProxy(app, 'app'))
  707. }
  708. get Editor(): IEditorProxy {
  709. if (this.#editorProxy) return this.#editorProxy
  710. return (this.#editorProxy = this._makeUserProxy(editor, 'editor'))
  711. }
  712. get DB(): IDBProxy {
  713. if (this.#dbProxy) return this.#dbProxy
  714. return (this.#dbProxy = this._makeUserProxy(db, 'db'))
  715. }
  716. get UI(): IUIProxy {
  717. if (this.#uiProxy) return this.#uiProxy
  718. return (this.#uiProxy = this._makeUserProxy(ui, 'ui'))
  719. }
  720. get Utils(): IUtilsProxy {
  721. if (this.#utilsProxy) return this.#utilsProxy
  722. return (this.#utilsProxy = this._makeUserProxy(utils, 'utils'))
  723. }
  724. get Git(): IGitProxy {
  725. return this._makeUserProxy(git, 'git')
  726. }
  727. get Assets(): IAssetsProxy {
  728. return this._makeUserProxy(assets, 'assets')
  729. }
  730. get FileStorage(): LSPluginFileStorage {
  731. let m = this._mFileStorage
  732. if (!m) m = this._mFileStorage = new LSPluginFileStorage(this)
  733. return m
  734. }
  735. get Request(): LSPluginRequest {
  736. let m = this._mRequest
  737. if (!m) m = this._mRequest = new LSPluginRequest(this)
  738. return m
  739. }
  740. get Experiments(): LSPluginExperiments {
  741. let m = this._mExperiments
  742. if (!m) m = this._mExperiments = new LSPluginExperiments(this)
  743. return m
  744. }
  745. }
  746. export * from './LSPlugin'
  747. /**
  748. * @internal
  749. */
  750. export function setupPluginUserInstance(
  751. pluginBaseInfo: LSPluginBaseInfo,
  752. pluginCaller: LSPluginCaller
  753. ) {
  754. return new LSPluginUser(pluginBaseInfo, pluginCaller)
  755. }
  756. // entry of iframe mode
  757. if (window.__LSP__HOST__ == null) {
  758. const caller = new LSPluginCaller(null)
  759. window.logseq = setupPluginUserInstance({} as any, caller)
  760. }