Procházet zdrojové kódy

Merge branch 'master' into enhance/mobile

llcc před 3 roky
rodič
revize
d2e54d31fe
52 změnil soubory, kde provedl 944 přidání a 411 odebrání
  1. 125 15
      .github/workflows/build-desktop-release.yml
  2. 0 1
      .github/workflows/build.yml
  3. 2 1
      deps.edn
  4. 18 4
      e2e-tests/basic.spec.ts
  5. 179 0
      e2e-tests/code-editing.spec.ts
  6. 1 1
      e2e-tests/hotkey.spec.ts
  7. 21 9
      e2e-tests/utils.ts
  8. 9 0
      externs.js
  9. 3 3
      package.json
  10. 0 7
      resources/css/common.css
  11. 2 2
      resources/package.json
  12. 4 1
      src/electron/electron/core.cljs
  13. 2 3
      src/electron/electron/fs_watcher.cljs
  14. 18 0
      src/electron/electron/utils.js
  15. 53 50
      src/main/frontend/components/block.cljs
  16. 3 1
      src/main/frontend/components/editor.cljs
  17. 4 0
      src/main/frontend/components/header.css
  18. 5 4
      src/main/frontend/components/lazy_editor.cljs
  19. 2 2
      src/main/frontend/components/page.cljs
  20. 5 1
      src/main/frontend/components/query_table.cljs
  21. 5 5
      src/main/frontend/components/repo.cljs
  22. 57 51
      src/main/frontend/components/search.cljs
  23. 1 3
      src/main/frontend/components/settings.cljs
  24. 2 2
      src/main/frontend/components/sidebar.cljs
  25. 3 3
      src/main/frontend/components/sidebar.css
  26. 6 0
      src/main/frontend/components/theme.css
  27. 1 1
      src/main/frontend/db.cljs
  28. 44 30
      src/main/frontend/db/model.cljs
  29. 6 3
      src/main/frontend/db/query_dsl.cljs
  30. 13 30
      src/main/frontend/extensions/code.cljs
  31. 11 4
      src/main/frontend/extensions/code.css
  32. 6 11
      src/main/frontend/extensions/graph.cljs
  33. 4 0
      src/main/frontend/extensions/graph.css
  34. 107 51
      src/main/frontend/extensions/graph/pixi.cljs
  35. 29 14
      src/main/frontend/format/block.cljs
  36. 1 0
      src/main/frontend/fs/capacitor_fs.cljs
  37. 12 9
      src/main/frontend/handler.cljs
  38. 41 34
      src/main/frontend/handler/editor.cljs
  39. 10 5
      src/main/frontend/handler/file.cljs
  40. 1 1
      src/main/frontend/handler/plugin.cljs
  41. 8 0
      src/main/frontend/handler/route.cljs
  42. 2 2
      src/main/frontend/modules/file/core.cljs
  43. 9 1
      src/main/frontend/modules/instrumentation/posthog.cljs
  44. 28 6
      src/main/frontend/modules/shortcut/config.cljs
  45. 9 4
      src/main/frontend/page.cljs
  46. 28 2
      src/main/frontend/state.cljs
  47. 1 14
      src/main/frontend/text.cljs
  48. 14 4
      src/main/frontend/ui.cljs
  49. 7 7
      src/main/frontend/ui.css
  50. 19 6
      src/main/frontend/util.cljc
  51. 2 2
      src/main/frontend/util/drawer.cljs
  52. 1 1
      src/main/frontend/version.cljs

+ 125 - 15
.github/workflows/build-desktop-release.yml

@@ -9,23 +9,23 @@ on:
         description: 'Build Target ("nightly"/"beta")'
         type: string
         required: true
-        default: "nightly"
+        default: "beta"
       git-ref:
-        description: "Release Git Ref"
+        description: "Release Git Ref(master)"
         required: true
         default: "master"
       is-draft:
-        description: 'Draft Release? '
+        description: 'Draft Release? (Beta only, Nightly will always be a non-draft)'
         type: boolean
         required: true
         default: true
       is-pre-release:
-        description: 'Pre Release?'
+        description: 'Pre Release? (labeled as "PreRelease")'
         type: boolean
         required: true
         default: true
-  # schedule: # Every workday at the noon (UTC) we run a scheduled nightly build
-  #   - cron: '0 12 * * MON-FRI'
+  schedule: # Every workday at the 2 P.M. (UTC) we run a scheduled nightly build
+    - cron: '0 14 * * MON-FRI'
 
 env:
   CLOJURE_VERSION: '1.10.1.763'
@@ -213,7 +213,7 @@ jobs:
           name: logseq-win64-builds
           path: builds
 
-  build-macos:
+  build-macos-x64:
     needs: [ compile-cljs ]
     runs-on: macos-11
 
@@ -277,8 +277,61 @@ jobs:
           mv static/out/make/Logseq.dmg ./builds/Logseq-darwin-x64-${{ steps.ref.outputs.version }}.dmg
           mv static/out/make/zip/darwin/x64/*.zip ./builds/Logseq-darwin-x64-${{ steps.ref.outputs.version }}.zip
 
+      - name: Upload Artifact
+        uses: actions/upload-artifact@v2
+        with:
+          name: logseq-darwin-x64-builds
+          path: builds
+
+  build-macos-arm64:
+    needs: [ compile-cljs ]
+    runs-on: macos-11
+
+    steps:
+      - name: Download The Static Asset
+        uses: actions/download-artifact@v2
+        with:
+          name: static
+          path: static
+
+      - name: Retrieve tag version
+        id: ref
+        run: |
+          pkgver=$(cat ./static/VERSION)
+          echo ::set-output name=version::$pkgver
+
+      - name: Install Node.js, NPM and Yarn
+        uses: actions/setup-node@v2
+        with:
+          node-version: ${{ env.NODE_VERSION }}
+
+      - name: Get yarn cache directory path
+        id: yarn-cache-dir-path
+        run: echo "::set-output name=dir::$(yarn cache dir)"
+      - name: Cache yarn cache directory
+        uses: actions/cache@v2
+        id: yarn-cache
+        with:
+          path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
+          key: ${{ runner.os }}-arm64-yarn-${{ hashFiles('**/yarn.lock') }}
+          restore-keys: |
+            ${{ runner.os }}-arm64-yarn-
+
+      - name: Signing By Apple Developer ID
+        uses: apple-actions/import-codesign-certs@v1
+        with:
+          p12-file-base64: ${{ secrets.APPLE_CERTIFICATES_P12 }}
+          p12-password: ${{ secrets.APPLE_CERTIFICATES_P12_PASSWORD }}
+
+      # - name: Cache Node Modules
+      #   uses: actions/cache@v2
+      #   with:
+      #     path: |
+      #       **/node_modules
+      #     key: ${{ runner.os }}-node-modules
+
       - name: Build/Release Electron App for arm64
-        run: yarn electron:make-macos-arm64
+        run: yarn install && yarn electron:make-macos-arm64
         working-directory: ./static
         env:
           APPLE_ID: ${{ secrets.APPLE_ID_EMAIL }}
@@ -287,25 +340,82 @@ jobs:
 
       - name: Save arm64 artifacts
         run: |
