Explorar el Código

Enhance / Plugin APIs (#6945)

Added
1. All configurations of current graph.
App.getCurrentGraphConfigs: () => Promise<any>
2. All favorite pages list of current graph.
App.getCurrentGraphFavorites: () => Promise<Array<string> | null>
3. All recent pages list of current graph.
App.getCurrentGraphRecent: () => Promise<Array<string> | null>
4. Clear right sidebar blocks.
App.clearRightSidebarBlocks: (opts?: { close: boolean }) => void
5. Support register CodeMirror enhancer. #Experiment feature
Experiments.registerExtensionsEnhancer<T = any>(type: 'katex' | 'codemirror', enhancer: (v: T) => Promise<any>)
6. Support hooks for app search service. #Alpha stage
App.registerSearchService<T extends IPluginSearchServiceHooks>(s: T): void
7. Support focus option for App.insertBlock. Credit to [[tennox]] #PR

Fixed
1. Adjust build script to be compatible for shadow-cljs bundler.
How to set up a clojurescript project with shadow-cljs?
https://github.com/rlhk/logseq-url-plus/blob/main/doc/dev-notes.md
Charlie hace 2 años
padre
commit
dda1f9bd9f

+ 1 - 3
e2e-tests/random.spec.ts

@@ -1,7 +1,7 @@
 import { expect } from '@playwright/test'
 import { test } from './fixtures'
 import {
-  createRandomPage, randomInt, randomInsert, randomEditDelete, randomEditMoveUpDown, IsMac, randomString,
+  createRandomPage, randomInt, IsMac, randomString,
 } from './utils'
 
 /**
@@ -175,7 +175,5 @@ test('Random editor operations', async ({ page, block }) => {
 
     // FIXME: CHECK await block.waitForBlocks(expectedBlocks)
     await page.waitForTimeout(50)
-
   }
-
 })

+ 1 - 1
e2e-tests/utils.ts

@@ -2,7 +2,7 @@ import { Page, Locator } from 'playwright'
 import { expect, ConsoleMessage } from '@playwright/test'
 import * as process from 'process'
 import { Block } from './types'
-import pathlib from 'path'
+import * as pathlib from 'path'
 
 export const IsMac = process.platform === 'darwin'
 export const IsLinux = process.platform === 'linux'

+ 1 - 0
libs/.npmignore

@@ -1,3 +1,4 @@
 src/
 webpack.*
 .DS_Store
+docs/

+ 30 - 0
libs/CHANGELOG.md

@@ -0,0 +1,30 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+## [Unreleased]
+
+## [0.0.11]
+
+### Added
+
+- All configurations of current graph.
+  `App.getCurrentGraphConfigs: () => Promise<any>`
+- All favorite pages list of current graph.
+  `App.getCurrentGraphFavorites: () => Promise<Array<string> | null>`
+- All recent pages list of current graph.
+  `App.getCurrentGraphRecent: () => Promise<Array<string> | null>`
+- Clear right sidebar blocks.
+  `App.clearRightSidebarBlocks: (opts?: { close: boolean }) => void`
+- Support register `CodeMirror` enhancer. _#Experiment feature_
+  `Experiments.registerExtensionsEnhancer<T = any>(type: 'katex' | 'codemirror', enhancer: (v: T) => Promise<any>)`
+- Support hooks for app search service. _#Alpha stage_
+  `App.registerSearchService<T extends IPluginSearchServiceHooks>(s: T): void`
+- Support `focus` option for `App.insertBlock`. Credit
+  to [[[tennox](https://github.com/tennox)]] [#PR](https://github.com/logseq/logseq/commit/4217057a44de65e5c64be37857af2fb4e9534b24)
+
+### Fixed
+
+- Adjust build script to be compatible for `shadow-cljs` bundler.
+  > How to set up a clojurescript project with shadow-cljs?
+  > https://github.com/rlhk/logseq-url-plus/blob/main/doc/dev-notes.md

+ 10 - 0
libs/babel.config.json

@@ -0,0 +1,10 @@
+{
+  "presets": [
+    [
+      "@babel/preset-env",
+      {
+        "targets": "chrome 72"
+      }
+    ]
+  ]
+}

+ 9 - 2
libs/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@logseq/libs",
-  "version": "0.0.10",
+  "version": "0.0.11",
   "description": "Logseq SDK libraries",
   "main": "dist/lsplugin.user.js",
   "typings": "index.d.ts",
@@ -12,7 +12,8 @@
     "dev:core": "npm run build:core -- --mode development --watch",
     "build": "tsc && rm dist/*.js && npm run build:user",
     "lint": "prettier --check \"src/**/*.{ts, js}\"",
-    "fix": "prettier --write \"src/**/*.{ts, js}\""
+    "fix": "prettier --write \"src/**/*.{ts, js}\"",
+    "build:docs": "typedoc --plugin typedoc-plugin-lsp-docs src/LSPlugin.user.ts && typedoc --json docs/out.json ./src/LSPlugin.user.ts"
   },
   "dependencies": {
     "csstype": "3.1.0",
@@ -25,12 +26,18 @@
     "snake-case": "3.0.4"
   },
   "devDependencies": {
+    "@babel/core": "^7.20.2",
+    "@babel/preset-env": "^7.20.2",
     "@types/debug": "^4.1.5",
     "@types/dompurify": "2.3.3",
     "@types/lodash-es": "4.17.6",
+    "babel-loader": "^9.1.0",
     "prettier": "^2.6.2",
     "prettier-config-standard": "^5.0.0",
+    "terser-webpack-plugin": "^5.3.6",
     "ts-loader": "9.3.0",
+    "typedoc": "^0.23.17",
+    "typedoc-plugin-lsp-docs": "^0.0.1",
     "typescript": "4.7.3",
     "webpack": "5.73.0",
     "webpack-bundle-analyzer": "4.5.0",

+ 1 - 0
libs/src/LSPlugin.caller.ts

@@ -279,6 +279,7 @@ class LSPluginCaller extends EventEmitter {
             debug(`[user -> *host] `, type, payload)
 
             this._pluginLocal?.emit(type, payload || {})
+            this._pluginLocal?.caller.emit(type, payload || {})
           })
 
           this._call = async (...args: any) => {

+ 13 - 4
libs/src/LSPlugin.core.ts

@@ -139,7 +139,12 @@ class PluginLogger extends EventEmitter<'change'> {
     super()
   }
 
-  write(type: string, payload: any[]) {
+  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}`
@@ -150,6 +155,11 @@ class PluginLogger extends EventEmitter<'change'> {
     }, `[${this._tag}][${new Date().toLocaleTimeString()}] `)
 
     this._logs.push([type, msg])
+
+    if (inConsole) {
+      console?.['ERROR' === type ? 'error' : 'debug'](`${type}: ${msg}`)
+    }
+
     this.emit('change')
   }
 
@@ -907,9 +917,9 @@ class PluginLocal extends EventEmitter<'loaded'
 
       this._dispose(cleanInjectedScripts.bind(this))
     } catch (e) {
-      console.error('[Load Plugin Error] ', e)
-      this.logger?.error(e)
+      this.logger?.error('[Load Plugin]', e, true)
 
+      this.dispose().catch(null)
       this._status = PluginLocalLoadStatus.ERROR
       this._loadErr = e
     } finally {
@@ -1329,7 +1339,6 @@ class LSPluginCore
           }
         }
 
-
         pluginLocal.settings?.on('change', (a) => {
           this.emit('settings-changed', pluginLocal.id, a)
           pluginLocal.caller?.callUserModel(LSPMSG_SETTINGS, { payload: a })

+ 38 - 2
libs/src/LSPlugin.ts

@@ -289,6 +289,34 @@ export type ExternalCommandType =
 
 export type UserProxyTags = 'app' | 'editor' | 'db' | 'git' | 'ui' | 'assets'
 
+export type SearchIndiceInitStatus = boolean
+export type SearchBlockItem = { id: EntityID, uuid: BlockIdentity, content: string, page: EntityID }
+export type SearchPageItem = string
+export type SearchFileItem = string
+
+export interface IPluginSearchServiceHooks {
+  name: string
+  options?: Record<string, any>
+
+  onQuery: (
+    graph: string,
+    key: string,
+    opts: Partial<{ limit: number }>
+  ) =>
+    Promise<{
+      graph: string,
+      key: string,
+      blocks?: Array<Partial<SearchBlockItem>>,
+      pages?: Array<SearchPageItem>,
+      files?: Array<SearchFileItem>
+    }>
+
+  onIndiceInit: (graph: string) => Promise<SearchIndiceInitStatus>
+  onIndiceReset: (graph: string) => Promise<void>
+  onBlocksChanged: (graph: string, changes: { added: Array<SearchBlockItem>, removed: Array<BlockEntity> }) => Promise<void>
+  onGraphRemoved: (graph: string, opts?: {}) => Promise<any>
+}
+
 /**
  * App level APIs
  */
@@ -302,6 +330,9 @@ export interface IAppProxy {
   getUserInfo: () => Promise<AppUserInfo | null>
   getUserConfigs: () => Promise<AppUserConfigs>
 
+  // services
+  registerSearchService<T extends IPluginSearchServiceHooks>(s: T): void
+
   // commands
   registerCommand: (
     type: string,
@@ -352,6 +383,7 @@ export interface IAppProxy {
    * @param path
    */
   getStateFromStore: <T = any>(path: string | Array<string>) => Promise<T>
+  setStateFromStore: (path: string | Array<string>, value: any) => Promise<void>
 
   // native
   relaunch: () => Promise<void>
@@ -367,6 +399,9 @@ export interface IAppProxy {
 
   // graph
   getCurrentGraph: () => Promise<AppGraphInfo | null>
+  getCurrentGraphConfigs: () => Promise<any>
+  getCurrentGraphFavorites: () => Promise<Array<string> | null>
+  getCurrentGraphRecent: () => Promise<Array<string> | null>
 
   // router
   pushState: (
@@ -403,6 +438,7 @@ export interface IAppProxy {
   setFullScreen: (flag: boolean | 'toggle') => void
   setLeftSidebarVisible: (flag: boolean | 'toggle') => void
   setRightSidebarVisible: (flag: boolean | 'toggle') => void
+  clearRightSidebarBlocks: (opts?: { close: boolean }) => void
 
   registerUIItem: (
     type: 'toolbar' | 'pagebar',
@@ -805,14 +841,14 @@ export interface IAssetsProxy {
    * @added 0.0.2
    * @param exts
    */
-  listFilesOfCurrentGraph(exts?: string | string[]): Promise<{
+  listFilesOfCurrentGraph(exts?: string | string[]): Promise<Array<{
     path: string
     size: number
     accessTime: number
     modifiedTime: number
     changeTime: number
     birthTime: number
-  }>
+  }>>
 
   /**
    * @example https://github.com/logseq/logseq/pull/6488

+ 25 - 3
libs/src/LSPlugin.user.ts

@@ -33,7 +33,7 @@ import {
   BlockEntity,
   IDatom,
   IAssetsProxy,
-  AppInfo,
+  AppInfo, IPluginSearchServiceHooks,
 } from './LSPlugin'
 import Debug from 'debug'
 import * as CSS from 'csstype'
@@ -41,6 +41,7 @@ import EventEmitter from 'eventemitter3'
 import { IAsyncStorage, LSPluginFileStorage } from './modules/LSPlugin.Storage'
 import { LSPluginExperiments } from './modules/LSPlugin.Experiments'
 import { LSPluginRequest } from './modules/LSPlugin.Request'
+import { LSPluginSearchService } from './modules/LSPlugin.Search'
 
 declare global {
   interface Window {
@@ -86,13 +87,15 @@ function registerSimpleCommand(
     method: 'register-plugin-simple-command',
     args: [
       this.baseInfo.id,
-      [{ key, label, type, desc, keybinding, extras}, ['editor/hook', eventKey]],
+      [{ key, label, type, desc, keybinding, extras }, ['editor/hook', eventKey]],
       palette,
     ],
   })
 }
 
 let _appBaseInfo: AppInfo = null
+let _searchServices: Map<string, LSPluginSearchService> = new Map()
+
 const app: Partial<IAppProxy> = {
   async getInfo(
     this: LSPluginUser,
@@ -106,6 +109,17 @@ const app: Partial<IAppProxy> = {
 
   registerCommand: registerSimpleCommand,
 
+  registerSearchService<T extends IPluginSearchServiceHooks>(
+    this: LSPluginUser,
+    s: T
+  ) {
+    if (_searchServices.has(s.name)) {
+      throw new Error(`SearchService: #${s.name} has registered!`)
+    }
+
+    _searchServices.set(s.name, new LSPluginSearchService(this, s))
+  },
+
   registerCommandPalette(
     opts: { key: string; label: string; keybinding?: SimpleCommandKeybinding },
     action: SimpleCommandCallback
@@ -356,7 +370,11 @@ type uiState = {
 const KEY_MAIN_UI = 0
 
 /**
- * User plugin instance
+ * User plugin instance from global namespace `logseq`.
+ * @example
+ * ```ts
+ * logseq.UI.showMsg('Hello, Logseq')
+ * ```
  * @public
  */
 export class LSPluginUser
@@ -420,6 +438,7 @@ export class LSPluginUser
     })
   }
 