+          mkdir -p builds
           mv static/out/make/Logseq.dmg ./builds/Logseq-darwin-arm64-${{ steps.ref.outputs.version }}.dmg
           mv static/out/make/zip/darwin/arm64/*.zip ./builds/Logseq-darwin-arm64-${{ steps.ref.outputs.version }}.zip
 
       - name: Upload Artifact
         uses: actions/upload-artifact@v2
         with:
-          name: logseq-darwin-builds
+          name: logseq-darwin-arm64-builds
           path: builds
 
+  nightly-release:
+    if: ${{ github.event_name == 'schedule' || github.event.inputs.build-target == 'nightly' }}
+    needs: [ build-macos-x64, build-macos-arm64, build-linux, build-windows ]
+    runs-on: ubuntu-18.04
+    steps:
+      - name: Download MacOS x64 Artifacts
+        uses: actions/download-artifact@v2
+        with:
+          name: logseq-darwin-x64-builds
+          path: ./
+
+      - name: Download MacOS arm64 Artifacts
+        uses: actions/download-artifact@v2
+        with:
+          name: logseq-darwin-arm64-builds
+          path: ./
+
+      - name: Download The Linux Artifacts
+        uses: actions/download-artifact@v2
+        with:
+          name: logseq-linux-builds
+          path: ./
+
+      - name: Download The Windows Artifact
+        uses: actions/download-artifact@v2
+        with:
+          name: logseq-win64-builds
+          path: ./
+
+      - name: List files
+        run: ls -rl
+
+      - name: Update Nightly Release
+        uses: andelf/nightly-release@main
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        with:
+          tag_name: nightly
+          name: 'Desktop App Nightly Relase $$'
+          draft: false
+          prerelease: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.is-pre-release) || (github.event_name == 'schedule')}}
+          body: "This is a nightly release of the Logseq desktop app."
+          files: |
+            ./VERSION
+            ./SHA256SUMS.txt
+            ./*.zip
+            ./*.dmg
+            ./*.exe
+            ./*.AppImage
+
   release:
     # NOTE: For now, we only have beta channel to be released on Github
-    if: ${{ github.event_name == 'workflow_dispatch' && (github.event.inputs.build-target == 'beta') }}
-    needs: [ build-macos, build-linux, build-windows ]
+    if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.build-target != 'nightly' }}
+    needs: [ build-macos-x64, build-macos-arm64, build-linux, build-windows ]
     runs-on: ubuntu-18.04
     steps:
-      - name: Download MacOS Artifacts
+      - name: Download MacOS x64 Artifacts
+        uses: actions/download-artifact@v2
+        with:
+          name: logseq-darwin-x64-builds
+          path: ./
+
+      - name: Download MacOS arm64 Artifacts
         uses: actions/download-artifact@v2
         with:
-          name: logseq-darwin-builds
+          name: logseq-darwin-arm64-builds
           path: ./
 
       - name: Download The Linux Artifacts
@@ -344,8 +454,8 @@ jobs:
           tag_name: ${{ steps.ref.outputs.version }}
           name: Desktop APP ${{ steps.ref.outputs.version }} (Beta Testing)
           body: "TODO: Fill this changelog. Sorry for the inconvenience!"
-          draft: ${{ github.event.inputs.is-draft == true || github.event_name == 'schedule' }}
-          prerelease: ${{ github.event.inputs.is-pre-release == true || github.event_name == 'schedule' }}
+          draft: ${{ github.event.inputs.is-draft }}
+          prerelease: ${{ github.event.inputs.is-pre-release }}
           files: |
             ./VERSION
             ./SHA256SUMS.txt

+ 0 - 1
.github/workflows/build.yml

@@ -83,7 +83,6 @@ jobs:
           node static/tests.js
 
       - name: Run Playwright test
-        if: github.event_name == 'pull_request'
         run: |
           yarn release
           (cd static && yarn install && yarn rebuild:better-sqlite3)

+ 2 - 1
deps.edn

@@ -37,7 +37,8 @@
   camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.2"}
   instaparse/instaparse       {:mvn/version "1.4.10"}
   nubank/workspaces           {:mvn/version "1.1.1"}
-  frankiesardo/linked         {:mvn/version "1.3.0"}}
+  frankiesardo/linked         {:mvn/version "1.3.0"}
+  org.clojars.mmb90/cljs-cache {:mvn/version "0.1.4"}}
 
  :aliases {:cljs {:extra-paths ["src/dev-cljs/" "src/test/" "src/electron/"]
                   :extra-deps  {org.clojure/clojurescript   {:mvn/version "1.10.879"}

+ 18 - 4
e2e-tests/basic.spec.ts

@@ -1,6 +1,6 @@
 import { expect } from '@playwright/test'
 import { test } from './fixtures'
-import { randomString, createRandomPage, openSidebar, newBlock, lastBlock } from './utils'
+import { randomString, createRandomPage, newBlock } from './utils'
 
 
 test('render app', async ({ page }) => {
@@ -12,11 +12,25 @@ test('render app', async ({ page }) => {
   expect(await page.title()).toMatch(/^Logseq.*?/)
 })
 
-test('open sidebar', async ({ page }) => {
-  await openSidebar(page)
+test('toggle sidebar', async ({ page }) => {
+  let sidebar = page.locator('#left-sidebar')
+
+  // Left sidebar is toggled by `is-open` class
+  if (/is-open/.test(await sidebar.getAttribute('class'))) {
+    await page.click('#left-menu.button')
+    expect(await sidebar.getAttribute('class')).not.toMatch(/is-open/)
+  } else {
+    await page.click('#left-menu.button')
+    expect(await sidebar.getAttribute('class')).toMatch(/is-open/)
+    await page.click('#left-menu.button')
+    expect(await sidebar.getAttribute('class')).not.toMatch(/is-open/)
+  }
+
+  await page.click('#left-menu.button')
 
+  expect(await sidebar.getAttribute('class')).toMatch(/is-open/)
+  await page.waitForSelector('#left-sidebar .left-sidebar-inner', { state: 'visible' })
   await page.waitForSelector('#left-sidebar a:has-text("New page")', { state: 'visible' })
-  await page.waitForSelector('#left-sidebar >> text=Journals', { state: 'visible' })
 })
 
 test('search', async ({ page }) => {

+ 179 - 0
e2e-tests/code-editing.spec.ts

@@ -0,0 +1,179 @@
+import { expect } from '@playwright/test'
+import { test } from './fixtures'
+import { createRandomPage, escapeToCodeEditor, escapeToBlockEditor } from './utils'
+
+/**
+ * NOTE: CodeMirror is a complex library that requires a lot of setup to work.
+ * This test suite is designed to test the basic functionality of the editor.
+ * It is not intended to test the full functionality of CodeMirror.
+ * For more information, see: https://codemirror.net/doc/manual.html
+ */
+
+test('switch code editing mode', async ({ page }) => {
+  await createRandomPage(page)
+
+  // NOTE: ` will trigger auto-pairing in Logseq
+  // NOTE: ( will trigger auto-pairing in CodeMirror
+  // NOTE: waitForTimeout is needed to ensure that the hotkey handler is finished (shift+enter)
+  // NOTE: waitForTimeout is needed to ensure that the CodeMirror editor is fully loaded and unloaded
+  // NOTE: multiple textarea elements are existed in the editor, be careful to select the right one
+
+  // code block with 0 line
+  await page.type(':nth-match(textarea, 1)', '```clojure\n')
+  // line number: 1
+  await page.waitForSelector('.CodeMirror pre', { state: 'visible' })
+  expect(await page.locator('.CodeMirror-gutter-wrapper .CodeMirror-linenumber').innerText()).toBe('1')
+  // lang label: clojure
+  expect(await page.innerText('.block-body .extensions__code-lang')).toBe('clojure')
+
+  await page.press('.CodeMirror textarea', 'Escape')
+  await page.waitForSelector('.CodeMirror pre', { state: 'hidden' })
+  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('```clojure\n```')
+
+  await page.waitForTimeout(500)
+  await page.press(':nth-match(textarea, 1)', 'Escape')
+  await page.waitForSelector('.CodeMirror pre', { state: 'visible' })
+
+  // NOTE: must wait here, await loading of CodeMirror editor
+  await page.waitForTimeout(500)
+  await page.click('.CodeMirror pre')
+  await page.waitForTimeout(500)
+
+  await page.type('.CodeMirror textarea', '(+ 1 1')
+  await page.press('.CodeMirror textarea', 'Escape')
+  await page.waitForSelector('.CodeMirror pre', { state: 'hidden' })
+  expect(await page.inputValue('.block-editor textarea')).toBe('```clojure\n(+ 1 1)\n```')
+
+  await page.waitForTimeout(500) // editor unloading
+  await page.press('.block-editor textarea', 'Escape')
+  await page.waitForTimeout(500) // editor loading
+  // click position is estimated to be at the begining of the first line
+  await page.click('.CodeMirror pre', { position: { x: 1, y: 5 } })
+  await page.waitForTimeout(500)
+
+  await page.type('.CodeMirror textarea', ';; comment\n\n  \n')
+
+  await page.press('.CodeMirror textarea', 'Escape')
+  await page.waitForSelector('.CodeMirror pre', { state: 'hidden' })
+  expect(await page.inputValue('.block-editor textarea')).toBe('```clojure\n;; comment\n\n  \n(+ 1 1)\n```')
+
+  await page.waitForTimeout(500)
+})
+
+
+test('convert from block content to code', async ({ page }) => {
+  await createRandomPage(page)
+
+  await page.type('.block-editor textarea', '```')
+  await page.press('.block-editor textarea', 'Shift+Enter')
+  await page.waitForTimeout(100) // wait for hotkey handler
+  await page.press('.block-editor textarea', 'Escape')
+  await page.waitForSelector('.CodeMirror pre', { state: 'visible' })
+
+  await page.waitForTimeout(500)
+  await page.click('.CodeMirror pre')
+  await page.waitForTimeout(500)
+  expect(await page.locator('.CodeMirror-gutter-wrapper .CodeMirror-linenumber >> nth=-1').innerText()).toBe('1')
+
+  await page.press('.CodeMirror textarea', 'Escape')
+  await page.waitForTimeout(500)
+
+  expect(await page.inputValue('.block-editor textarea')).toBe('```\n```')
+
+  // reset block, code block with 1 line
+  await page.fill('.block-editor textarea', '```\n\n```')
+  await page.waitForTimeout(500) // wait for fill
+  await escapeToCodeEditor(page)
+  expect(await page.locator('.CodeMirror-gutter-wrapper .CodeMirror-linenumber >> nth=-1').innerText()).toBe('1')
+  await escapeToBlockEditor(page)
+  expect(await page.inputValue('.block-editor textarea')).toBe('```\n\n```')
+
+  // reset block, code block with 2 line
+  await page.fill('.block-editor textarea', '```\n\n\n```')
+  await page.waitForTimeout(500)
+  await escapeToCodeEditor(page)
+  expect(await page.locator('.CodeMirror-gutter-wrapper .CodeMirror-linenumber >> nth=-1').innerText()).toBe('2')
+  await escapeToBlockEditor(page)
+  expect(await page.inputValue('.block-editor textarea')).toBe('```\n\n\n```')
+
+  await page.fill('.block-editor textarea', '```\n  indented\nsecond line\n\n```')
+  await page.waitForTimeout(500)
+  await escapeToCodeEditor(page)
+  await escapeToBlockEditor(page)
+  expect(await page.inputValue('.block-editor textarea')).toBe('```\n  indented\nsecond line\n\n```')
+
+  await page.fill('.block-editor textarea', '```\n  indented\n  indented\n```')
+  await page.waitForTimeout(500)
+  await escapeToCodeEditor(page)
+  await escapeToBlockEditor(page)
+  expect(await page.inputValue('.block-editor textarea')).toBe('```\n  indented\n  indented\n```')
+})
+
+test('code block mixed input source', async ({ page }) => {
+  await createRandomPage(page)
+
+  await page.fill('.block-editor textarea', '```\n  ABC\n```')
+  await page.waitForTimeout(500) // wait for fill
+  await escapeToCodeEditor(page)
+  await page.type('.CodeMirror textarea', '  DEF\nGHI')
+
+  await page.waitForTimeout(500)
+  await page.press('.CodeMirror textarea', 'Escape')
+  await page.waitForTimeout(500)
+  // NOTE: auto-indent is on
+  expect(await page.inputValue('.block-editor textarea')).toBe('```\n  ABC  DEF\n  GHI\n```')
+})
+
+test('code block with text around', async ({ page }) => {
+  await createRandomPage(page)
+
+  await page.fill('.block-editor textarea', 'Heading\n```\n```\nFooter')
+  await page.waitForTimeout(500)
+  await escapeToCodeEditor(page)
+  await page.type('.CodeMirror textarea', 'first\n  second')
+
+  await page.waitForTimeout(500)
+  await page.press('.CodeMirror textarea', 'Escape')
+  await page.waitForTimeout(500)
+  expect(await page.inputValue('.block-editor textarea')).toBe('Heading\n```\nfirst\n  second\n```\nFooter')
+})
+
+test('multiple code block', async ({ page }) => {
+  await createRandomPage(page)
+
+  // NOTE: the two code blocks are of the same content
+  await page.fill('.block-editor textarea', '中文 Heading\n```clojure\n```\nMiddle 🚀\n```clojure\n```\nFooter')
+  await page.waitForTimeout(500)
+
+  await page.press('.block-editor textarea', 'Escape')
+  await page.waitForSelector('.CodeMirror pre', { state: 'visible' })
+
+  // first
+  await page.waitForTimeout(500)
+  await page.click('.CodeMirror pre >> nth=0')
+  await page.waitForTimeout(500)
+
+  await page.type('.CodeMirror textarea >> nth=0', ':key-test\n', { strict: true })
+  await page.waitForTimeout(500)
+
+  await page.press('.CodeMirror textarea >> nth=0', 'Escape')
+  await page.waitForTimeout(500)
+  expect(await page.inputValue('.block-editor textarea'))
+    .toBe('中文 Heading\n```clojure\n:key-test\n\n```\nMiddle 🚀\n```clojure\n```\nFooter')
+
+  // second
+  await page.press('.block-editor textarea', 'Escape')
+  await page.waitForSelector('.CodeMirror pre', { state: 'visible' })
+
+  await page.waitForTimeout(500)
+  await page.click('.CodeMirror pre >> nth=1')
+  await page.waitForTimeout(500)
+
+  await page.type('.CodeMirror textarea >> nth=1', '\n  :key-test 日本語\n', { strict: true })
+  await page.waitForTimeout(500)
+
+  await page.press('.CodeMirror textarea >> nth=1', 'Escape')
+  await page.waitForTimeout(500)
+  expect(await page.inputValue('.block-editor textarea'))
+    .toBe('中文 Heading\n```clojure\n:key-test\n\n```\nMiddle 🚀\n```clojure\n\n  :key-test 日本語\n\n```\nFooter')
+})

+ 1 - 1
e2e-tests/hotkey.spec.ts

@@ -1,6 +1,6 @@
 import { expect } from '@playwright/test'
 import { test } from './fixtures'
-import { createRandomPage, newBlock, lastBlock, appFirstLoaded, IsMac, IsLinux } from './utils'
+import { createRandomPage, newBlock, lastBlock, IsMac, IsLinux } from './utils'
 
 test('open search dialog', async ({ page }) => {
   if (IsMac) {

+ 21 - 9
e2e-tests/utils.ts

@@ -22,18 +22,10 @@ export async function appFirstLoaded(page: Page) {
     await page.waitForSelector('text=This is a demo graph, changes will not be saved until you open a local folder')
 }
 
-export async function openSidebar(page: Page) {
-    let sidebarVisible = await page.isVisible('#left-sidebar .left-sidebar-inner')
-    if (!sidebarVisible) {
-        await page.click('#left-menu.button')
-    }
-    await page.waitForSelector('#left-sidebar .left-sidebar-inner', { state: 'visible' })
-}
-
 export async function createRandomPage(page: Page) {
     const randomTitle = randomString(20)
 
-    // Click #left-sidebar a:has-text("New page")
+    // Click #search-button
     await page.click('#search-button')
     // Fill [placeholder="Search or create page"]
     await page.fill('[placeholder="Search or create page"]', randomTitle)
@@ -60,3 +52,23 @@ export async function newBlock(page: Page): Promise<Locator> {
 
     return page.locator(':nth-match(textarea, 1)')
 }
+
+export async function escapeToCodeEditor(page: Page): Promise<void> {
+    await page.press('.block-editor textarea', 'Escape')
+    await page.waitForSelector('.CodeMirror pre', { state: 'visible' })
+
+    await page.waitForTimeout(500)
+    await page.click('.CodeMirror pre')
+    await page.waitForTimeout(500)
+
+    await page.waitForSelector('.CodeMirror textarea', { state: 'visible' })
+}
+
+export async function escapeToBlockEditor(page: Page): Promise<void> {
+    await page.waitForTimeout(500)
+    await page.click('.CodeMirror pre')
+    await page.waitForTimeout(500)
+
+    await page.press('.CodeMirror textarea', 'Escape')
+    await page.waitForTimeout(500)
+}

+ 9 - 0
externs.js

@@ -118,6 +118,15 @@ dummy.convertToViewportRectangle = function() {};
 dummy.init = function() {};
 dummy.commit = function() {};
 dummy.raw = function() {};
+dummy.onHeadersReceived = function() {};
+dummy.responseHeaders = function() {};
+dummy.velocityDecay = function() {};
+dummy.velocityDecay = function() {};
+dummy.updatePosition = function() {};
+dummy.getNodesObjects = function() {};
+dummy.getEdgesObjects = function() {};
+dummy.alphaTarget = function() {};
+dummy.restart = function() {};
 
 /**
  * @typedef {{

+ 3 - 3
package.json

@@ -82,7 +82,7 @@
         "d3-force": "3.0.0",
         "diff": "5.0.0",
         "diff-match-patch": "1.0.5",
-        "electron": "16.0.4",
+        "electron": "15.1.2",
         "fs": "0.0.1-security",
         "fs-extra": "9.1.0",
         "fuse.js": "6.4.6",
@@ -92,9 +92,9 @@
         "ignore": "5.1.8",
         "is-svg": "4.2.2",
         "jszip": "3.5.0",
-        "mldoc": "1.2.4",
+        "mldoc": "1.2.5",
         "path": "0.12.7",
-        "pixi-graph-fork": "0.1.6",
+        "pixi-graph-fork": "0.2.0",
         "pixi.js": "6.2.0",
         "posthog-js": "1.10.2",
         "react": "17.0.2",

+ 0 - 7
resources/css/common.css

@@ -1123,13 +1123,6 @@ a.tooltip-priority {
   background: var(--ls-tertiary-background-color);
 }
 
-.references-blocks .block-control {
-    margin-left: -22px;
-    @screen sm {
-        margin-left: 2px;
-    }
-}
-
 #head .fade-link {
   font-weight: 600;
   font-size: 13px;

+ 2 - 2
resources/package.json

@@ -1,6 +1,6 @@
 {
   "name": "Logseq",
-  "version": "0.5.2",
+  "version": "0.5.4",
   "main": "electron.js",
   "author": "Logseq",
   "description": "A privacy-first, open-source platform for knowledge management and collaboration.",
@@ -11,7 +11,7 @@
     "electron:make": "electron-forge make",
     "electron:make-macos-arm64": "electron-forge make --platform=darwin --arch=arm64",
     "electron:publish:github": "electron-forge publish",
-    "rebuild:better-sqlite3": "electron-rebuild -v 16.0.4 -f -w better-sqlite3",
+    "rebuild:better-sqlite3": "electron-rebuild -v 15.1.2 -f -w better-sqlite3",
     "postinstall": "install-app-deps"
   },
   "config": {

+ 4 - 1
src/electron/electron/core.cljs

@@ -15,7 +15,8 @@
             ["electron-window-state" :as windowStateKeeper]
             [clojure.core.async :as async]
             [electron.state :as state]
-            [electron.git :as git]))
+            [electron.git :as git]
+            ["/electron/utils" :as utils]))
 
 (defonce LSP_SCHEME "lsp")
 (defonce LSP_PROTOCOL (str LSP_SCHEME "://"))
@@ -295,6 +296,8 @@
                    *quitting? (atom false)]
                (.. logger (info (str "Logseq App(" (.getVersion app) ") Starting... ")))
 
+               (utils/disableXFrameOptions win)
+
                (when (search/version-changed?)
                  (search/rm-search-dir!))
 

+ 2 - 3
src/electron/electron/fs_watcher.cljs

@@ -10,7 +10,7 @@
 ;; TODO: explore different solutions for different platforms
 ;; 1. https://github.com/Axosoft/nsfw
 
-(defonce polling-interval 5000)
+(defonce polling-interval 10000)
 (defonce file-watcher (atom nil))
 
 (defonce file-watcher-chan "file-watcher")
@@ -26,8 +26,7 @@
                           (clj->js
                            {:ignored (fn [path]
                                        (utils/ignored-path? dir path))
-                            ;; :ignoreInitial false
-                            :ignoreInitial true
+                            :ignoreInitial false
                             :ignorePermissionErrors true
                             :interval polling-interval
                             :binaryInterval polling-interval

+ 18 - 0
src/electron/electron/utils.js

@@ -0,0 +1,18 @@
+// workaround from https://github.com/electron/electron/issues/426#issuecomment-658901422
+// We set an intercept on incoming requests to disable x-frame-options
+// headers.
+
+export const disableXFrameOptions = (win) => {
+  win.webContents.session.webRequest.onHeadersReceived({ urls: [ "*://*/*" ] },
+                                                       (d, c)=>{
+                                                         if(d.responseHeaders['X-Frame-Options']){
+                                                           delete d.responseHeaders['X-Frame-Options'];
+                                                         } else if(d.responseHeaders['x-frame-options']) {
+                                                           delete d.responseHeaders['x-frame-options'];
+                                                         }
+
+                                                         c({cancel: false, responseHeaders: d.responseHeaders});
+                                                       }
+                                                      );
+
+};

+ 53 - 50
src/main/frontend/components/block.cljs

@@ -1785,7 +1785,7 @@
 
 (defn clock-summary-cp
   [block body]
-  [:span.text-right {:style {:max-width 100}}
+  [:div {:style {:max-width 100}}
    (when (and (state/enable-timetracking?)
               (or (= (:block/marker block) "DONE")
                   (contains? #{"TODO" "LATER"} (:block/marker block))))
@@ -1812,7 +1812,7 @@
 (rum/defc block-content < rum/reactive
   [config {:block/keys [uuid content children properties scheduled deadline format pre-block?] :as block} edit-input-id block-id slide?]
   (let [{:block/keys [title body] :as block} (if (:block/title block) block
-                                                 (merge block (block/parse-title-and-body format pre-block? content)))
+                                                 (merge block (block/parse-title-and-body uuid format pre-block? content)))
         collapsed? (get properties :collapsed)
         block-ref? (:block-ref? config)
         block-ref-with-title? (and block-ref? (seq title))
@@ -1841,51 +1841,48 @@
        (merge attrs))
 
      [:span
-      ;; .flex.relative {:style {:width "100%"}}
-      [:span
-       ;; .flex-1.flex-col.relative.block-content
-       [:span.flex.flex-row.justify-between
-        [:span
-         (cond
-           (seq title)
-           (build-block-title config block)
-
-           :else
-           nil)]
-
-        (clock-summary-cp block body)]
-
-       (when (seq children)
-         (dnd-separator-wrapper block block-id slide? false true))
-
-       (when deadline
-         (when-let [deadline-ast (block-handler/get-deadline-ast block)]
-           (timestamp-cp block "DEADLINE" deadline-ast)))
-
-       (when scheduled
-         (when-let [scheduled-ast (block-handler/get-scheduled-ast block)]
-           (timestamp-cp block "SCHEDULED" scheduled-ast)))
-
-       (when (and (seq properties)
-                  (let [hidden? (property/properties-built-in? properties)]
-                    (not hidden?))
-                  (not block-ref?)
-                  (not (:slide? config)))
-         (properties-cp config block))
-
-       (when (and (not block-ref-with-title?) (seq body))
-         [:div.block-body {:style {:display (if (and collapsed? (seq title)) "none" "")}}
-          ;; TODO: consistent id instead of the idx (since it could be changed later)
-          (let [body (block/trim-break-lines! (:block/body block))]
-            (for [[idx child] (medley/indexed body)]
-              (when-let [block (markup-element-cp config child)]
-                (rum/with-key (block-child block)
-                  (str uuid "-" idx)))))])
-
-       (case (:block/warning block)
-         :multiple-blocks
-         [:p.warning.text-sm "Full content is not displayed, Logseq doesn't support multiple unordered lists or headings in a block."]
-         nil)]]]))
+      [:div.flex.flex-row.justify-between
+       [:div.flex-1
+        (cond
+          (seq title)
+          (build-block-title config block)
+
+          :else
+          nil)]
+
+       (clock-summary-cp block body)]
+
+      (when (seq children)
+        (dnd-separator-wrapper block block-id slide? false true))
+
+      (when deadline
+        (when-let [deadline-ast (block-handler/get-deadline-ast block)]
+          (timestamp-cp block "DEADLINE" deadline-ast)))
+
+      (when scheduled
+        (when-let [scheduled-ast (block-handler/get-scheduled-ast block)]
+          (timestamp-cp block "SCHEDULED" scheduled-ast)))
+
+      (when (and (seq properties)
+                 (let [hidden? (property/properties-built-in? properties)]
+                   (not hidden?))
+                 (not block-ref?)
+                 (not (:slide? config)))
+        (properties-cp config block))
+
+      (when (and (not block-ref-with-title?) (seq body))
+        [:div.block-body {:style {:display (if (and collapsed? (seq title)) "none" "")}}
+         ;; TODO: consistent id instead of the idx (since it could be changed later)
+         (let [body (block/trim-break-lines! (:block/body block))]
+           (for [[idx child] (medley/indexed body)]
+             (when-let [block (markup-element-cp config child)]
+               (rum/with-key (block-child block)
+                 (str uuid "-" idx)))))])
+
+      (case (:block/warning block)
+        :multiple-blocks
+        [:p.warning.text-sm "Full content is not displayed, Logseq doesn't support multiple unordered lists or headings in a block."]
+        nil)]]))
 
 (rum/defc block-refs-count < rum/reactive
   [block]
@@ -2000,7 +1997,11 @@
             parents-props (doall
                            (for [{:block/keys [uuid name content] :as block} parents]
                              (when-not name ; not page
-                               (let [{:block/keys [title body]} (block/parse-title-and-body (:block/format block) (:block/pre-block? block) content)]
+                               (let [{:block/keys [title body]} (block/parse-title-and-body
+                                                                 uuid
+                                                                 (:block/format block)
+                                                                 (:block/pre-block? block)
+                                                                 content)]
                                  [block
                                  (if (seq title)
                                    (->elem :span (map-inline config title))
@@ -2160,7 +2161,7 @@
                        (not= (select-keys (first (:rum/args old-state)) config-compare-keys)
                              (select-keys (first (:rum/args new-state)) config-compare-keys)))))}
   [state config {:block/keys [uuid repo children pre-block? top? properties refs heading-level level type format content] :as block}]
-  (let [block (merge block (block/parse-title-and-body format pre-block? content))
+  (let [block (merge block (block/parse-title-and-body uuid format pre-block? content))
         body (:block/body block)
         blocks-container-id (:blocks-container-id config)
         config (update config :block merge block)
@@ -2638,7 +2639,9 @@
               [:div.opacity-50.font-medium
                (util/format ":%s:" (string/upper-case name))]
               [:div.opacity-50.font-medium
-               (logbook-cp lines)
+               (if (= name "logbook")
+                 (logbook-cp lines)
+                 (apply str lines))
                [:div ":END:"]]
               {:default-collapsed? true
                :title-trigger? true})]]])

+ 3 - 1
src/main/frontend/components/editor.cljs

@@ -74,7 +74,9 @@
                                    (not (contains? #{"Date picker" "Template" "Deadline" "Scheduled" "Upload an image"} command))))]
               (editor-handler/insert-command! id command-steps
                                               format
-                                              {:restore? restore-slash?}))))
+                                              {:restore? restore-slash?})
+              (state/pub-event! [:instrument {:type :editor/command-triggered
+                                              :payload {:command command}}]))))
         :class
         "black"}))))
 