+  // Life related
   async ready(model?: any, callback?: any) {
     if (this._connected) return
 
@@ -495,6 +514,7 @@ export class LSPluginUser
     return this
   }
 
+  // Settings related
   useSettingsSchema(schema: Array<SettingSchemaDesc>) {
     if (this.connected) {
       this.caller.call('settings:schema', {
@@ -526,6 +546,7 @@ export class LSPluginUser
     this.caller.call('settings:visible:changed', { visible: false })
   }
 
+  // UI related
   setMainUIAttrs(attrs: Partial<UIContainerAttrs>): void {
     this.caller.call('main-ui:attrs', attrs)
   }
@@ -566,6 +587,7 @@ export class LSPluginUser
     }
   }
 
+  // Getters
   get version(): string {
     return this._version
   }

+ 1 - 1
libs/src/modules/LSPlugin.Experiments.ts

@@ -60,7 +60,7 @@ export class LSPluginExperiments {
   }
 
   registerExtensionsEnhancer<T = any>(
-    type: 'katex',
+    type: 'katex' | 'codemirror',
     enhancer: (v: T) => Promise<any>
   ) {
     const host = this.ensureHostScope()

+ 82 - 0
libs/src/modules/LSPlugin.Search.ts

@@ -0,0 +1,82 @@
+import { IPluginSearchServiceHooks } from '../LSPlugin'
+import { LSPluginUser } from '../LSPlugin.user'
+import { isArray, isFunction, mapKeys } from 'lodash-es'
+
+export class LSPluginSearchService {
+
+  /**
+   * @param ctx
+   * @param serviceHooks
+   */
+  constructor(
+    private ctx: LSPluginUser,
+    private serviceHooks: IPluginSearchServiceHooks
+  ) {
+    ctx._execCallableAPI(
+      'register-search-service',
+      ctx.baseInfo.id,
+      serviceHooks.name,
+      serviceHooks.options
+    )
+
+    // hook events TODO: remove listeners
+    const wrapHookEvent = (k) => `service:search:${k}:${serviceHooks.name}`
+
+    Object.entries(
+      {
+        query: {
+          f: 'onQuery', args: ['graph', 'q', true], reply: true,
+          transformOutput: (data: any) => {
+            // TODO: transform keys?
+            if (isArray(data?.blocks)) {
+              data.blocks = data.blocks.map(it => {
+                return it && mapKeys(it, (_, k) => `block/${k}`)
+              })
+            }
+
+            return data
+          }
+        },
+        rebuildBlocksIndice: { f: 'onIndiceInit', args: ['graph', 'blocks'] },
+        transactBlocks: { f: 'onBlocksChanged', args: ['graph', 'data'] },
+        truncateBlocks: { f: 'onIndiceReset', args: ['graph'] },
+        removeDb: { f: 'onGraph', args: ['graph'] }
+      }
+    ).forEach(
+      ([k, v]) => {
+        const hookEvent = wrapHookEvent(k)
+        ctx.caller.on(hookEvent, async (payload: any) => {
+          if (isFunction(serviceHooks?.[v.f])) {
+            let ret = null
+
+            try {
+              ret = await serviceHooks[v.f].apply(
+                serviceHooks, (v.args || []).map((prop: any) => {
+                  if (!payload) return
+                  if (prop === true) return payload
+                  if (payload.hasOwnProperty(prop)) {
+                    const ret = payload[prop]
+                    delete payload[prop]
+                    return ret
+                  }
+                })
+              )
+
+              if (v.transformOutput) {
+                ret = v.transformOutput(ret)
+              }
+            } catch (e) {
+              console.error('[SearchService] ', e)
+              ret = e
+            } finally {
+              if (v.reply) {
+                ctx.caller.call(
+                  `${hookEvent}:reply`, ret
+                )
+              }
+            }
+          }
+        })
+      })
+  }
+}

+ 16 - 2
libs/webpack.config.js

@@ -2,6 +2,7 @@ const pkg = require('./package.json')
 const path = require('path')
 const webpack = require('webpack')
 const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
+const TerserPlugin = require('terser-webpack-plugin')
 
 module.exports = {
   entry: './src/LSPlugin.user.ts',
@@ -9,14 +10,27 @@ module.exports = {
     rules: [
       {
         test: /\.tsx?$/,
-        use: 'ts-loader',
+        use: [
+          {
+            loader: 'babel-loader'
+          },
+          {
+            loader: 'ts-loader'
+          }
+        ],
         exclude: /node_modules/,
-      },
+      }
     ],
   },
   resolve: {
     extensions: ['.tsx', '.ts', '.js'],
   },
+  optimization: {
+    minimize: true,
+    minimizer: [
+      new TerserPlugin()
+    ]
+  },
   plugins: [
     new webpack.ProvidePlugin({
       process: 'process/browser',

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 956 - 4
libs/yarn.lock


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
resources/js/lsplugin.core.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
resources/js/lsplugin.user.js


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

@@ -39,9 +39,9 @@
            api #(str "https://api.github.com/repos/" repo "/" %)
            endpoint (api url-suffix)
            ^js res (fetch endpoint)
+           _ (debug "[Release URL] " endpoint "[Response Status/Text]" (.-status res) "-")
            res (response-transform res)
            res (.json res)
-           _ (debug "[Release URL] " endpoint)
            res (bean/->clj res)
            version (:tag_name res)
            asset (first (filter #(string/ends-with? (:name %) ".zip") (:assets res)))]

+ 24 - 10
src/main/frontend/components/lazy_editor.cljs

@@ -3,7 +3,10 @@
             [rum.core :as rum]
             [shadow.lazy :as lazy]
             [frontend.ui :as ui]
-            [frontend.state :as state]))
+            [frontend.config :as config]
+            [frontend.state :as state]
+            [frontend.handler.plugin :refer [hook-extensions-enhancer-by-type]]
+            [promesa.core :as p]))
 
 ;; TODO: Why does shadow fail when code is required
 #_:clj-kondo/ignore
@@ -11,17 +14,28 @@
 
 (defonce loaded? (atom false))
 
-(rum/defc editor < rum/reactive
-  {:will-mount (fn [state]
-                 (lazy/load lazy-editor
-                            (fn []
-                              (reset! loaded? true)))
-                 state)}
+(rum/defc editor <
+  rum/reactive
+  {:will-mount
+   (fn [state]
+     (lazy/load lazy-editor
+                (fn []
+                  (if-not @loaded?
+                    (p/finally
+                     (p/all (when-let [enhancers (and config/lsp-enabled?
+                                                      (seq (hook-extensions-enhancer-by-type :codemirror)))]
+                              (for [{f :enhancer} enhancers]
+                                (when (fn? f) (f (. js/window -CodeMirror))))))
+                     (fn []
+                       (-> (p/delay 200)
+                           (p/then #(reset! loaded? true)))))
+                    (reset! loaded? true))))
+     state)}
   [config id attr code options]
   (let [loaded? (rum/react loaded?)
-        theme (state/sub :ui/theme)
-        code (or code "")
-        code (string/replace-first code #"\n$" "")] ;; See-also: #3410
+        theme   (state/sub :ui/theme)
+        code    (or code "")
+        code    (string/replace-first code #"\n$" "")]      ;; See-also: #3410
     (if loaded?
       (@lazy-editor config id attr code theme options)
       (ui/loading "CodeMirror"))))

+ 53 - 10
src/main/frontend/components/search.cljs

@@ -231,25 +231,33 @@
                            (highlight-exact-query data search-q))
 
        :block
-       (let [{:block/keys [page uuid]} data  ;; content here is normalized
+       (let [{:block/keys [page uuid content]} data  ;; content here is normalized
              page (util/get-page-original-name page)
              repo (state/sub :git/current-repo)
              format (db/get-page-format page)
-             block (model/query-block-by-uuid uuid)
-             content (:block/content block)]
+             block (when-not (string/blank? uuid)
+                     (model/query-block-by-uuid uuid))
+             content' (if block (:block/content block) content)]
          [:span {:data-block-ref uuid}
           (search-result-item {:name "block"
                                :title (t :search-item/block)
                                :extension? true}
-                              (if block
-                                (block-search-result-item repo uuid format content search-q search-mode)
+
+                              (cond
+                                (some? block)
+                                (block-search-result-item repo uuid format content' search-q search-mode)
+
+                                (not (string/blank? content'))
+                                content'
+
+                                :else
                                 (do (log/error "search result with non-existing uuid: " data)
                                     (str "Cache is outdated. Please click the 'Re-index' button in the graph's dropdown menu."))))])
 
        nil)]))
 
 (rum/defc search-auto-complete
-  [{:keys [pages files blocks has-more?] :as result} search-q all?]
+  [{:keys [engine pages files blocks has-more?] :as result} search-q all?]
   (let [pages (when-not all? (map (fn [page]
                                     (let [alias (model/get-redirect-page-name page)]
                                       (cond->
@@ -264,6 +272,7 @@
         blocks (map (fn [block] {:type :block :data block}) blocks)
         search-mode (state/sub :search/mode)
         new-page (if (or
+                      (some? engine)
                       (and (seq pages)
                            (= (util/safe-page-name-sanity-lc search-q)
                               (util/safe-page-name-sanity-lc (:data (first pages)))))
@@ -401,10 +410,13 @@
       state
       :on-hide (fn []
                  (search-handler/clear-search!)))))
+  (rum/local nil ::active-engine-tab)
   [state]
   (let [search-result (state/sub :search/result)
         search-q (state/sub :search/q)
         search-mode (state/sub :search/mode)
+        engines (state/sub :search/engines)
+        *active-engine-tab (::active-engine-tab state)
         timeout 300
         in-page-search? (= search-mode :page)]
     [:div.cp__palette.cp__palette-main
@@ -421,7 +433,12 @@
                          (default-placeholder search-mode))
         :auto-complete (if (util/chrome?) "chrome-off" "off") ; off not working here
         :value         search-q
-        :on-change     (fn [e]
+        :on-key-down   (fn [^js e]
+                         (when (= 27 (.-keyCode e))
+                           (when-not (string/blank? search-q)
+                             (util/stop e)
+                             (search-handler/clear-search!))))
+        :on-change     (fn [^js e]
                          (when @search-timeout
                            (js/clearTimeout @search-timeout))
                          (let [value (util/evalue e)
@@ -443,9 +460,35 @@
                                             (search-handler/search (state/get-current-repo) value)))
                                         timeout))))))}]]
       [:div.search-results-wrap
-       (if (seq search-result)
-         (search-auto-complete search-result search-q false)
-         (recent-search-and-pages in-page-search?))]]]))
+        ;; list registered search engines
+       (when (seq engines)
+         [:ul.search-results-engines-tabs
+          [:li
+           {:class (when-not @*active-engine-tab "is-active")}
+           (ui/button
+            [:span.flex.items-center
+             (svg/logo 14) [:span.pl-2 "Default"]]
+            :background "orange"
+            :on-click #(reset! *active-engine-tab nil))]
+
+          (for [[k v] engines]
+            [:li
+             {:key k
+              :class (if (= k @*active-engine-tab) "is-active" "")}
+             (ui/button [:span.flex.items-center
+                         [:span.pr-2 (ui/icon "puzzle")]
+                         (:name v)
+                         (when-let [result (and v (:result v))]
+                           (str " (" (count (:blocks result)) ")"))]
+                        :on-click #(reset! *active-engine-tab k))])])
+
+       (if-not (nil? @*active-engine-tab)
+         (let [active-engine-result (get-in engines [@*active-engine-tab :result])]
+           (search-auto-complete
+            (merge active-engine-result {:engine @*active-engine-tab}) search-q false))
+         (if (seq search-result)
+           (search-auto-complete search-result search-q false)
+           (recent-search-and-pages in-page-search?)))]]]))
 
 (rum/defc more < rum/reactive
   [route]

+ 32 - 8
src/main/frontend/components/search.css

@@ -12,21 +12,45 @@
 }
 
 #search-wrapper svg {
-    color: var(--ls-search-icon-color, #9fa6b2);
-    opacity: 0.6;
-    transition: .3s;
+  color: var(--ls-search-icon-color, #9fa6b2);
+  opacity: 0.6;
+  transition: .3s;
 }
 
 #search-wrapper:hover svg, #search-wrapper:focus-within svg {
-    color: var(--ls-link-text-hover-color, #4b5563);
-    opacity: 0.8;
+  color: var(--ls-link-text-hover-color, #4b5563);
+  opacity: 0.8;
 }
 
 #search-wrapper {
-    transition: .3s;
-    padding-right: 12px;
+  transition: .3s;
+  padding-right: 12px;
 }
 
 .search-item svg {
-    transform: scale(0.8);
+  transform: scale(0.8);
 }
+
+.search-results-engines {
+  &-tabs {
+    @apply flex list-none -mx-2 mb-2;
+
+    background: var(--ls-primary-background-color);
+
+    > li {
+      @apply p-0 m-0 min-w-[150px] flex-1;
+
+      .ui__button {
+        @apply w-full h-full justify-center border;
+
+        background: transparent;
+        border-radius: 0;
+        line-height: 1;
+      }
+
+      &.is-active .ui__button {
+        background: var(--ls-quaternary-background-color);
+      }
+    }
+  }
+}

+ 20 - 19
src/main/frontend/components/svg.cljs

@@ -166,25 +166,26 @@
     {:d         "M0 384.662V127.338c0-17.818 21.543-26.741 34.142-14.142l128.662 128.662c7.81 7.81 7.81 20.474 0 28.284L34.142 398.804C21.543 411.404 0 402.48 0 384.662z"
      :fill-rule "evenodd"}]])
 
-(rum/defc logo
-  [_dark?]
-  [:svg
-   {:fill "currentColor", :view-box "0 0 21 21", :height "21", :width "21"}
-   [:ellipse
-    {:transform
-         "matrix(0.987073 0.160274 -0.239143 0.970984 11.7346 2.59206)"
-     :ry "2.04373"
-     :rx "3.29236"}]
-   [:ellipse
-    {:transform
-         "matrix(-0.495846 0.868411 -0.825718 -0.564084 3.97209 5.54515)"
-     :ry "3.37606"
-     :rx "2.95326"}]
-   [:ellipse
-    {:transform
-         "matrix(0.987073 0.160274 -0.239143 0.970984 13.0843 14.72)"
-     :ry "6.13006"
-     :rx "7.78547"}]])
+(defn logo
+  ([] (logo 20))
+  ([size]
+   [:svg
+    {:fill "currentColor", :view-box "0 0 21 21", :height size, :width size}
+    [:ellipse
+     {:transform
+      "matrix(0.987073 0.160274 -0.239143 0.970984 11.7346 2.59206)"
+      :ry "2.04373"
+      :rx "3.29236"}]
+    [:ellipse
+     {:transform
+      "matrix(-0.495846 0.868411 -0.825718 -0.564084 3.97209 5.54515)"
+      :ry "3.37606"
+      :rx "2.95326"}]
+    [:ellipse
+     {:transform
+      "matrix(0.987073 0.160274 -0.239143 0.970984 13.0843 14.72)"
+      :ry "6.13006"
+      :rx "7.78547"}]]))
 
 (def page
   [:svg.h-5.w-4 {:viewBox "0 0 24 24", :fill "none", :xmlns "http://www.w3.org/2000/svg"}

+ 3 - 0
src/main/frontend/extensions/code.cljs

@@ -146,6 +146,9 @@
 (def textarea-ref-name "textarea")
 (def codemirror-ref-name "codemirror-instance")
 
+;; export CodeMirror to global scope
+(set! js/window -CodeMirror cm)
+
 (defn- extra-codemirror-options []
   (get (state/get-config)
        :editor/extra-codemirror-options {}))

+ 202 - 202
src/main/frontend/fs/sync.cljs

@@ -301,9 +301,9 @@
 (defn <request [api-name & args]
   (let [name (str api-name (.now js/Date))]
     (go (swap! *on-flying-request conj name)
-        (let [r (<! (apply <request* api-name args))]
-          (swap! *on-flying-request disj name)
-          r))))
+      (let [r (<! (apply <request* api-name args))]
+        (swap! *on-flying-request disj name)
+        r))))
 
 (defn- remove-dir-prefix [dir path]
   (let [r (string/replace path (js/RegExp. (str "^" (gstring/regExpEscape dir))) "")]
@@ -327,18 +327,18 @@
 (defn relative-path [o]
   (let [repo-dir (config/get-repo-dir (state/get-current-repo))]
     (cond
-     (implements? IRelativePath o)
-     (-relative-path o)
+      (implements? IRelativePath o)
+      (-relative-path o)
 
-     ;; full path
-     (and (string? o) (string/starts-with? o repo-dir))
-     (string/replace o (str repo-dir "/") "")
+      ;; full path
+      (and (string? o) (string/starts-with? o repo-dir))
+      (string/replace o (str repo-dir "/") "")
 
-     (string? o)
-     (remove-user-graph-uuid-prefix o)
+      (string? o)
+      (remove-user-graph-uuid-prefix o)
 
-     :else
-     (throw (js/Error. (str "unsupport type " (str o)))))))
+      :else
+      (throw (js/Error. (str "unsupport type " (str o)))))))
 
 (defprotocol IChecksum
   (-checksum [this]))
@@ -347,7 +347,7 @@
   (-stop! [this]))
 (defprotocol IStopped?
   (-stopped? [this]))
-                                        ;from-path, to-path is relative path
+;from-path, to-path is relative path
 (deftype FileTxn [from-path to-path updated? deleted? txid checksum]
   Object
   (renamed? [_]
@@ -392,19 +392,19 @@
   (let [update? (= "update_files" TXType)
         delete? (= "delete_files" TXType)
         update-xf
-        (comp
-         (remove #(or (empty? (first %))
-                      (empty? (last %))))
-         (map #(->FileTxn (first %) (first %) update? delete? TXId (last %))))
+                (comp
+                 (remove #(or (empty? (first %))
+                              (empty? (last %))))
+                 (map #(->FileTxn (first %) (first %) update? delete? TXId (last %))))
         delete-xf
-        (comp
-         (remove #(empty? (first %)))
-         (map #(->FileTxn (first %) (first %) update? delete? TXId nil)))
+                (comp
+                 (remove #(empty? (first %)))
+                 (map #(->FileTxn (first %) (first %) update? delete? TXId nil)))
         rename-xf
-        (comp
-         (remove #(or (empty? (first %))
-                      (empty? (second %))))
-         (map #(->FileTxn (second %) (first %) false false TXId nil)))
+                (comp
+                 (remove #(or (empty? (first %))
+                              (empty? (second %))))
+                 (map #(->FileTxn (second %) (first %) false false TXId nil)))
         xf (case TXType
              "delete_files" delete-xf
              "update_files" update-xf
@@ -623,21 +623,21 @@
    #{} s1))
 
 (comment
-  (defn map->FileMetadata [m]
-    (apply ->FileMetadata ((juxt :size :etag :path :encrypted-path :last-modified :remote? (constantly nil)) m)))
-
-  (assert
-   (=
-    #{(map->FileMetadata {:size 1 :etag 2 :path 2 :encrypted-path 2 :last-modified 2})}
-    (diff-file-metadata-sets
-     (into #{}
-           (map map->FileMetadata)
-           [{:size 1 :etag 1 :path 1 :encrypted-path 1 :last-modified 1}
-            {:size 1 :etag 2 :path 2 :encrypted-path 2 :last-modified 2}])
-     (into #{}
-           (map map->FileMetadata)
-           [{:size 1 :etag 1 :path 1 :encrypted-path 1 :last-modified 1}
-            {:size 1 :etag 1 :path 2 :encrypted-path 2 :last-modified 1}])))))
+ (defn map->FileMetadata [m]
+   (apply ->FileMetadata ((juxt :size :etag :path :encrypted-path :last-modified :remote? (constantly nil)) m)))
+
+ (assert
+  (=
+   #{(map->FileMetadata {:size 1 :etag 2 :path 2 :encrypted-path 2 :last-modified 2})}
+   (diff-file-metadata-sets
+    (into #{}
+          (map map->FileMetadata)
+          [{:size 1 :etag 1 :path 1 :encrypted-path 1 :last-modified 1}
+           {:size 1 :etag 2 :path 2 :encrypted-path 2 :last-modified 2}])
+    (into #{}
+          (map map->FileMetadata)
+          [{:size 1 :etag 1 :path 1 :encrypted-path 1 :last-modified 1}
+           {:size 1 :etag 1 :path 2 :encrypted-path 2 :last-modified 1}])))))
 
 (extend-protocol IChecksum
   FileMetadata
@@ -1079,8 +1079,8 @@
      (go-loop []
        (let [{:keys [val stop]}
              (async/alt!
-               debug-print-sync-events-loop-stop-chan {:stop true}
-               out-ch ([v] {:val v}))]
+              debug-print-sync-events-loop-stop-chan {:stop true}
+              out-ch ([v] {:val v}))]
          (cond
            stop (do (async/unmix-all out-mix)
                     (doseq [[topic ch] topic&chs]
@@ -1096,28 +1096,28 @@
 
 
 (comment
-  ;; sub one type event example:
-  (def c1 (chan 10))
-  (async/sub sync-events-publication :created-local-version-file c1)
-  (offer! sync-events-chan {:event :created-local-version-file :data :xxx})
-  (poll! c1)
-
-  ;; sub multiple type events example:
-  ;; sub :created-local-version-file and :finished-remote->local events,
-  ;; output into channel c4-out
-  (def c2 (chan 10))
-  (def c3 (chan 10))
-  (def c4-out (chan 10))
-  (def mix-out (async/mix c4-out))
-  (async/admix mix-out c2)
-  (async/admix mix-out c3)
-  (async/sub sync-events-publication :created-local-version-file c2)
-  (async/sub sync-events-publication :finished-remote->local c3)
-  (offer! sync-events-chan {:event :created-local-version-file :data :xxx})
-  (offer! sync-events-chan {:event :finished-remote->local :data :xxx})
-  (poll! c4-out)
-  (poll! c4-out)
-  )
+ ;; sub one type event example:
+ (def c1 (chan 10))
+ (async/sub sync-events-publication :created-local-version-file c1)
+ (offer! sync-events-chan {:event :created-local-version-file :data :xxx})
+ (poll! c1)
+
+ ;; sub multiple type events example:
+ ;; sub :created-local-version-file and :finished-remote->local events,
+ ;; output into channel c4-out
+ (def c2 (chan 10))
+ (def c3 (chan 10))
+ (def c4-out (chan 10))
+ (def mix-out (async/mix c4-out))
+ (async/admix mix-out c2)
+ (async/admix mix-out c3)
+ (async/sub sync-events-publication :created-local-version-file c2)
+ (async/sub sync-events-publication :finished-remote->local c3)
+ (offer! sync-events-chan {:event :created-local-version-file :data :xxx})
+ (offer! sync-events-chan {:event :finished-remote->local :data :xxx})
+ (poll! c4-out)
+ (poll! c4-out)
+ )
 
 ;;; sync events ends
 
@@ -1189,27 +1189,27 @@
      (let [file-meta-list      (transient #{})
            encrypted-path-list (transient [])
            exp-r
-           (<!
-            (go-loop [continuation-token nil]
-              (let [r (<! (.<request this "get_all_files"
-                                     (into
-                                      {}
-                                      (remove (comp nil? second)
-                                              {:GraphUUID graph-uuid :ContinuationToken continuation-token}))))]
-                (if (instance? ExceptionInfo r)
-                  r
-                  (let [next-continuation-token (:NextContinuationToken r)
-                        objs                    (:Objects r)]
-                    (apply conj! encrypted-path-list (map (comp remove-user-graph-uuid-prefix :Key) objs))
-                    (apply conj! file-meta-list
-                           (map
-                            #(hash-map :checksum (:checksum %)
-                                       :encrypted-path (remove-user-graph-uuid-prefix (:Key %))
-                                       :size (:Size %)
-                                       :last-modified (:LastModified %))
-                            objs))
-                    (when-not (empty? next-continuation-token)
-                      (recur next-continuation-token)))))))]
+                               (<!
+                                (go-loop [continuation-token nil]
+                                  (let [r (<! (.<request this "get_all_files"
+                                                         (into
+                                                          {}
+                                                          (remove (comp nil? second)
+                                                                  {:GraphUUID graph-uuid :ContinuationToken continuation-token}))))]
+                                    (if (instance? ExceptionInfo r)
+                                      r
+                                      (let [next-continuation-token (:NextContinuationToken r)
+                                            objs                    (:Objects r)]
+                                        (apply conj! encrypted-path-list (map (comp remove-user-graph-uuid-prefix :Key) objs))
+                                        (apply conj! file-meta-list
+                                               (map
+                                                #(hash-map :checksum (:checksum %)
+                                                           :encrypted-path (remove-user-graph-uuid-prefix (:Key %))
+                                                           :size (:Size %)
+                                                           :last-modified (:LastModified %))
+                                                objs))
+                                        (when-not (empty? next-continuation-token)
+                                          (recur next-continuation-token)))))))]
        (if (instance? ExceptionInfo exp-r)
          exp-r
          (let [file-meta-list*      (persistent! file-meta-list)
@@ -1278,58 +1278,58 @@
          (let [txns-with-encrypted-paths (mapv #(update % :path remove-user-graph-uuid-prefix) (:Transactions r))
                encrypted-paths           (mapv :path txns-with-encrypted-paths)
                encrypted-path->path-map
-               (zipmap
-                encrypted-paths
-                (<! (<decrypt-fnames rsapi graph-uuid encrypted-paths)))
+                                         (zipmap
+                                          encrypted-paths
+                                          (<! (<decrypt-fnames rsapi graph-uuid encrypted-paths)))
                txns
-               (mapv
-                (fn [txn] (update txn :path #(get encrypted-path->path-map %)))
-                txns-with-encrypted-paths)]
+                                         (mapv
+                                          (fn [txn] (update txn :path #(get encrypted-path->path-map %)))
+                                          txns-with-encrypted-paths)]
            txns)))))
 
   (<get-diff [this graph-uuid from-txid]
-    ;; TODO: path in transactions should be relative path(now s3 key, which includes graph-uuid and user-uuid)
+   ;; TODO: path in transactions should be relative path(now s3 key, which includes graph-uuid and user-uuid)
     (user/<wrap-ensure-id&access-token
      (let [r (<! (.<request this "get_diff" {:GraphUUID graph-uuid :FromTXId from-txid}))]
        (if (instance? ExceptionInfo r)
          r
          (let [txns-with-encrypted-paths (sort-by :TXId (:Transactions r))
                txns-with-encrypted-paths*
-               (mapv
-                (fn [txn]
-                  (assoc txn :TXContent
-                         (mapv
-                          (fn [[to-path from-path checksum]]
-                            [(remove-user-graph-uuid-prefix to-path)
-                             (some-> from-path remove-user-graph-uuid-prefix)
-                             checksum])
-                          (:TXContent txn))))
-                txns-with-encrypted-paths)
+                                         (mapv
+                                          (fn [txn]
+                                            (assoc txn :TXContent
+                                                   (mapv
+                                                    (fn [[to-path from-path checksum]]
+                                                      [(remove-user-graph-uuid-prefix to-path)
+                                                       (some-> from-path remove-user-graph-uuid-prefix)
+                                                       checksum])
+                                                    (:TXContent txn))))
+                                          txns-with-encrypted-paths)
                encrypted-paths
-               (mapcat
-                (fn [txn]
-                  (remove
-                   #(or (nil? %) (not (string/starts-with? % "e.")))
-                   (mapcat
-                    (fn [[to-path from-path _checksum]] [to-path from-path])
-                    (:TXContent txn))))
-                txns-with-encrypted-paths*)
+                                         (mapcat
+                                          (fn [txn]
+                                            (remove
+                                             #(or (nil? %) (not (string/starts-with? % "e.")))
+                                             (mapcat
+                                              (fn [[to-path from-path _checksum]] [to-path from-path])
+                                              (:TXContent txn))))
+                                          txns-with-encrypted-paths*)
                encrypted-path->path-map
-               (zipmap
-                encrypted-paths
-                (<! (<decrypt-fnames rsapi graph-uuid encrypted-paths)))
+                                         (zipmap
+                                          encrypted-paths
+                                          (<! (<decrypt-fnames rsapi graph-uuid encrypted-paths)))
                txns
-               (mapv
-                (fn [txn]
-                  (assoc
-                   txn :TXContent
-                   (mapv
-                    (fn [[to-path from-path checksum]]
-                      [(get encrypted-path->path-map to-path to-path)
-                       (some->> from-path (get encrypted-path->path-map))
-                       checksum])
-                    (:TXContent txn))))
-                txns-with-encrypted-paths*)]
+                                         (mapv
+                                          (fn [txn]
+                                            (assoc
+                                              txn :TXContent
+                                              (mapv
+                                               (fn [[to-path from-path checksum]]
+                                                 [(get encrypted-path->path-map to-path to-path)
+                                                  (some->> from-path (get encrypted-path->path-map))
+                                                  checksum])
+                                               (:TXContent txn))))
+                                          txns-with-encrypted-paths*)]
            [txns
             (:TXId (last txns))
             (:TXId (first txns))])))))
@@ -1470,12 +1470,12 @@
                       (.-deleted? e) :delete-filetxns
                       (.renamed? e)  :rename-filetxns)) filetxns)
         update-file-items (map
-                            (fn [filetxn]
-                              (let [path (relative-path filetxn)]
-                                {:remote->local-type :update
-                                 :checksum (-checksum filetxn)
-                                 :path path}))
-                            update-filetxns)
+                           (fn [filetxn]
+                             (let [path (relative-path filetxn)]
+                               {:remote->local-type :update
+                                :checksum (-checksum filetxn)
+                                :path path}))
+                           update-filetxns)
         rename-file-items (mapcat
                            (fn [^FileTxn filetxn]
                              (let [to-path (relative-path filetxn)
@@ -1488,12 +1488,12 @@
                                  :path from-path}]))
                            rename-filetxns)
         delete-file-items (map
-                            (fn [filetxn]
-                              (let [path (relative-path filetxn)]
-                                {:remote->local-type :delete
-                                 :checksum (-checksum filetxn)
-                                 :path path}))
-                            delete-filetxns)]
+                           (fn [filetxn]
+                             (let [path (relative-path filetxn)]
+                               {:remote->local-type :delete
+                                :checksum (-checksum filetxn)
+                                :path path}))
+                           delete-filetxns)]
     (set (concat update-file-items rename-file-items delete-file-items))))
 
 (defn- apply-filetxns
@@ -1528,8 +1528,8 @@
                      [recent-remote->local-file-item])
               (<! (<delete-local-files rsapi graph-uuid base-path [relative-p*]))
               (go (<! (timeout 5000))
-                  (swap! *sync-state sync-state--remove-recent-remote->local-files
-                         [recent-remote->local-file-item])))))
+                (swap! *sync-state sync-state--remove-recent-remote->local-files
+                       [recent-remote->local-file-item])))))
 
         (let [update-local-files-ch (<update-local-files rsapi graph-uuid base-path (map relative-path filetxns))
               r (<! (<with-pause update-local-files-ch *paused))]
@@ -1581,8 +1581,8 @@
                                                      (not (instance? ExceptionInfo r)))]
           ;; remove these recent-remote->local-file-items 5s later
           (go (<! (timeout 5000))
-              (swap! *sync-state sync-state--remove-recent-remote->local-files
-                     recent-remote->local-file-items))
+            (swap! *sync-state sync-state--remove-recent-remote->local-files
+                   recent-remote->local-file-items))
           (cond
             (instance? ExceptionInfo r) r
             @*paused                    {:pause true}
@@ -1682,7 +1682,7 @@
           path (relative-path e)]
       {:remote->local-type tp
        :checksum (if (= tp :delete) nil
-                     (val (first (<! (get-local-files-checksum graph-uuid (.-dir e) [path])))))
+                                    (val (first (<! (get-local-files-checksum graph-uuid (.-dir e) [path])))))
        :path path})))
 
 (defn- distinct-file-change-events-xf
@@ -1744,8 +1744,8 @@
     (go-loop []
       (let [{:keys [rename-event local-change]}
             (async/alt!
-              rename-page-event-chan ([v] {:rename-event v}) ;; {:repo X :old-path X :new-path}
-              local-changes-chan ([v] {:local-change v}))]
+             rename-page-event-chan ([v] {:rename-event v}) ;; {:repo X :old-path X :new-path}
+             local-changes-chan ([v] {:local-change v}))]
         (cond
           rename-event
           (let [repo-dir (config/get-repo-dir (:repo rename-event))
@@ -1758,7 +1758,7 @@
             (swap! *rename-events conj k1 k2)
             ;; remove rename-events after 2s
             (go (<! (timeout 3000))
-                (swap! *rename-events disj k1 k2))
+              (swap! *rename-events disj k1 k2))
             ;; add 2 simulated file-watcher events
             (>! ch (->FileChangeEvent "unlink" repo-dir (:old-path rename-event*) nil nil))
             (>! ch (->FileChangeEvent "add" repo-dir (:new-path rename-event*)
@@ -2200,8 +2200,8 @@
   if local-txid != remote-txid, return {:need-sync-remote true}"))
 
 (defrecord ^:large-vars/cleanup-todo
-    Remote->LocalSyncer [user-uuid graph-uuid base-path repo *txid *sync-state remoteapi
-                         ^:mutable local->remote-syncer *stopped *paused]
+  Remote->LocalSyncer [user-uuid graph-uuid base-path repo *txid *sync-state remoteapi
+                       ^:mutable local->remote-syncer *stopped *paused]
   Object
   (set-local->remote-syncer! [_ s] (set! local->remote-syncer s))
   (sync-files-remote->local!
@@ -2293,8 +2293,8 @@
                 diff-remote-files       (diff-file-metadata-sets remote-all-files-meta local-all-files-meta)
                 recent-10-days-range    ((juxt #(tc/to-long (t/minus % (t/days 10))) #(tc/to-long %)) (t/today))
                 sorted-diff-remote-files
-                (sort-by
-                 (sort-file-metadata-fn :recent-days-range recent-10-days-range) > diff-remote-files)
+                                        (sort-by
+                                         (sort-file-metadata-fn :recent-days-range recent-10-days-range) > diff-remote-files)
                 remote-graph-info-or-ex (<! (<get-remote-graph remoteapi nil graph-uuid))
                 latest-txid             (:TXId remote-graph-info-or-ex)]
             (if (or (instance? ExceptionInfo remote-graph-info-or-ex) (nil? latest-txid))
@@ -2308,7 +2308,7 @@
                     (swap! *sync-state #(sync-state-reset-full-remote->local-files % sorted-diff-remote-files))
                     (<! (.sync-files-remote->local!
                          this (map (juxt relative-path -checksum)
-                                filtered-files)
+                                   filtered-files)
                          latest-txid)))))))))))
 
 (defn- <file-changed?
@@ -2409,17 +2409,17 @@
          (comp
           (filter
            (fn [[path _]]
-                                        ; 950 = (- 1024 36 36 2)
-                                        ; 1024 - length of 'user-uuid/graph-uuid/'
+             ; 950 = (- 1024 36 36 2)
+             ; 1024 - length of 'user-uuid/graph-uuid/'
              (<= (count (get fnames-map path)) 950)))
           (map second))
          local-files-meta-map))))
 
 (defrecord ^:large-vars/cleanup-todo
-    Local->RemoteSyncer [user-uuid graph-uuid base-path repo *sync-state remoteapi
-                         ^:mutable rate *txid ^:mutable remote->local-syncer stop-chan *stopped *paused
-                         ;; control chans
-                         private-immediately-local->remote-chan private-recent-edited-chan]
+  Local->RemoteSyncer [user-uuid graph-uuid base-path repo *sync-state remoteapi
+                       ^:mutable rate *txid ^:mutable remote->local-syncer stop-chan *stopped *paused
+                       ;; control chans
+                       private-immediately-local->remote-chan private-recent-edited-chan]
   Object
   (filter-file-change-events-fn [_]
     (fn [^FileChangeEvent e]
@@ -2577,21 +2577,21 @@
                                             <!
                                             (sort-by (sort-file-metadata-fn :recent-days-range recent-10-days-range) >))
                 change-events
-                (sequence
-                 (comp
-                  ;; convert to FileChangeEvent
-                  (map #(->FileChangeEvent "change" base-path (.get-normalized-path ^FileMetadata %)
-                                           {:size (:size %)} (:etag %)))
-                  (remove ignored?))
-                 diff-local-files)
+                                       (sequence
+                                        (comp
+                                         ;; convert to FileChangeEvent
+                                         (map #(->FileChangeEvent "change" base-path (.get-normalized-path ^FileMetadata %)
+                                                                  {:size (:size %)} (:etag %)))
+                                         (remove ignored?))
+                                        diff-local-files)
                 distinct-change-events (-> (distinct-file-change-events change-events)
                                            filter-upload-files-with-reserved-chars)
                 _                      (swap! *sync-state #(sync-state-reset-full-local->remote-files % distinct-change-events))
                 change-events-partitions
-                (sequence
-                 ;; partition FileChangeEvents
-                 (partition-file-change-events upload-batch-size)
-                 distinct-change-events)]
+                                       (sequence
+                                        ;; partition FileChangeEvents
+                                        (partition-file-change-events upload-batch-size)
+                                        distinct-change-events)]
             (println "[full-sync(local->remote)]"
                      (count (flatten change-events-partitions)) "files need to sync and"
                      (count delete-local-files) "local files need to delete")