+ 4 - 0
src/main/frontend/components/header.css

@@ -191,4 +191,8 @@ html.is-ios.is-safari {
 
 html.is-native-ios {
   --ls-headbar-inner-top-padding: 36px;
+
+  .cp__header > .r {
+    display: flex;
+  }
 }

+ 5 - 4
src/main/frontend/components/lazy_editor.cljs

@@ -1,9 +1,9 @@
 (ns frontend.components.lazy-editor
-  (:require [rum.core :as rum]
+  (:require [clojure.string :as string]
+            [rum.core :as rum]
             [shadow.lazy :as lazy]
             [frontend.ui :as ui]
-            [frontend.state :as state]
-            [frontend.text :as text]))
+            [frontend.state :as state]))
 
 (def lazy-editor (lazy/loadable frontend.extensions.code/editor))
 
@@ -18,7 +18,8 @@
   [config id attr code options]
   (let [loaded? (rum/react loaded?)
         theme (state/sub :ui/theme)
-        code (when code (text/remove-indentations code))]
+        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"))))

+ 2 - 2
src/main/frontend/components/page.cljs

@@ -582,7 +582,7 @@
     (global-graph-inner graph settings theme)))
 
 (rum/defc page-graph-inner < rum/static
-  [graph dark?]
+  [page graph dark?]
   [:div.sidebar-item.flex-col
    (graph/graph-2d {:nodes (:nodes graph)
                     :links (:links graph)
@@ -605,7 +605,7 @@
                 (graph-handler/build-block-graph (uuid page) theme)
                 (graph-handler/build-page-graph page theme))]
     (when (seq (:nodes graph))
-      (page-graph-inner graph dark?))))
+      (page-graph-inner page graph dark?))))
 
 (defn- sort-pages-by
   [by-item desc? pages]

+ 5 - 1
src/main/frontend/components/query_table.cljs

@@ -128,7 +128,11 @@
 
                              :block       ; block title
                              (let [content (:block/content item)
-                                   {:block/keys [title]} (block/parse-title-and-body (:block/format item) (:block/pre-block? item) content)]
+                                   {:block/keys [title]} (block/parse-title-and-body
+                                                          (:block/uuid item)
+                                                          (:block/format item)
+                                                          (:block/pre-block? item)
+                                                          content)]
                                (if (seq title)
                                  [:element (->elem :div (map-inline config title))]
                                  [:string content]))

+ 5 - 5
src/main/frontend/components/repo.cljs

@@ -76,11 +76,11 @@
             (let [local? (config/local-db? url)]
               [:div.flex.justify-between.mb-4 {:key id}
                (if local?
-                 [:a
-                  {:title url ;; show full path on hover
-                   :on-click #(open-repo-url url)}
-                  (some-> (config/get-local-dir url)
-                          (text/get-graph-name-from-path))]
+                 (let [local-dir (config/get-local-dir url)
+                       graph-name (text/get-graph-name-from-path local-dir)]
+                   [:a {:title local-dir
+                        :on-click #(open-repo-url url)}
+                    graph-name])
                  [:a {:target "_blank"
                       :href url}
                   (db/get-repo-path url)])

+ 57 - 51
src/main/frontend/components/search.cljs

@@ -33,38 +33,39 @@
   [content q]
   (if (or (string/blank? content) (string/blank? q))
     content
-    (let [q-words (string/split q #" ")
-          lc-content (string/lower-case content)
-          lc-q (string/lower-case q)]
-      (if (and (string/includes? lc-content lc-q)
-              (not (util/safe-re-find #" " q)))
-        (let [i (string/index-of lc-content lc-q)
-              [before after] [(subs content 0 i) (subs content (+ i (count q)))]]
-          [:div
-           (when-not (string/blank? before)
-             [:span before])
-           [:mark {:class "p-0 rounded-none"} (subs content i (+ i (count q)))]
-           (when-not (string/blank? after)
-             [:span after])])
-        (let [elements (loop [words q-words
-                              content content
-                              result []]
-                         (if (and (seq words) content)
-                           (let [word (first words)
-                                 lc-word (string/lower-case word)
-                                 lc-content (string/lower-case content)]
-                             (if-let [i (string/index-of lc-content lc-word)]
-                               (recur (rest words)
-                                      (subs content (+ i (count word)))
-                                      (vec
-                                       (concat result
-                                               [[:span (subs content 0 i)]
-                                                [:mark (subs content i (+ i (count word)))]])))
-                               (recur nil
-                                      content
-                                      result)))
-                           (conj result [:span content])))]
-          [:p {:class "m-0"} elements])))))
+    (when (and content q)
+      (let [q-words (string/split q #" ")
+            lc-content (string/lower-case content)
+            lc-q (string/lower-case q)]
+        (if (and (string/includes? lc-content lc-q)
+                 (not (util/safe-re-find #" " q)))
+          (let [i (string/index-of lc-content lc-q)
+                [before after] [(subs content 0 i) (subs content (+ i (count q)))]]
+            [:div
+             (when-not (string/blank? before)
+               [:span before])
+             [:mark {:class "p-0 rounded-none"} (subs content i (+ i (count q)))]
+             (when-not (string/blank? after)
+               [:span after])])
+          (let [elements (loop [words q-words
+                                content content
+                                result []]
+                           (if (and (seq words) content)
+                             (let [word (first words)
+                                   lc-word (string/lower-case word)
+                                   lc-content (string/lower-case content)]
+                               (if-let [i (string/index-of lc-content lc-word)]
+                                 (recur (rest words)
+                                        (subs content (+ i (count word)))
+                                        (vec
+                                         (concat result
+                                                 [[:span (subs content 0 i)]
+                                                  [:mark (subs content i (+ i (count word)))]])))
+                                 (recur nil
+                                        content
+                                        result)))
+                             (conj result [:span content])))]
+            [:p {:class "m-0"} elements]))))))
 
 (rum/defc search-result-item
   [type content]
@@ -148,16 +149,18 @@
                                         (cond->
                                           {:type :page
                                            :data page}
-                                          (not= (string/lower-case page)
-                                                (string/lower-case alias))
-                                          (assoc :alias alias)))) pages))
+                                          (and alias
+                                               (not= (string/lower-case page)
+                                                     (string/lower-case alias)))
+                                          (assoc :alias alias))))
+                                 (remove nil? pages)))
           files (when-not all? (map (fn [file] {:type :file :data file}) files))
           blocks (map (fn [block] {:type :block :data block}) blocks)
           search-mode (state/sub :search/mode)
           new-page (if (or
                         (and (seq pages)
-                             (= (string/lower-case search-q)
-                                (string/lower-case (:data (first pages)))))
+                             (= (util/safe-lower-case search-q)
+                                (util/safe-lower-case (:data (first pages)))))
                         (nil? result)
                         all?)
                      []
@@ -205,12 +208,13 @@
                             (case type
                               :page
                               (let [data (or alias data)
-                                    page (db/entity [:block/name (string/lower-case data)])]
-                                (state/sidebar-add-block!
-                                 (state/get-current-repo)
-                                 (:db/id page)
-                                 :page
-                                 {:page page}))
+                                    page (when data (db/entity [:block/name (string/lower-case data)]))]
+                                (when page
+                                  (state/sidebar-add-block!
+                                   (state/get-current-repo)
+                                   (:db/id page)
+                                   :page
+                                   {:page page})))
 
                               :block
                               (let [block-uuid (uuid (:block/uuid data))
@@ -297,6 +301,7 @@
    (let [recent-search (mapv (fn [q] {:type :search :data q}) (db/get-key-value :recent/search))
          pages (->> (db/get-key-value :recent/pages)
                     (remove nil?)
+                    (filter string?)
                     (remove #(= (string/lower-case %) "contents"))
                     (mapv (fn [page] {:type :page :data page})))
          result (concat (take 5 recent-search) pages)]
@@ -324,12 +329,13 @@
                           (case type
                             :page
                             (let [page data]
-                              (when-let [page (db/pull [:block/name (string/lower-case page)])]
-                               (state/sidebar-add-block!
-                                (state/get-current-repo)
-                                (:db/id page)
-                                :page
-                                {:page page})))
+                              (when (string? page)
+                                (when-let [page (db/pull [:block/name (string/lower-case page)])]
+                                 (state/sidebar-add-block!
+                                  (state/get-current-repo)
+                                  (:db/id page)
+                                  :page
+                                  {:page page}))))
 
                             nil))
        :item-render (fn [{:keys [type data]}]
@@ -386,8 +392,8 @@
                                  (search-handler/clear-search! false)
                                  (let [search-mode (state/get-search-mode)
                                        opts (if (= :page search-mode)
-                                              (let [current-page (or (state/get-current-page)
-                                                                     (date/today))]
+                                              (when-let [current-page (or (state/get-current-page)
+                                                                          (date/today))]
                                                 {:page-db-id (:db/id (db/entity [:block/name (string/lower-case current-page)]))})
                                               {})]
                                    (state/set-q! value)

+ 1 - 3
src/main/frontend/components/settings.cljs

@@ -219,9 +219,7 @@
 
 (rum/defcs switch-spell-check-row < rum/reactive
   [state t]
-  (let [enabled? (state/sub [:electron/user-cfgs :spell-check])
-        enabled? (if (nil? enabled?) true enabled?)]
-
+  (let [enabled? (state/sub [:electron/user-cfgs :spell-check])]
     [:div.it.sm:grid.sm:grid-cols-3.sm:gap-4.sm:items-start
      [:label.block.text-sm.font-medium.leading-5.opacity-70
       (t :settings-page/spell-checker)]

+ 2 - 2
src/main/frontend/components/sidebar.cljs

@@ -336,8 +336,8 @@
              (ui/loading (t :loading))]]
 
            :else
-           [:div.pb-24 {:class (if global-graph-pages? "" (util/hiccup->class "max-w-7xl.mx-auto"))
-                        :style {:margin-bottom (if global-graph-pages? 0 120)}}
+           [:div {:class (if global-graph-pages? "" (util/hiccup->class "max-w-7xl.mx-auto.pb-24"))
+                  :style {:margin-bottom (if global-graph-pages? 0 120)}}
             main-content])]]])))
 
 (rum/defc footer

+ 3 - 3
src/main/frontend/components/sidebar.css

@@ -188,7 +188,7 @@
   width: 0;
   height: 100vh;
   position: fixed;
-  top: 0;
+  top: var(--ls-headbar-inner-top-padding);
   left: 0;
   z-index: 9;
   transition: width 1.2s;
@@ -224,11 +224,11 @@
     height: 3rem;
     width: calc(var(--ls-left-sidebar-sm-width) - var(--ls-scrollbar-width));
     transition: width .3s;
-    background-color: transparent;
+    background-color: var(--ls-primary-background-color);
     position: fixed;
     left: 0;
     top: 0;
-    opacity: .8;
+    opacity: 1;
     z-index: 5;
   }
 

+ 6 - 0
src/main/frontend/components/theme.css

@@ -36,6 +36,12 @@ html {
   }
 }
 
+@media (prefers-color-scheme: dark) {
+  .preboot-loading {
+    color: lightgray;
+  }
+}
+
 .form-checkbox {
   color: var(--ls-page-checkbox-color, #6093a0);
   background-color: var(--ls-page-checkbox-color, #6093a0);

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

@@ -34,7 +34,7 @@
 
  [frontend.db.model
   block-and-children-transform blocks-count blocks-count-cache clean-export!  cloned? delete-blocks get-pre-block
-  delete-file! delete-file-blocks! delete-file-pages! delete-file-tx delete-files delete-pages-by-files
+  delete-file! delete-file-blocks! delete-page-blocks delete-file-pages! delete-file-tx delete-files delete-pages-by-files
   filter-only-public-pages-and-blocks get-all-block-contents get-all-tagged-pages
   get-all-templates get-block-and-children get-block-by-uuid get-block-children sort-by-left
   get-block-parent get-block-parents parents-collapsed? get-block-referenced-blocks

+ 44 - 30
src/main/frontend/db/model.cljs

@@ -473,23 +473,24 @@
   ([repo-url page {:keys [use-cache? pull-keys]
                    :or {use-cache? true
                         pull-keys '[*]}}]
-   (let [page (string/lower-case (string/trim page))
-         page-entity (or (db-utils/entity repo-url [:block/name page])
-                         (db-utils/entity repo-url [:block/original-name page]))
-         page-id (:db/id page-entity)
-         db (conn/get-conn repo-url)]
-     (when page-id
-       (some->
-        (react/q repo-url [:page/blocks page-id]
-                 {:use-cache? use-cache?
-                  :transform-fn #(page-blocks-transform repo-url %)
-                  :query-fn (fn [db]
-                              (let [datoms (d/datoms db :avet :block/page page-id)
-                                    block-eids (mapv :e datoms)]
-                                (db-utils/pull-many repo-url pull-keys block-eids)))}
-                 nil)
-        react
-        (flatten-blocks-sort-by-left page-entity))))))
+   (when page
+     (let [page (string/lower-case (string/trim page))
+           page-entity (or (db-utils/entity repo-url [:block/name page])
+                           (db-utils/entity repo-url [:block/original-name page]))
+           page-id (:db/id page-entity)
+           db (conn/get-conn repo-url)]
+       (when page-id
+         (some->
+          (react/q repo-url [:page/blocks page-id]
+            {:use-cache? use-cache?
+             :transform-fn #(page-blocks-transform repo-url %)
+             :query-fn (fn [db]
+                         (let [datoms (d/datoms db :avet :block/page page-id)
+                               block-eids (mapv :e datoms)]
+                           (db-utils/pull-many repo-url pull-keys block-eids)))}
+            nil)
+          react
+          (flatten-blocks-sort-by-left page-entity)))))))
 
 (defn get-page-blocks-no-cache
   ([page]
@@ -498,15 +499,16 @@
    (get-page-blocks-no-cache repo-url page nil))
   ([repo-url page {:keys [pull-keys]
                    :or {pull-keys '[*]}}]
-   (let [page (string/lower-case page)
-         page-id (or (:db/id (db-utils/entity repo-url [:block/name page]))
-                     (:db/id (db-utils/entity repo-url [:block/original-name page])))
-         db (conn/get-conn repo-url)]
-     (when page-id
-       (let [datoms (d/datoms db :avet :block/page page-id)
-             block-eids (mapv :e datoms)]
-         (some->> (db-utils/pull-many repo-url pull-keys block-eids)
-                  (page-blocks-transform repo-url)))))))
+   (when page
+     (let [page (string/lower-case page)
+           page-id (or (:db/id (db-utils/entity repo-url [:block/name page]))
+                       (:db/id (db-utils/entity repo-url [:block/original-name page])))
+           db (conn/get-conn repo-url)]
+       (when page-id
+         (let [datoms (d/datoms db :avet :block/page page-id)
+               block-eids (mapv :e datoms)]
+           (some->> (db-utils/pull-many repo-url pull-keys block-eids)
+                    (page-blocks-transform repo-url))))))))
 
 (defn get-page-blocks-count
   [repo page-id]
@@ -697,10 +699,12 @@
         first)))))
 
 (defn get-page-file
-  [page-name]
-  (some-> (or (db-utils/entity [:block/name page-name])
-              (db-utils/entity [:block/original-name page-name]))
-          :block/file))
+  ([page-name]
+   (get-page-file (state/get-current-repo) page-name))
+  ([repo page-name]
+   (some-> (or (db-utils/entity repo [:block/name page-name])
+               (db-utils/entity repo [:block/original-name page-name]))
+           :block/file)))
 
 (defn get-block-file-path
   [block]
@@ -1345,6 +1349,16 @@
   (let [blocks (get-file-blocks repo-url path)]
     (mapv (fn [eid] [:db.fn/retractEntity eid]) blocks)))
 
+(defn delete-page-blocks
+  [repo-url page]
+  (when page
+    (let [db (conn/get-conn repo-url)
+          page (db-utils/pull [:block/name (string/lower-case page)])]
+      (when page
+        (let [datoms (d/datoms db :avet :block/page (:db/id page))
+              block-eids (mapv :e datoms)]
+          (mapv (fn [eid] [:db.fn/retractEntity eid]) block-eids))))))
+
 (defn delete-file-pages!
   [repo-url path]
   (let [pages (get-file-pages repo-url path)]

+ 6 - 3
src/main/frontend/db/query_dsl.cljs

@@ -417,7 +417,8 @@
              (not (string/blank? s)))
     (let [counter (atom 0)]
       (try
-        (let [form (some-> s
+        (let [s (if (= \# (first s)) (util/format "[[%s]]" (subs s 1)) s)
+              form (some-> s
                            (pre-transform)
                            (reader/read-string))]
           (if (symbol? form)
@@ -460,8 +461,10 @@
     (let [query-string (template/resolve-dynamic-template! query-string)]
       (when-not (string/blank? query-string)
         (let [{:keys [query sort-by blocks? sample] :as result} (parse repo query-string)
-              query (if (string? query) (string/trim query) query)]
-          (if (and (string? result) (not (string/includes? result " ")))
+              query (if (string? query) (string/trim query) query)
+              full-text-query? (and (string? result)
+                                    (not (string/includes? result " ")))]
+          (if full-text-query?
             (if (= "\"" (first result) (last result))
               (subs result 1 (dec (count result)))
               result)

+ 13 - 30
src/main/frontend/extensions/code.cljs

@@ -45,8 +45,8 @@
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.file :as file-handler]
             [frontend.state :as state]
+            [frontend.utf8 :as utf8]
             [frontend.util :as util]
-            [frontend.text :as text]
             [goog.dom :as gdom]
             [goog.object :as gobj]
             [rum.core :as rum]))
@@ -68,29 +68,15 @@
       (cond
         (:block/uuid config)
         (let [block (db/pull [:block/uuid (:block/uuid config)])
-              format (:block/format block)
               content (:block/content block)
-              {:keys [lines]} (last (:rum/args state))
-              full-content (:full_content (last (:rum/args state)))
-              value (text/remove-indentations value)]
-          (when full-content
-            (let [lines (string/split-lines full-content)
-                  fl (first lines)
-                  ll (last lines)]
-              (when (and fl ll)
-                (let [src (->> (subvec (vec lines) 1 (dec (count lines)))
-                               (string/join "\n"))
-                      src (text/remove-indentations src)
-                      full-content (str (string/trim fl)
-                                        (if (seq src)
-                                          (str "\n" src "\n")
-                                          "\n")
-                                        (string/trim ll))]
-                  (when (string/includes? content full-content)
-                    (let [value' (str (string/trim fl) "\n" value "\n" (string/trim ll))
-                          ;; FIXME: What if there're multiple code blocks with the same value?
-                          content' (string/replace-first content full-content value')]
-                      (editor-handler/save-block-if-changed! block content'))))))))
+              {:keys [start_pos end_pos]} (:pos_meta (last (:rum/args state)))
+              raw-content (utf8/encode content) ;; NOTE: :pos_meta is based on byte position
+              prefix (utf8/decode (.slice raw-content 0 (- start_pos 2)))
+              surfix (utf8/decode (.slice raw-content (- end_pos 2)))
+              new-content (if (string/blank? value)
+                            (str prefix surfix)
+                            (str prefix value "\n" surfix))]
+          (editor-handler/save-block-if-changed! block new-content))
 
         (:file-path config)
         (let [path (:file-path config)
@@ -137,9 +123,8 @@
                               (get-in config [:block :block/uuid])))
         _ (state/set-state! :editor/code-mode? false)
         original-mode (get attr :data-lang)
-        mode original-mode
-        clojure? (contains? #{"clojure" "clj" "text/x-clojure" "cljs" "cljc"} mode)
-        mode (if clojure? "clojure" (text->cm-mode mode))
+        clojure? (contains? #{"clojure" "clj" "text/x-clojure" "cljs" "cljc"} original-mode)
+        mode (if clojure? "clojure" (text->cm-mode original-mode))
         lisp? (or clojure?
                   (contains? #{"scheme" "racket" "lisp"} mode))
         textarea (gdom/getElement id)
@@ -152,14 +137,12 @@
                                      :lineNumbers true
                                      :styleActiveLine true
                                      :extraKeys #js {"Esc" (fn [cm]
+                                                             (reset! esc-pressed? true)
                                                              (save-file-or-block-when-blur-or-esc! cm textarea config state)
                                                              (when-let [block-id (:block/uuid config)]
-                                                               (let [block (db/pull [:block/uuid block-id])
-                                                                     value (.getValue cm)
-                                                                     textarea-value (gobj/get textarea "value")]
+                                                               (let [block (db/pull [:block/uuid block-id])]
                                                                  (editor-handler/edit-block! block :max block-id)))
                                                              ;; TODO: return "handled" or false doesn't always prevent event bubbles
-                                                             (reset! esc-pressed? true)
                                                              (js/setTimeout #(reset! esc-pressed? false) 10))}}))]
     (when editor
       (let [textarea-ref (rum/ref-node state textarea-ref-name)]

+ 11 - 4
src/main/frontend/extensions/code.css

@@ -1,12 +1,17 @@
 .extensions__code {
   @apply relative;
   z-index: 0;
-
+  display: flex;
+  flex-direction: row;
+  flex-wrap: nowrap;
+  justify-content: space-between;
   &-lang {
-    @apply absolute right-0 p-1 text-sm;
-    top: 3px;
-    z-index: 1;
+    @apply p-1 text-sm;
+    margin-top: 3px;
     background: var(--ls-secondary-background-color);
+    word-break: keep-all;
+    height:2rem;
+    order:3;
   }
 
   &-calc {
@@ -31,6 +36,8 @@
   }
 
   > .CodeMirror {
+    order: 1;
+    width: 100%;
     z-index: 0;
     height: auto;
     margin-top: 4px;

+ 6 - 11
src/main/frontend/extensions/graph.cljs

@@ -6,13 +6,6 @@
             [goog.object :as gobj]
             [rum.core :as rum]))
 
-(defn- highlight-node!
-  [^js graph node]
-  (.resetNodeStyle graph node
-                   (bean/->js {:color "#6366F1"
-                               :border {:width 2
-                                        :color "#6366F1"}})))
-
 (defn- highlight-neighbours!
   [^js graph node focus-nodes dark?]
   (.forEachNeighbor
@@ -61,14 +54,16 @@
 (rum/defcs graph-2d <
   (rum/local nil :ref)
   {:did-update pixi/render!
+   :should-update (fn [old-state new-state]
+                    (not= (select-keys (first (:rum/args old-state))
+                                       [:nodes :links :dark?])
+                          (select-keys (first (:rum/args new-state))
+                                       [:nodes :links :dark?])))
    :will-unmount (fn [state]
-                   (when-let [graph (:graph state)]
-                     (.destroy graph))
                    (reset! pixi/*graph-instance nil)
                    state)}
   [state opts]
-  [:div.graph {:style {:height "100vh"}
-               :ref (fn [value]
+  [:div.graph {:ref (fn [value]
                       (let [ref (get state :ref)]
                         (when (and ref value)
                           (reset! ref value))))}])

+ 4 - 0
src/main/frontend/extensions/graph.css

@@ -7,3 +7,7 @@
     position: relative;
     z-index: 4;
 }
+
+.graph {
+    height: calc(100vh - 100px) !important;
+}

+ 107 - 51
src/main/frontend/extensions/graph/pixi.cljs

@@ -7,6 +7,9 @@
             ["graphology" :as graphology]
             ["pixi-graph-fork" :as Pixi-Graph]))
 
+(defonce *graph-instance (atom nil))
+(defonce *simulation (atom nil))
+
 (def Graph (gobj/get graphology "Graph"))
 
 (defonce colors
@@ -38,7 +41,6 @@
                    :type     (.-TEXT (.-TextType Pixi-Graph))
                    :fontSize 12
                    :color (if dark? "rgba(255, 255, 255, 0.8)" "rgba(0, 0, 0, 0.8)")
-                   ;                  :backgroundColor "rgba(255, 255, 255, 0.5)"
                    :padding  4}}
    :edge {:width 1
           :color (if dark? "#094b5a" "#cccccc")}})
@@ -46,15 +48,14 @@
 (defn default-hover-style
   [dark?]
   {:node {:color  "#6366F1"
-          :border {:width 2
-                   :color "#6366F1"}
           :label  {:backgroundColor "rgba(238, 238, 238, 1)"
                    :color           "#333333"}}
    :edge {:color "#A5B4FC"}})
 
 (defn layout!
   [nodes links]
-  (let [simulation (forceSimulation nodes)]
+  (let [nodes-count (count nodes)
+        simulation (forceSimulation nodes)]
     (-> simulation
         (.force "link"
                 (-> (forceLink)
@@ -63,19 +64,19 @@
                     (.links links)))
         (.force "charge"
                 (-> (forceManyBody)
-                    (.distanceMax 4000)
+                    (.distanceMax (if (> nodes-count 500) 4000 600))
                     (.theta 0.5)
                     (.strength -600)))
         (.force "collision"
                 (-> (forceCollide)
-                    (.radius (+ 8 18))))
+                    (.radius (+ 8 18))
+                    (.iterations 2)))
         (.force "x" (-> (forceX 0) (.strength 0.02)))
         (.force "y" (-> (forceY 0) (.strength 0.02)))
         (.force "center" (forceCenter))
-        (.tick 3)
-        (.stop))))
-
-(defonce *graph-instance (atom nil))
+        (.velocityDecay 0.8))
+    (reset! *simulation simulation)
+    simulation))
 
 (defn- clear-nodes!
   [graph]
@@ -83,63 +84,118 @@
                 (fn [node]
                   (.dropNode graph node))))
 
+;; (defn- clear-edges!
+;;   [graph]
+;;   (.forEachEdge graph
+;;                 (fn [edge]
+;;                   (.dropEdge graph edge))))
+
 (defn destroy-instance!
   []
   (when-let [instance (:pixi @*graph-instance)]
     (.destroy instance)
-    (reset! *graph-instance nil)))
+    (reset! *graph-instance nil)
+    (reset! *simulation nil)))
 
 (defonce *dark? (atom nil))
 
+(defn- update-position!
+  [node obj]
+  (.updatePosition node #js {:x (.-x obj)
+                             :y (.-y obj)}))
+
+(defn- tick!
+  [pixi graph nodes-js links-js]
+  (fn []
+    (let [nodes-objects (.getNodesObjects pixi)
+          edges-objects (.getEdgesObjects pixi)]
+      (doseq [node nodes-js]
+        (when-let [node-object (.get nodes-objects (.-id node))]
+          (update-position! node-object node)))
+      (doseq [edge links-js]
+        (when-let [edge-object (.get edges-objects (str (.-index edge)))]
+          (.updatePosition edge-object
+                           #js {:x (.-x (.-source edge))
+                                :y (.-y (.-source edge))}
+                           #js {:x (.-x (.-target edge))
+                                :y (.-y (.-target edge))}))))))
+
+(defn- set-up-listeners!
+  [pixi-graph]
+  (when pixi-graph
+    ;; drag start
+    (let [*dragging? (atom false)
+          nodes (.getNodesObjects pixi-graph)
+          on-drag-end (fn [node event]
+                        (.stopPropagation event)
+                        (when-let [s @*simulation]
+                          (when-not (.-active event)
+                            (.alphaTarget s 0)))
+                        (reset! *dragging? false))]
+      (.on pixi-graph "nodeMousedown"
+           (fn [event node-key]
+             (when-let [node (.get nodes node-key)]
+               (when-let [s @*simulation]
+                 (when-not (.-active event)
+                   (-> (.alphaTarget s 0.3)
+                       (.restart))
+                   (js/setTimeout #(.alphaTarget s 0) 2000))
+                 (reset! *dragging? true)))))
+
+      (.on pixi-graph "nodeMouseup"
+           (fn [event node-key]
+             (when-let [node (.get nodes node-key)]
+               (on-drag-end node event))))
+
+      (.on pixi-graph "nodeMousemove"
+           (fn [event node-key]
+             (when-let [node (.get nodes node-key)]
+               (when @*dragging?
+                 (update-position! node event))))))))
+
 (defn render!
   [state]
-  (let [dark? (:dark? (first (:rum/args state)))]
-    (when (and (some? @*dark?) (not= @*dark? dark?))
-      (destroy-instance!))
-    (reset! *dark? dark?))
-
   (try
-    (let [old-instance         @*graph-instance
-          {:keys [graph pixi]} old-instance]
-      (when (and graph pixi)
-            (clear-nodes! graph))
-      (let [{:keys [nodes links style hover-style height register-handlers-fn dark?]} (first (:rum/args state))
-            style                                                                     (or style (default-style dark?))
-            hover-style                                                               (or hover-style (default-hover-style dark?))
-            graph                                                                     (or graph (Graph.))
-            nodes-set                                                                 (set (map :id nodes))
-            links                                                                     (->>
-                                                                                        (filter
-                                                                                          (fn [link]
-                                                                                            (and (nodes-set (:source link)) (nodes-set (:target link))))
-                                                                                          links)
-                                                                                        (distinct))
-            nodes                                                                     (remove nil? nodes)
-            links                                                                     (remove (fn [{:keys [source target]}] (or (nil? source) (nil? target))) links)
-            nodes-js                                                                  (bean/->js nodes)
-            links-js                                                                  (bean/->js links)]
-        (layout! nodes-js links-js)
+    (when @*graph-instance
+      (clear-nodes! (:graph @*graph-instance))
+      (destroy-instance!))
+    (let [{:keys [nodes links style hover-style height register-handlers-fn dark?]} (first (:rum/args state))
+          style                                                                     (or style (default-style dark?))
+          hover-style                                                               (or hover-style (default-hover-style dark?))
+          graph                                                                     (Graph.)
+          nodes-set                                                                 (set (map :id nodes))
+          links                                                                     (->>
+                                                                                     (filter
+                                                                                      (fn [link]
+                                                                                        (and (nodes-set (:source link)) (nodes-set (:target link))))
+                                                                                      links)
+                                                                                     (distinct))
+          nodes                                                                     (remove nil? nodes)
+          links                                                                     (remove (fn [{:keys [source target]}] (or (nil? source) (nil? target))) links)
+          nodes-js                                                                  (bean/->js nodes)
+          links-js                                                                  (bean/->js links)]
+      (let [simulation (layout! nodes-js links-js)]
         (doseq [node nodes-js]
           (.addNode graph (.-id node) node))
         (doseq [link links-js]
           (let [source (.-id (.-source link))
                 target (.-id (.-target link))]
             (.addEdge graph source target link)))
-        (if pixi
-          (.resetView pixi)
-          (when-let [container-ref (:ref state)]
-            (let [pixi-graph (new (.-PixiGraph Pixi-Graph)
-                                  (bean/->js
-                                   {:container  @container-ref
-                                    :graph      graph
-                                    :style      style
-                                    :hoverStyle hover-style
-                                    :height     height}))]
-              (reset! *graph-instance
-                      {:graph graph
-                       :pixi  pixi-graph})
-              (when register-handlers-fn
-                (register-handlers-fn pixi-graph)))))))
+        (when-let [container-ref (:ref state)]
+          (let [pixi-graph (new (.-PixiGraph Pixi-Graph)
+                                (bean/->js
+                                 {:container  @container-ref
+                                  :graph      graph
+                                  :style      style
+                                  :hoverStyle hover-style
+                                  :height     height}))]
+            (reset! *graph-instance
+                    {:graph graph
+                     :pixi  pixi-graph})
+            (when register-handlers-fn
+              (register-handlers-fn pixi-graph))
+            (set-up-listeners! pixi-graph)
+            (.on simulation "tick" (tick! pixi-graph graph nodes-js links-js))))))
     (catch js/Error e
       (js/console.error e)))
   state)

+ 29 - 14
src/main/frontend/format/block.cljs

@@ -12,7 +12,8 @@
             [frontend.util :as util]
             [frontend.util.property :as property]
             [lambdaisland.glogi :as log]
-            [medley.core :as medley]))
+            [medley.core :as medley]
+            [frontend.format.mldoc :as mldoc]))
 
 (defn heading-block?
   [block]
@@ -20,6 +21,12 @@
    (vector? block)
    (= "Heading" (first block))))
 
+(defn properties-block?
+  [block]
+  (and
+   (vector? block)
+   (= "Properties" (first block))))
+
 (defn get-tag
   [block]
   (when-let [tag-value (and (vector? block)
@@ -724,21 +731,29 @@
   ([block]
    (when (map? block)
      (merge block
-            (parse-title-and-body (:block/format block)
+            (parse-title-and-body (:block/uuid block)
+                                  (:block/format block)
                                   (:block/pre-block? block)
                                   (:block/content block)))))
-  ([format pre-block? content]
-   (let [content (if pre-block? content
-                     (str (config/get-block-pattern format) " " (string/triml content)))
-         content (property/remove-properties format content)
-         ast (->> (format/to-edn content format nil)
-                  (map first))
-         title (when (heading-block? (first ast))
-                 (:title (second (first ast))))]
-     (cond->
-       {:block/body (vec (if title (rest ast) ast))}
-       title
-       (assoc :block/title title)))))
+  ([block-uuid format pre-block? content]
+   (when-not (string/blank? content)
+     (let [content (if pre-block? content
+                       (str (config/get-block-pattern format) " " (string/triml content)))
+           content (property/remove-properties format content)]
+       (if-let [result (state/get-block-ast block-uuid content)]
+         result
+         (let [ast (->> (format/to-edn content format (mldoc/default-config format))
+                        (map first))
+               title (when (heading-block? (first ast))
+                       (:title (second (first ast))))
+               body (vec (if title (rest ast) ast))
+               body (drop-while properties-block? body)
+               result (cond->
+                        (if (seq body) {:block/body body} {})
+                        title
+                        (assoc :block/title title))]
+           (state/add-block-ast-cache! block-uuid content result)
+           result))))))
 
 (defn macro-subs
   [macro-content arguments]

+ 1 - 0
src/main/frontend/fs/capacitor_fs.cljs

@@ -47,6 +47,7 @@
                              files (->> files
                                         (remove (fn [file]
                                                   (or (string/starts-with? file ".")
+                                                      (string/starts-with? file "#")
                                                       (= file "bak")))))
                              files (->> files
                                         (map (fn [file]

+ 12 - 9
src/main/frontend/handler.cljs

@@ -55,17 +55,19 @@
             (let [repo (state/get-current-repo)]
               (when-not (state/nfs-refreshing?)
                 ;; Don't create the journal file until user writes something
-                (page-handler/create-today-journal!))
-
-              (when (and (state/input-idle? repo)
-                         (> (- (util/time-ms) @cards-last-check-time)
-                            (* 60 1000)))
-                (let [total (srs/get-srs-cards-total)]
-                  (state/set-state! :srs/cards-due-count total)
-                  (reset! cards-last-check-time (util/time-ms))))))]
+                (page-handler/create-today-journal!))))]
     (f)
     (js/setInterval f 5000)))
 
+(defn- instrument!
+  []
+  (let [total (srs/get-srs-cards-total)]
+    (state/set-state! :srs/cards-due-count total)
+    (state/pub-event! [:instrument {:type :flashcards/count
+                                    :payload {:total (or total 0)}}])
+    (state/pub-event! [:instrument {:type :blocks/count
+                                    :payload {:total (db/blocks-count)}}])))
+
 (defn store-schema!
   []
   (storage/set :db-schema (assoc db-schema/schema
@@ -235,7 +237,8 @@
       (enable-datalog-console))
     (when (util/electron?)
       (el/listen!))
-    (mobile/init!)))
+    (mobile/init!)
+    (js/setTimeout instrument! (* 60 1000))))
 
 (defn stop! []
   (prn "stop!"))

+ 41 - 34
src/main/frontend/handler/editor.cljs

@@ -348,9 +348,9 @@
 
 (defn wrap-parse-block
   [{:block/keys [content format left page uuid level pre-block?] :as block}]
-  (let [block (merge
-               (or (and (:db/id block) (db/pull (:db/id block))) block)
-               (block/parse-title-and-body format pre-block? content))
+  (let [block (or (and (:db/id block) (db/pull (:db/id block))) block)
+        block (merge block
+                     (block/parse-title-and-body uuid format pre-block? (:block/content block)))
         properties (:block/properties block)
         real-content (:block/content block)
         content (if (and (seq properties) real-content (not= real-content content))
@@ -707,8 +707,13 @@
                             (assoc :block/uuid (or custom-uuid (db/new-block-id))))
               [block-m sibling?] (cond
                                    before?
-                                   (let [block (db/pull (:db/id (:block/left block)))
-                                         sibling? (if (:block/name block) false sibling?)]
+                                   (let [first-child? (->> [:block/parent :block/left]
+                                                           (map #(:db/id (get block %)))
+                                                           (apply =))
+                                         block (db/pull (:db/id (:block/left block)))
+                                         sibling? (if (or first-child? ;; insert as first child
+                                                          (:block/name block))
+                                                    false sibling?)]
                                      [block sibling?])
 
                                    sibling?
@@ -1113,18 +1118,19 @@
     (state/exit-editing-and-set-selected-blocks! [block])))
 
 (defn- blocks-with-level
+  "Should be sorted already."
   [blocks]
-  (let [level-blocks (mapv #(assoc % :level 1) blocks)
-        level-blocks-map (into {} (mapv (fn [b] [(:db/id b) b]) level-blocks))
-        [level-blocks-map _]
-        (reduce (fn [[r state] [id block]]
-                  (if-let [parent-level (get-in state [(:db/id (:block/parent block)) :level])]
-                    [(conj r [id (assoc block :level (inc parent-level))])
-                     (assoc-in state [(:db/id block) :level] (inc parent-level))]
-                    [(conj r [id block])
-                     state]))
-                [{} level-blocks-map] level-blocks-map)]
-    level-blocks-map))
+  (let [root (assoc (first blocks) :level 1)]
+    (loop [m [[(:db/id root) root]]
+           blocks (rest blocks)]
+      (if (empty? blocks)
+        m
+        (let [block (first blocks)
+              parent-id (:db/id (:block/parent block))
+              parent-level (:level (second (first (filter (fn [x] (= (first x) parent-id)) m))))
+              block (assoc block :level (inc parent-level))
+              m' (vec (conj m [(:db/id block) block]))]
+          (recur m' (rest blocks)))))))
 
 (defn- blocks-vec->tree
   [blocks]
@@ -1160,8 +1166,8 @@
                                  (vec (tree/sort-blocks (db/get-block-children repo (:block/uuid b)) b))
                                  [b])) blocks))
         block-ids* (mapv :block/uuid blocks*)
-        level-blocks-map (blocks-with-level blocks*)
-        level-blocks-uuid-map (into {} (mapv (fn [b] [(:block/uuid b) b]) (vals level-blocks-map)))
+        level-blocks (blocks-with-level blocks*)
+        level-blocks-uuid-map (into {} (mapv (fn [b] [(:block/uuid b) b]) (map second level-blocks)))
         level-blocks (mapv (fn [uuid] (get level-blocks-uuid-map uuid)) block-ids*)
         tree (blocks-vec->tree level-blocks)
         top-level-block-uuids (mapv :block/uuid (filterv #(not (vector? %)) tree))
@@ -1254,8 +1260,8 @@
                                [b])) )
                (flatten))
           block-ids* (mapv :block/uuid blocks*)
-          level-blocks-map (blocks-with-level blocks*)
-          level-blocks-uuid-map (into {} (mapv (fn [b] [(:block/uuid b) b]) (vals level-blocks-map)))
+          level-blocks (blocks-with-level blocks*)
+          level-blocks-uuid-map (into {} (mapv (fn [b] [(:block/uuid b) b]) (map second level-blocks)))
           level-blocks (mapv (fn [uuid] (get level-blocks-uuid-map uuid)) block-ids*)
           tree (blocks-vec->tree level-blocks)
           top-level-block-uuids (mapv :block/uuid (filterv #(not (vector? %)) tree))]
@@ -1372,8 +1378,11 @@
 (defn cut-block!
   [block-id]
   (when-let [block (db/pull [:block/uuid block-id])]
-    (let [content (:block/content block)]
-      (common-handler/copy-to-clipboard-without-id-property! (:block/format block) content)
+    (let [repo (state/get-current-repo)
+          content (:block/content block)
+          ;; TODO: support org mode
+          [md-content _tree] (compose-copied-blocks-contents-&-block-tree repo [block-id])]
+      (common-handler/copy-to-clipboard-without-id-property! (:block/format block) md-content)
       (delete-block-aux! block true))))
 
 (defn clear-last-selected-block!
@@ -1869,11 +1878,6 @@
           (util/stop event)
           (state/append-current-edit-content! doc-text))))))
 
-(defn- block-and-children-content
-  [block-children]
-  (-> (map :block/content block-children)
-      string/join))
-
 (defn- reorder-selected-blocks
   [blocks]
   (let [repo (state/get-current-repo)
@@ -2298,12 +2302,15 @@
            block-uuid (:block/uuid block)
            template-including-parent? (not (false? (:template-including-parent (:block/properties block))))
            blocks (if template-including-parent? (db/get-block-and-children repo block-uuid) (db/get-block-children repo block-uuid))
-           level-blocks (vals (blocks-with-level blocks))
-           grouped-blocks (group-by #(= db-id (:db/id %)) level-blocks)
-           root-block (or (first (get grouped-blocks true)) (assoc (db/pull db-id) :level 1))
-           blocks-exclude-root (get grouped-blocks false)
+           root-block (db/pull db-id)
+           blocks-exclude-root (remove (fn [b] (= (:db/id b) db-id)) blocks)
            sorted-blocks (tree/sort-blocks blocks-exclude-root root-block)
-           result-blocks (if template-including-parent? sorted-blocks (drop 1 sorted-blocks))
+           sorted-blocks (->> (blocks-with-level sorted-blocks)
+                              (map second))
+           result-blocks (if template-including-parent?
+                           sorted-blocks
+                           (->> (drop 1 sorted-blocks)
+                                (map (fn [block] (update block :level dec)))))
            tree (blocks-vec->tree result-blocks)]
        (when element-id
          (insert-command! element-id "" format {}))
@@ -2608,7 +2615,7 @@
           (cursor/move-cursor-forward input))))))
 
 (defn- delete-and-update [^js input start end]
-  (.setRangeText input "" start end)
+  (util/safe-set-range-text! input "" start end)
   (state/set-edit-content! (state/get-edit-input-id) (.-value input)))
 
 (defn- delete-concat [current-block]
@@ -3309,7 +3316,7 @@
      (map (fn [x] (dissoc x :block/children))))))
 
 (defn collapsable? [block-id]
-  (if-let [block (db-model/get-block-by-uuid block-id)]
+  (if-let [block (db-model/query-block-by-uuid block-id)]
     (let [block (block/parse-title-and-body block)]
       (and
        (nil? (-> block :block/properties :collapsed))

+ 10 - 5
src/main/frontend/handler/file.cljs

@@ -125,9 +125,9 @@
       data)))
 
 (defn- page-exists-in-another-file
-  [page file]
+  [repo-url page file]
   (when-let [page-name (:block/name page)]
-    (let [current-file (:file/path (db/get-page-file page-name))]
+    (let [current-file (:file/path (db/get-page-file repo-url page-name))]
       (when (not= file current-file)
        current-file))))
 
@@ -157,9 +157,14 @@
           utf8-content (utf8/encode content)
           file-content [{:file/path file}]]
       (p/let [tx (if (contains? config/mldoc-support-formats format)
-                   (p/let [delete-blocks (db/delete-file-blocks! repo-url file)
-                           [pages blocks] (extract-handler/extract-blocks-pages repo-url file content utf8-content)
-                           _ (when-let [current-file (page-exists-in-another-file (first pages) file)]
+                   (p/let [[pages blocks] (extract-handler/extract-blocks-pages repo-url file content utf8-content)
+                           first-page (first pages)
+                           delete-blocks (->
+                                          (concat
+                                           (db/delete-file-blocks! repo-url file)
+                                           (when first-page (db/delete-page-blocks repo-url (:block/name first-page))))
+                                          (distinct))
+                           _ (when-let [current-file (page-exists-in-another-file repo-url first-page file)]
                                (when (not= file current-file)
                                  (let [error (str "Page already exists with another file: " current-file ", current file: " file)]
                                    (state/pub-event! [:notification/show

+ 1 - 1
src/main/frontend/handler/plugin.cljs

@@ -349,7 +349,7 @@
   []
   (let [text (state/sub :plugin/indicator-text)]
     (if-not (= text "END")
-      [:div.flex.align-items.justify-center.h-screen.w-full
+      [: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.block.text-sm.relative.opacity-50 {:style {:right "-8px"}} text]]])))

+ 8 - 0
src/main/frontend/handler/route.cljs

@@ -27,6 +27,14 @@
    (when pub-event? (state/pub-event! [:redirect-to-home]))
    (redirect! {:to :home})))
 
+(defn redirect-to-all-pages!
+  []
+  (redirect! {:to :all-pages}))
+
+(defn redirect-to-graph-view!
+  []
+  (redirect! {:to :graph}))
+
 (defn redirect-to-page!
   ([page-name]
    (recent-handler/add-page-to-recent! (state/get-current-repo) page-name)

+ 2 - 2
src/main/frontend/modules/file/core.cljs

@@ -24,8 +24,8 @@
         (ffirst body))))
 
 (defn transform-content
-  [{:block/keys [format pre-block? unordered content heading-level left page scheduled deadline parent] :as block} level {:keys [heading-to-list?]}]
-  (let [{:block/keys [title body]} (block/parse-title-and-body format pre-block? content)
+  [{:block/keys [uuid format pre-block? unordered content heading-level left page scheduled deadline parent] :as block} level {:keys [heading-to-list?]}]
+  (let [{:block/keys [title body]} (block/parse-title-and-body uuid format pre-block? content)
         content (or content "")
         heading-with-title? (seq title)
         allowed-block-as-title? (allowed-block-as-title? title body)

+ 9 - 1
src/main/frontend/modules/instrumentation/posthog.cljs

@@ -1,6 +1,7 @@
 (ns frontend.modules.instrumentation.posthog
   (:require [frontend.config :as cfg]
             [frontend.util :as util]
+            [frontend.mobile.util :as mobile]
             [frontend.version :refer [version]]
             ["posthog-js" :as posthog]
             [cljs-bean.core :as bean]))
@@ -11,7 +12,14 @@
 (defn register []
   (posthog/register
    (clj->js
-    {:app_type (if (util/electron?) "electron" "web")
+    {:app_type (let [platform (mobile/platform)]
+                 (cond
+                   platform
+                   platform
+                   (util/electron?)
+                   "electron"
+                   :else
+                   "web"))
      :app_env (if cfg/dev? "development" "production")
      :app_ver version
      :schema_ver 0

+ 28 - 6
src/main/frontend/modules/shortcut/config.cljs

@@ -285,8 +285,8 @@
                                     :binding "mod+k"
                                     :fn      #(route-handler/go-to-search! :global)}
 
-   :go/journals                    {:desc    "Jump to journals"
-                                    :binding (if mac? "mod+j" "alt+j")
+   :go/journals                    {:desc    "Go to journals"
+                                    :binding "g j"
                                     :fn      route-handler/go-to-journals!}
 
    :go/backward                    {:desc    "Backwards"
@@ -325,7 +325,16 @@
 
    :go/home                        {:desc    "Go to home"
                                     :binding "g h"
-                                    :fn      #(route-handler/redirect-to-home!)}
+                                    :fn      route-handler/redirect-to-home!}
+
+   :go/all-pages                   {:desc    "Go to all pages"
+                                    :binding "g a"
+                                    :fn      route-handler/redirect-to-all-pages!}
+
+   :go/graph-view                  {:desc    "Go to graph view"
+                                    :binding "g g"
+                                    :fn      route-handler/redirect-to-graph-view!}
+
 
    :go/keyboard-shortcuts          {:desc    "Go to keyboard shortcuts"
                                     :binding "g s"
@@ -343,6 +352,13 @@
                                     :binding "g p"
                                     :fn      journal-handler/go-to-prev-journal!}
 
+   :go/flashcards                  {:desc    "Toggle flashcards"
+                                    :binding "g f"
+                                    :fn      (fn []
+                                               (if (state/modal-opened?)
+                                                 (state/close-modal!)
+                                                 (state/pub-event! [:modal/show-cards])))}
+
    :ui/toggle-document-mode        {:desc    "Toggle document mode"
                                     :binding "t d"
                                     :fn      state/toggle-document-mode!}
@@ -367,8 +383,8 @@
                                      :binding "t t"
                                      :fn      state/toggle-theme!}
 
-   :ui/toggle-contents              {:desc    "Toggle Favorites in sidebar"
-                                     :binding "t f"
+   :ui/toggle-contents              {:desc    "Toggle Contents in sidebar"
+                                     :binding "mod+shift+c"
                                      :fn      ui-handler/toggle-contents!}
 
    :command/toggle-favorite         {:desc    "Add to/remove from favorites"
@@ -500,7 +516,6 @@
                           :ui/toggle-brackets
                           :go/search-in-page
                           :go/search
-                          :go/journals
                           :go/backward
                           :go/forward
                           :search/re-index
@@ -517,6 +532,10 @@
     (->
      (build-category-map [:command/run
                           :go/home
+                          :go/journals
+                          :go/all-pages
+                          :go/flashcards
+                          :go/graph-view
                           :go/keyboard-shortcuts
                           :go/tomorrow
                           :go/next-journal
@@ -633,6 +652,9 @@
    ^{:doc "Others"}
    [:go/home
     :go/journals
+    :go/all-pages
+    :go/graph-view
+    :go/flashcards
     :go/tomorrow
     :go/next-journal
     :go/prev-journal

+ 9 - 4
src/main/frontend/page.cljs

@@ -10,6 +10,14 @@
   [view route-match]
   (view route-match))
 
+(defn- teardown-fn
+  []
+  (try
+    (ui/setup-active-keystroke!)
+    (ui/setup-patch-ios-visual-viewport-state!)
+    (catch js/Error _e
+      nil)))
+
 (rum/defc current-page < rum/reactive
   {:did-mount    (fn [state]
                    (state/set-root-component! (:rum/react-component state))
@@ -17,10 +25,7 @@
                    (ui/inject-document-devices-envs!)
                    (ui/inject-dynamic-style-node!)
                    (plugin-handler/host-mounted!)
-                   (let [teardown-fn (comp
-                                      (ui/setup-active-keystroke!)
-                                      (ui/setup-patch-ios-visual-viewport-state!))]
-                     (assoc state ::teardown teardown-fn)))
+                   (assoc state ::teardown teardown-fn))
    :will-unmount (fn [state]
                    (let [teardown (::teardown state)]
                      (when-not (nil? teardown)

+ 28 - 2
src/main/frontend/state.cljs

@@ -16,7 +16,8 @@
             [promesa.core :as p]
             [rum.core :as rum]
             [frontend.mobile.util :as mobile]
-            [frontend.mobile.util :as mobile-util]))
+            [frontend.mobile.util :as mobile-util]
+            [cljs.cache :as cache]))
 
 (defonce state
   (let [document-mode? (or (storage/get :document/mode?) false)
@@ -188,6 +189,31 @@
 
       :srs/cards-due-count nil})))
 
+;; block uuid -> {content(String) -> ast}
+(def blocks-ast-cache (atom (cache/lru-cache-factory {} :threshold 5000)))
+(defn add-block-ast-cache!
+  [block-uuid content ast]
+  (when (and block-uuid content ast)
+    (let [k block-uuid
+          add-cache! (fn []
+                       (reset! blocks-ast-cache (cache/evict @blocks-ast-cache block-uuid))
+                       (reset! blocks-ast-cache (cache/miss @blocks-ast-cache k {content ast})))]
+      (if (cache/has? @blocks-ast-cache k)
+        (let [m (cache/lookup @blocks-ast-cache k)]
+          (if (and (map? m) (get m content))
+            (reset! blocks-ast-cache (cache/hit @blocks-ast-cache k))
+            (add-cache!)))
+        (add-cache!)))))
+
+(defn get-block-ast
+  [block-uuid content]
+  (when (and block-uuid content)
+    (let [k block-uuid]
+      (when (cache/has? @blocks-ast-cache k)
+        (let [m (cache/lookup @blocks-ast-cache k)]
+          (when-let [result (and (map? m) (get m content))]
+            (reset! blocks-ast-cache (cache/hit @blocks-ast-cache k))
+            result))))))
 
 (defn sub
   [ks]
@@ -1587,4 +1613,4 @@
 
 (defn get-visual-viewport-state
   []
-  (:ui/visual-viewport-state @state))
+  (:ui/visual-viewport-state @state))

+ 1 - 14
src/main/frontend/text.cljs

@@ -1,9 +1,7 @@
 (ns frontend.text
   (:require [frontend.config :as config]
             [frontend.util :as util]
-            [clojure.string :as string]
-            [clojure.set :as set]
-            [medley.core :as medley]))
+            [clojure.string :as string]))
 
 (def page-ref-re-0 #"\[\[(.*)\]\]")
 (def org-page-ref-re #"\[\[(file:.*)\]\[.+?\]\]")
@@ -346,14 +344,3 @@
       (if (not= (first parts) "0")
         (string/join "/" parts)
         (last parts)))))
-
-(defn remove-indentations
-  [text]
-  (when (string? text)
-    (let [lines (string/split-lines text)
-          spaces (re-find #"^[\s\t]+" (first lines))
-          spaces-count (count spaces)]
-      (string/join "\n" (map (fn [line]
-                               (let [spaces (re-find #"^[\s\t]+" line)
-                                     spaces-count (min (count spaces) spaces-count)]
-                                 (util/safe-subs line spaces-count))) lines)))))

+ 14 - 4
src/main/frontend/ui.cljs

@@ -270,7 +270,7 @@
 
 (defn main-node
   []
-  (gdom/getElement "main-content"))
+  (gdom/getElement "main-container"))
 
 (defn get-scroll-top []
   (.-scrollTop (main-node)))
@@ -369,16 +369,26 @@
       (.removeEventListener js/window "blur" clear-all)
       (.removeEventListener js/window "visibilitychange" clear-all))))
 
+(defonce last-scroll-top (atom 0))
+
+(defn scroll-down?
+  []
+  (let [scroll-top (get-scroll-top)]
+    (let [down? (> scroll-top @last-scroll-top)]
+      (reset! last-scroll-top scroll-top)
+      down?)))
+
 (defn on-scroll
   [node on-load on-top-reached]
   (let [full-height (gobj/get node "scrollHeight")
         scroll-top (gobj/get node "scrollTop")
         client-height (gobj/get node "clientHeight")
         bottom-reached? (<= (- full-height scroll-top client-height) 100)
-        top-reached? (= scroll-top 0)]
-    (when (and bottom-reached? on-load)
+        top-reached? (= scroll-top 0)
+        down? (scroll-down?)]
+    (when (and down? bottom-reached? on-load)
       (on-load))
-    (when (and top-reached? on-top-reached)
+    (when (and (not down?) top-reached? on-top-reached)
       (on-top-reached))))
 
 (defn attach-listeners

+ 7 - 7
src/main/frontend/ui.css

@@ -85,13 +85,7 @@
 }
 
 .ui__modal {
-  @apply fixed bottom-0 inset-x-0 px-4 pb-4;
-
-  @screen sm {
-    & {
-      @apply inset-0 flex items-baseline justify-center top-24;
-    }
-  }
+  @apply fixed px-4 pb-4 inset-0 flex items-baseline justify-center top-24;
 
   &-overlay {
     @apply fixed inset-0 transition-opacity;
@@ -123,6 +117,12 @@
   }
 }
 
+html.is-mobile {
+  .ui__modal {
+    @apply bottom-0 inset-x-0;
+  }
+}
+
 .ui__confirm-modal {
   .sublabel {
     display: flex;

+ 19 - 6
src/main/frontend/util.cljc

@@ -652,6 +652,19 @@
            (recur (conj res [(.-index m) (first m)]))
            res)))))
 
+#?(:cljs
+   (defn safe-set-range-text!
+     ([input text start end]
+      (try
+        (.setRangeText input "" start end)
+        (catch js/Error _e
+          nil)))
+     ([input text start end select-mode]
+      (try
+        (.setRangeText input "" start end select-mode)
+        (catch js/Error _e
+          nil)))))
+
 #?(:cljs
    (defn kill-line-before!
      [input]
@@ -659,7 +672,7 @@
            end (.-selectionStart input)
            n-pos (string/last-index-of val \newline (dec end))
            start (if n-pos (inc n-pos) 0)]
-       (.setRangeText input "" start end))))
+       (safe-set-range-text! input "" start end))))
 
 #?(:cljs
    (defn kill-line-after!
@@ -668,14 +681,14 @@
            start (.-selectionStart input)
            end   (or (string/index-of val \newline start)
                      (count val))]
-       (.setRangeText input "" start end))))
+       (safe-set-range-text! input "" start end))))
 
 #?(:cljs
    (defn insert-at-current-position!
      [input text]
      (let [start (.-selectionStart input)
            end   (.-selectionEnd input)]
-       (.setRangeText input text start end "end"))))
+       (safe-set-range-text! input text start end "end"))))
 
 ;; copied from re_com
 #?(:cljs
@@ -1323,7 +1336,7 @@
                           (recur (dec idx))
                           idx))
                       inc))]
-       (.setRangeText input "" idx current))))
+       (safe-set-range-text! input "" idx current))))
 
 #?(:cljs
    (defn forward-kill-word
@@ -1339,7 +1352,7 @@
                         (remove nil?)
                         (apply min))
                    (count val))]
-       (.setRangeText input "" current (inc idx)))))
+       (safe-set-range-text! input "" current (inc idx)))))
 
 #?(:cljs
    (defn fix-open-external-with-shift!
@@ -1476,4 +1489,4 @@
 #?(:cljs
    (defn sm-breakpoint?
      []
-     (< (.-offsetWidth js/document.documentElement) 640)))
+     (< (.-offsetWidth js/document.documentElement) 640)))

+ 2 - 2
src/main/frontend/util/drawer.cljs

@@ -61,8 +61,8 @@
 
                             :else
                             (let [properties-count (count (second (first (second ast))))
-                                  properties (subvec body-without-timestamps 0 (inc properties-count))
-                                  after (rest body-without-timestamps)]
+                                  properties (subvec body-without-timestamps 0 properties-count)
+                                  after (subvec body-without-timestamps properties-count)]
                               (string/join "\n" (concat [title] scheduled deadline properties [drawer] after))))
                           (string/join "\n" (concat [title] scheduled deadline [drawer] body-without-timestamps))))
 

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

@@ -1,3 +1,3 @@
 (ns frontend.version)
 
-(defonce version "0.5.2")
+(defonce version "0.5.4")