@@ -2612,8 +2612,8 @@
                              [fake-recent-remote->local-file-item])
                       (<! (<delete-local-files rsapi graph-uuid base-path [(relative-path f)]))
                       (go (<! (timeout 5000))
-                          (swap! *sync-state sync-state--remove-recent-remote->local-files
-                                 [fake-recent-remote->local-file-item])))))
+                        (swap! *sync-state sync-state--remove-recent-remote->local-files
+                               [fake-recent-remote->local-file-item])))))
                 (recur fs)))
 
             ;; 2. upload local files
@@ -2633,14 +2633,14 @@
 ;;; ### put all stuff together
 
 (defrecord ^:large-vars/cleanup-todo
-    SyncManager [graph-uuid base-path *sync-state
-                 ^Local->RemoteSyncer local->remote-syncer ^Remote->LocalSyncer remote->local-syncer remoteapi
-                 ^:mutable ratelimit-local-changes-chan
-                 *txid ^:mutable state ^:mutable remote-change-chan ^:mutable *ws *stopped? *paused?
-                 ^:mutable ops-chan
-                 ;; control chans
-                 private-full-sync-chan private-stop-sync-chan private-remote->local-sync-chan
-                 private-remote->local-full-sync-chan private-pause-resume-chan]
+  SyncManager [graph-uuid base-path *sync-state
+               ^Local->RemoteSyncer local->remote-syncer ^Remote->LocalSyncer remote->local-syncer remoteapi
+               ^:mutable ratelimit-local-changes-chan
+               *txid ^:mutable state ^:mutable remote-change-chan ^:mutable *ws *stopped? *paused?
+               ^:mutable ops-chan
+               ;; control chans
+               private-full-sync-chan private-stop-sync-chan private-remote->local-sync-chan
+               private-remote->local-full-sync-chan private-pause-resume-chan]
   Object
   (schedule [this next-state args reason]
     {:pre [(s/valid? ::state next-state)]}
@@ -2681,19 +2681,19 @@
     (go-loop []
       (let [{:keys [stop remote->local remote->local-full-sync local->remote-full-sync local->remote resume pause]}
             (async/alt!
-              private-stop-sync-chan {:stop true}
-              private-remote->local-full-sync-chan {:remote->local-full-sync true}
-              private-remote->local-sync-chan {:remote->local true}
-              private-full-sync-chan {:local->remote-full-sync true}
-              private-pause-resume-chan ([v] (if v {:resume true} {:pause true}))
-              remote-change-chan ([v] (println "remote change:" v) {:remote->local v})
-              ratelimit-local-changes-chan ([v]
-                                            (let [rest-v (util/drain-chan ratelimit-local-changes-chan)
-                                                  vs     (cons v rest-v)]
-                                              (println "local changes:" vs)
-                                              {:local->remote vs}))
-              (timeout (* 20 60 1000)) {:local->remote-full-sync true}
-              :priority true)]
+             private-stop-sync-chan {:stop true}
+             private-remote->local-full-sync-chan {:remote->local-full-sync true}
+             private-remote->local-sync-chan {:remote->local true}
+             private-full-sync-chan {:local->remote-full-sync true}
+             private-pause-resume-chan ([v] (if v {:resume true} {:pause true}))
+             remote-change-chan ([v] (println "remote change:" v) {:remote->local v})
+             ratelimit-local-changes-chan ([v]
+                                           (let [rest-v (util/drain-chan ratelimit-local-changes-chan)
+                                                 vs     (cons v rest-v)]
+                                             (println "local changes:" vs)
+                                             {:local->remote vs}))
+             (timeout (* 20 60 1000)) {:local->remote-full-sync true}
+             :priority true)]
         (cond
           stop
           (do (util/drain-chan ops-chan)
@@ -2906,13 +2906,13 @@
                 (.schedule this ::idle nil nil)))))))
 
   (local->remote [this {local-changes :local}]
-    ;; local-changes:: list of FileChangeEvent
+   ;; local-changes:: list of FileChangeEvent
     (assert (some? local-changes) local-changes)
     (go
       (let [distincted-local-changes (distinct-file-change-events local-changes)
             _ (swap! *sync-state #(sync-state-reset-full-local->remote-files % distincted-local-changes))
             change-events-partitions
-            (sequence (partition-file-change-events upload-batch-size) distincted-local-changes)
+                                     (sequence (partition-file-change-events upload-batch-size) distincted-local-changes)
             _ (put-sync-event! {:event :start
                                 :data  {:type       :local->remote
                                         :graph-uuid graph-uuid
@@ -3071,15 +3071,15 @@
   (go
     (let [r (<! (<list-remote-graphs remoteapi))
           result
-          (or
-           ;; if api call failed, assume this remote graph still exists
-           (instance? ExceptionInfo r)
-           (and
-            (contains? r :Graphs)
-            (->> (:Graphs r)
-                 (mapv :GraphUUID)
-                 set
-                 (#(contains? % local-graph-uuid)))))]
+            (or
+             ;; if api call failed, assume this remote graph still exists
+             (instance? ExceptionInfo r)
+             (and
+              (contains? r :Graphs)
+              (->> (:Graphs r)
+                   (mapv :GraphUUID)
+                   set
+                   (#(contains? % local-graph-uuid)))))]
 
       (when-not result
         (notification/show! (t :file-sync/graph-deleted) :warning false))
@@ -3243,7 +3243,7 @@
 
 ;;; add-tap
 (comment
-  (def *x (atom nil))
-  (add-tap (fn [v] (reset! *x v)))
+ (def *x (atom nil))
+ (add-tap (fn [v] (reset! *x v)))
 
-  )
+ )

+ 13 - 2
src/main/frontend/handler/plugin.cljs

@@ -329,6 +329,16 @@
     (swap! state/state medley/dissoc-in [:plugin/installed-resources pid])
     true))
 
+(defn register-plugin-search-service
+  [pid name opts]
+  (when-let [pid (and name (keyword pid))]
+    (state/install-plugin-service pid :search name opts)))
+
+(defn unregister-plugin-search-services
+  [pid]
+  (when-let [pid (keyword pid)]
+    (state/uninstall-plugin-service pid :search)))
+
 (defn unregister-plugin-themes
   ([pid] (unregister-plugin-themes pid true))
   ([pid effect]
@@ -564,7 +574,7 @@
     (when-not (= text "END")
       [:div.flex.align-items.justify-center.h-screen.w-full.preboot-loading
        [:span.flex.items-center.justify-center.w-60.flex-col
-        [:small.scale-250.opacity-70.mb-10.animate-pulse (svg/logo false)]
+        [:small.scale-250.opacity-70.mb-10.animate-pulse (svg/logo)]
         [:small.block.text-sm.relative.opacity-50 {:style {:right "-8px"}} text]]])))
 
 (defn ^:large-vars/cleanup-todo init-plugins!
@@ -586,7 +596,8 @@
                                 (invoke-exported-api "unregister_plugin_simple_command" pid)
                                 (invoke-exported-api "uninstall_plugin_hook" pid)
                                 (unregister-plugin-ui-items pid)
-                                (unregister-plugin-resources pid))
+                                (unregister-plugin-resources pid)
+                                (unregister-plugin-search-services pid))
 
               _               (doto js/LSPluginCore
                                 (.on "registered"

+ 2 - 1
src/main/frontend/handler/search.cljs

@@ -111,7 +111,8 @@
   ([clear-search-mode?]
    (let [m {:search/result nil
             :search/q ""}]
-     (swap! state/state merge m))
+     (swap! state/state merge m)
+     (when config/lsp-enabled? (state/reset-plugin-search-engines)))
    (when (and clear-search-mode? (not= (state/get-search-mode) :graph))
      (state/set-search-mode! :global))))
 

+ 1 - 1
src/main/frontend/mobile/graph_picker.cljs

@@ -84,7 +84,7 @@
 
      (when-not onboarding-and-home?
        [:h1.flex.items-center
-        [:span.scale-75 (svg/logo false)]
+        [:span.scale-75 (svg/logo)]
         [:span.pl-1 "Set up a graph"]])
 
      (case step

+ 2 - 5
src/main/frontend/search.cljs

@@ -7,9 +7,8 @@
             [frontend.db :as db]
             [frontend.db.model :as db-model]
             [frontend.regex :as regex]
-            [frontend.search.browser :as search-browser]
+            [frontend.search.agency :as search-agency]
             [frontend.search.db :as search-db :refer [indices]]
-            [frontend.search.node :as search-node]
             [frontend.search.protocol :as protocol]
             [frontend.state :as state]
             [frontend.util :as util]
@@ -19,9 +18,7 @@
 
 (defn get-engine
   [repo]
-  (if (util/electron?)
-    (search-node/->Node repo)
-    (search-browser/->Browser repo)))
+  (search-agency/->Agency repo))
 
 ;; Copied from https://gist.github.com/vaughnd/5099299
 (defn str-len-distance

+ 52 - 0
src/main/frontend/search/agency.cljs

@@ -0,0 +1,52 @@
+(ns frontend.search.agency
+  "Agent entry for search engine impls"
+  (:require [frontend.search.protocol :as protocol]
+            [frontend.search.browser :as search-browser]
+            [frontend.search.node :as search-node]
+            [frontend.search.plugin :as search-plugin]
+            [frontend.state :as state]
+            [frontend.util :as util]))
+
+(defn get-registered-engines
+  [repo]
+  (-> (if (util/electron?)
+        (search-node/->Node repo)
+        (search-browser/->Browser repo))
+      (cons
+       [(when state/lsp-enabled?
+          (for [s (state/get-all-plugin-services-with-type :search)]
+            (search-plugin/->Plugin s repo)))])))
+
+(deftype Agency [repo]
+  protocol/Engine
+
+  (query [_this q opts]
+    (println "D:Search > Query blocks:" repo q opts)
+    (let [[e1 e2] (get-registered-engines repo)]
+      (doseq [e e2]
+        (protocol/query e q opts))
+      (protocol/query e1 q opts)))
+
+  (rebuild-blocks-indice! [_this]
+    (println "D:Search > Initial blocks indice!:" repo)
+    (let [[e1 e2] (get-registered-engines repo)]
+      (doseq [e e2]
+        (protocol/rebuild-blocks-indice! e))
+      (protocol/rebuild-blocks-indice! e1)))
+
+  (transact-blocks! [_this data]
+    (println "D:Search > Transact blocks!:" repo)
+    (doseq [e (flatten (get-registered-engines repo))]
+      (protocol/transact-blocks! e data)))
+
+  (truncate-blocks! [_this]
+    (println "D:Search > Truncate blocks!" repo)
+    (doseq [e (flatten (get-registered-engines repo))]
+      (protocol/truncate-blocks! e)))
+
+  (remove-db! [_this]
+    (println "D:Search > Remove Db!" repo)
+    (doseq [e (flatten (get-registered-engines repo))]
+      (protocol/remove-db! e))))
+
+

+ 41 - 0
src/main/frontend/search/plugin.cljs

@@ -0,0 +1,41 @@
+(ns frontend.search.plugin
+  "Plugin service implementation of search protocol"
+  (:require [frontend.state :as state]
+            [frontend.handler.plugin :as plugin-handler]
+            [frontend.search.protocol :as protocol]
+            [cljs-bean.core :as bean]))
+
+(defn call-service!
+  ([service event payload] (call-service! service event payload false))
+  ([service event payload reply?]
+   (when-let [^js pl (plugin-handler/get-plugin-inst (:pid service))]
+     (let [{:keys [pid name]} service
+           hookEvent (str "service:" event ":" name)]
+       (.call (.-caller pl) hookEvent (bean/->js (merge {:graph (state/get-current-repo)} payload)))
+       (when reply?
+         (.once (.-caller pl) (str hookEvent ":reply")
+                (fn [^js e]
+                  (state/update-plugin-search-engine pid name #(assoc % :result (bean/->clj e))))))))))
+
+(deftype Plugin [service repo]
+  protocol/Engine
+
+  (query [_this q opts]
+    (call-service! service "search:query" (merge {:q q} opts) true))
+
+  (rebuild-blocks-indice! [_this]
+   ;; Not pushing all data for performance temporarily
+   ;;(let [blocks (search-db/build-blocks-indice repo)])
+    (call-service! service "search:rebuildBlocksIndice" {}))
+
+  (transact-blocks! [_this data]
+    (let [{:keys [blocks-to-remove-set blocks-to-add]} data]
+      (call-service! service "search:transactBlocks"
+                     {:data {:added   blocks-to-add
+                             :removed blocks-to-remove-set}})))
+
+  (truncate-blocks! [_this]
+    (call-service! service "search:truncateBlocks" {}))
+
+  (remove-db! [_this]
+    (call-service! service "search:removeDb" {})))

+ 57 - 0
src/main/frontend/state.cljs

@@ -54,6 +54,7 @@
      :search/mode                           :global
      :search/result                         nil
      :search/graph-filters                  []
+     :search/engines                        {}
 
      ;; modals
      :modal/dropdowns                       {}
@@ -188,6 +189,7 @@
      :plugin/installed-ui-items             {}
      :plugin/installed-resources            {}
      :plugin/installed-hooks                {}
+     :plugin/installed-services             {}
      :plugin/simple-commands                {}
      :plugin/selected-theme                 nil
      :plugin/selected-unpacked-pkg          nil
@@ -1454,6 +1456,61 @@ Similar to re-frame subscriptions"
         [:plugin/installed-resources (keyword pid) (keyword type) key] resource)
       resource)))
 
+(defn get-plugin-services
+  [pid type]
+  (when-let [installed (and pid (:plugin/installed-services @state))]
+    (some->> (seq (get installed (keyword pid)))
+             (filterv #(= type (:type %))))))
+
+(defn install-plugin-service
+  ([pid type name] (install-plugin-service pid type name nil))
+  ([pid type name opts]
+   (when-let [pid (and pid type name (keyword pid))]
+     (let [exists (get-plugin-services pid type)]
+       (when-let [service (and (or (not exists) (not (some #(= name (:name %)) exists)))
+                               {:pid pid :type type :name name :opts opts})]
+         (update-state! [:plugin/installed-services pid] #(conj (vec %) service))
+
+         ;; search engines state for results
+         (when (= type :search)
+           (set-state! [:search/engines (str pid name)] service)))))))
+
+(defn uninstall-plugin-service
+  [pid type-or-all]
+  (when-let [pid (keyword pid)]
+    (when-let [installed (get (:plugin/installed-services @state) pid)]
+      (let [remove-all? (or (true? type-or-all) (nil? type-or-all))
+            remains     (if remove-all? nil (filterv #(not= type-or-all (:type %)) installed))
+            removed     (if remove-all? installed (filterv #(= type-or-all (:type %)) installed))]
+        (set-state! [:plugin/installed-services pid] remains)
+
+        ;; search engines state for results
+        (when-let [removed' (seq (filter #(= :search (:type %)) removed))]
+          (update-state! :search/engines #(apply dissoc % (mapv (fn [{:keys [pid name]}] (str pid name)) removed'))))))))
+
+(defn get-all-plugin-services-with-type
+  [type]
+  (when-let [installed (vals (:plugin/installed-services @state))]
+    (mapcat (fn [s] (filter #(= (keyword type) (:type %)) s)) installed)))
+
+(defn get-all-plugin-search-engines
+  []
+  (:search/engines @state))
+
+(defn update-plugin-search-engine
+  [pid name f]
+  (when-let [pid (keyword pid)]
+    (set-state! :search/engines
+                (update-vals (get-all-plugin-search-engines)
+                             #(if (and (= pid (:pid %)) (= name (:name %)))
+                                (f %) %)))))
+
+(defn reset-plugin-search-engines
+  []
+  (when-let [engines (get-all-plugin-search-engines)]
+    (set-state! :search/engines
+                (update-vals engines #(assoc % :result nil)))))
+
 (defn install-plugin-hook
   [pid hook]
   (when-let [pid (keyword pid)]

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

@@ -966,7 +966,7 @@
   [text & {:keys [background href class intent on-click small? large? title icon icon-props disabled?]
            :or   {small? false large? false}
            :as   option}]
-  (let [klass (when-not intent ".bg-indigo-600.hover:bg-indigo-700.focus:border-indigo-700.active:bg-indigo-700.text-center")
+  (let [klass (if-not intent ".bg-indigo-600.hover:bg-indigo-700.focus:border-indigo-700.active:bg-indigo-700.text-center" intent)
         klass (if background (string/replace klass "indigo" background) klass)
         klass (if small? (str klass ".px-2.py-1") klass)
         klass (if large? (str klass ".text-base") klass)

+ 52 - 6
src/main/logseq/api.cljs

@@ -13,6 +13,7 @@
             [frontend.db.model :as db-model]
             [frontend.db.query-dsl :as query-dsl]
             [frontend.db.utils :as db-utils]
+            [frontend.db.react :refer [sub-key-value]]
             [frontend.db.query-react :as query-react]
             [frontend.fs :as fs]
             [frontend.handler.dnd :as editor-dnd-handler]
@@ -94,11 +95,23 @@
 (defn ^:export get_state_from_store
   [^js path]
   (when-let [path (if (string? path) [path] (bean/->clj path))]
-    (->> path
-         (map #(if (string/starts-with? % "@")
-                 (subs % 1)
-                 (keyword %)))
-         (get-in @state/state))))
+    (some->> path
+             (map #(if (string/starts-with? % "@")
+                     (subs % 1)
+                     (keyword %)))
+             (get-in @state/state)
+             (normalize-keyword-for-json)
+             (bean/->js))))
+
+(defn ^:export set_state_from_store
+  [^js path ^js value]
+  (when-let [path (if (string? path) [path] (bean/->clj path))]
+    (some->> path
+             (map #(if (string/starts-with? % "@")
+                     (subs % 1)
+                     (keyword %)))
+             (into [])
+             (#(state/set-state! % (bean/->clj value))))))
 
 (defn ^:export get_app_info
   ;; get app base info
@@ -130,6 +143,20 @@
             (normalize-keyword-for-json)
             (bean/->js))))
 
+(def ^:export get_current_graph_favorites
+  (fn []
+    (some->> (:favorites (state/get-config))
+             (remove string/blank?)
+             (filter string?)
+             (bean/->js))))
+
+(def ^:export get_current_graph_recent
+  (fn []
+    (some->> (sub-key-value :recent/pages)
+             (remove string/blank?)
+             (filter string?)
+             (bean/->js))))
+
 (def ^:export get_current_graph
   (fn []
     (when-let [repo (state/get-current-repo)]
@@ -370,6 +397,14 @@
           (js/console.debug :shortcut/unregister-shortcut cmd)
           (st/unregister-shortcut! (:handler-id cmd) (:id cmd)))))))
 
+(defn ^:export register_search_service
+  [pid name ^js opts]
+  (plugin-handler/register-plugin-search-service pid name (bean/->clj opts)))
+
+(defn ^:export unregister_search_services
+  [pid]
+  (plugin-handler/unregister-plugin-search-services pid))
+
 (def ^:export register_plugin_ui_item
   (fn [pid type ^js opts]
     (when-let [opts (bean/->clj opts)]
@@ -415,6 +450,13 @@
       (state/set-state! :ui/sidebar-open? (boolean flag)))
     nil))
 
+(def ^:export clear_right_sidebar_blocks
+  (fn [^js opts]
+    (state/clear-sidebar-blocks!)
+    (when-let [opts (and opts (bean/->clj opts))]
+      (and (:close opts) (state/hide-right-sidebar!)))
+    nil))
+
 (def ^:export push_state
   (fn [^js k ^js params ^js query]
     (rfe/push-state
@@ -802,7 +844,11 @@
   (when-let [repo (state/get-current-repo)]
     (when-let [db (db/get-db repo)]
       (let [query (cljs.reader/read-string query)
-            resolved-inputs (map (comp query-react/resolve-input cljs.reader/read-string) inputs)
+            resolved-inputs (map #(cond
+                                    (string? %)
+                                    (some-> % (cljs.reader/read-string) (query-react/resolve-input))
+                                    :else %)
+                                 inputs)
             result (apply d/q query db resolved-inputs)]
         (bean/->js (normalize-keyword-for-json result false))))))
 

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio