Merge branch 'main' into feat/move-block
34
.github/workflows/build.yml
vendored
@ -5,7 +5,7 @@ on: push
|
|||||||
jobs:
|
jobs:
|
||||||
publish:
|
publish:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.head_commit.message, '#build') }}
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build/release Electron app
|
- name: Build/release Electron app
|
||||||
#continue-on-error: true
|
#continue-on-error: true
|
||||||
uses: samuelmeuli/action-electron-builder@v1
|
uses: johannesjo/action-electron-builder@v1
|
||||||
with:
|
with:
|
||||||
# Specify electron-builder config file
|
# Specify electron-builder config file
|
||||||
args: -c electron-builder.json5
|
args: -c electron-builder.json5
|
||||||
@ -50,14 +50,38 @@ jobs:
|
|||||||
|
|
||||||
# If the commit is tagged with a version (e.g. "v1.0.0"),
|
# If the commit is tagged with a version (e.g. "v1.0.0"),
|
||||||
# release the app after building
|
# release the app after building
|
||||||
release: true
|
#release: true
|
||||||
#release: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
release: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||||
env:
|
env:
|
||||||
# macOS notarization API key
|
# macOS notarization API key
|
||||||
APPLE_API_KEY: ~/private_keys/AuthKey.p8
|
APPLE_API_KEY: ~/private_keys/AuthKey.p8
|
||||||
APPLE_API_KEY_ID: ${{ secrets.apple_api_key_id }}
|
APPLE_API_KEY_ID: ${{ secrets.apple_api_key_id }}
|
||||||
APPLE_API_KEY_ISSUER: ${{ secrets.apple_api_key_issuer_id }}
|
APPLE_API_ISSUER: ${{ secrets.apple_api_key_issuer_id }}
|
||||||
|
|
||||||
#- name: Print notarization-error.log
|
#- name: Print notarization-error.log
|
||||||
|
# if: ${{ matrix.os == 'macos-latest' }}
|
||||||
# run: cat notarization-error.log
|
# run: cat notarization-error.log
|
||||||
|
|
||||||
|
- name: Upload Linux artifact
|
||||||
|
if: ${{ matrix.os == 'ubuntu-latest' }}
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: heynote-linux-${{ github.sha }}
|
||||||
|
path: release/*/Heynote_*.AppImage
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
- name: Upload Mac artifact
|
||||||
|
if: ${{ matrix.os == 'macos-latest' }}
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: heynote-mac-arm64-${{ github.sha }}
|
||||||
|
path: release/*/Heynote_*_arm64.dmg
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
- name: Upload Windows artifact
|
||||||
|
if: ${{ matrix.os == 'windows-latest' }}
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: heynote-windows-${{ github.sha }}
|
||||||
|
path: release/*/Heynote_*.exe
|
||||||
|
retention-days: 30
|
||||||
|
27
.github/workflows/trigger-website-build.yml
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
name: Trigger Website Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
trigger:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Trigger Website Build
|
||||||
|
run: |
|
||||||
|
# Set the required variables
|
||||||
|
repo_owner="heyman"
|
||||||
|
repo_name="heynote-website"
|
||||||
|
event_type="trigger-workflow"
|
||||||
|
|
||||||
|
curl -L \
|
||||||
|
-X POST \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
|
-H "Authorization: Bearer ${{ secrets.HEYNOTE_WEBSITE_GITHUB_ACCESS_TOKEN }}" \
|
||||||
|
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||||
|
https://api.github.com/repos/$repo_owner/$repo_name/dispatches \
|
||||||
|
-d "{\"event_type\": \"$event_type\"}"
|
94
README.md
@ -3,6 +3,13 @@
|
|||||||
[](https://github.com/heyman/heynote/releases)
|
[](https://github.com/heyman/heynote/releases)
|
||||||
[](https://github.com/heyman/heynote/actions?query=workflow%3ATests)
|
[](https://github.com/heyman/heynote/actions?query=workflow%3ATests)
|
||||||
|
|
||||||
|
<img src="https://heynote.com/img/logo.png" style="width:79px;">
|
||||||
|
|
||||||
|
## General Information
|
||||||
|
|
||||||
|
- [Website](https://heynote.com)
|
||||||
|
- [Documentation](https://heynote.com/docs/)
|
||||||
|
- [Changelog](https://heynote.com/docs/changelog/)
|
||||||
|
|
||||||
Heynote is a dedicated scratchpad for developers. It functions as a large persistent text buffer where you can write down anything you like. Works great for that Slack message you don't want to accidentally send, a JSON response from an API you're working with, notes from a meeting, your daily to-do list, etc.
|
Heynote is a dedicated scratchpad for developers. It functions as a large persistent text buffer where you can write down anything you like. Works great for that Slack message you don't want to accidentally send, a JSON response from an API you're working with, notes from a meeting, your daily to-do list, etc.
|
||||||
|
|
||||||
@ -16,7 +23,7 @@ Available for Mac, Windows, and Linux.
|
|||||||
- Block-based
|
- Block-based
|
||||||
- Syntax highlighting:
|
- Syntax highlighting:
|
||||||
|
|
||||||
C++, C#, Clojure, CSS, Erlang, Go, Groovy, HTML, Java, JavaScript, JSX, Kotlin, TypeScript, TOML, TSX, JSON, Lezer, Markdown, PHP, Python, Ruby, Rust, Shell, SQL, Swift, XML, YAML
|
C++, C#, Clojure, CSS, Erlang, Dart, Go, Groovy, HTML, Java, JavaScript, JSX, Kotlin, TypeScript, TOML, TSX, JSON, Lezer, Markdown, PHP, Python, Ruby, Rust, Scala, Shell, SQL, Swift, Vue, XML, YAML
|
||||||
|
|
||||||
- Language auto-detection
|
- Language auto-detection
|
||||||
- Auto-formatting
|
- Auto-formatting
|
||||||
@ -28,19 +35,9 @@ Available for Mac, Windows, and Linux.
|
|||||||
- Default or Emacs-like key bindings
|
- Default or Emacs-like key bindings
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Documentation
|
||||||
|
|
||||||
Download the appropriate (Mac, Windows or Linux) version from the latest Github release (or from [heynote.com](https://heynote.com)). The Windows build is not signed, so you might see some scary warning (I can not justify paying a yearly fee for a certificate just to get rid of that).
|
[Documentation](https://heynote.com/docs/) is available on the Heynote website.
|
||||||
|
|
||||||
### Notes on Linux installation
|
|
||||||
|
|
||||||
It's been reported [(#48)](https://github.com/heyman/heynote/issues/48) that ChromeOS's Debian VM need the following packages installed to run the Heynote AppImage:
|
|
||||||
|
|
||||||
```
|
|
||||||
libfuse2
|
|
||||||
libnss3
|
|
||||||
libnspr4
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
@ -70,87 +67,20 @@ To run the tests in the Playwright UI:
|
|||||||
|
|
||||||
I'm happy to merge contributions that fit my vision for the app. Bug fixes are always welcome.
|
I'm happy to merge contributions that fit my vision for the app. Bug fixes are always welcome.
|
||||||
|
|
||||||
## Math Blocks
|
|
||||||
|
|
||||||
Heynote's Math blocks are powered by [Math.js expressions](https://mathjs.org/docs/expressions). Checkout their [documentation](https://mathjs.org/docs/) to see what [syntax](https://mathjs.org/docs/expressions/syntax.html), [functions](https://mathjs.org/docs/reference/functions.html), and [constants](https://mathjs.org/docs/reference/constants.html) are available.
|
|
||||||
|
|
||||||
### Accessing the previous result
|
|
||||||
|
|
||||||
The variable `prev` can be used to access the previous result. For example:
|
|
||||||
|
|
||||||
```
|
|
||||||
128
|
|
||||||
prev * 2 # 256
|
|
||||||
```
|
|
||||||
|
|
||||||
### Changing how the results of Math blocks are formatted?
|
|
||||||
|
|
||||||
You can define a custom `format` function within the Math block like this:
|
|
||||||
|
|
||||||
```
|
|
||||||
_format = format # store reference to the built in format
|
|
||||||
format(x) = _format(x, {notation:"exponential"})
|
|
||||||
```
|
|
||||||
|
|
||||||
See the [Math.js format()](https://mathjs.org/docs/reference/functions/format.html) function for more info on what's supported.
|
|
||||||
|
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
### Where is the buffer data stored?
|
### Where is the buffer data stored?
|
||||||
|
|
||||||
The default paths for the buffer data for the respective OS are:
|
See the [documentation](https://heynote.com/docs/#user-content-the-notes-library).
|
||||||
|
|
||||||
- Mac: `~/Library/Application Support/Heynote/buffer.txt`
|
|
||||||
- Windows: `%APPDATA%\Heynote\buffer.txt`
|
|
||||||
- Linux: `~/.config/Heynote/buffer.txt`
|
|
||||||
|
|
||||||
From version >=1.5.0, symlinks will be supported and you'll be able to configure the path where `buffer.txt` is stored.
|
|
||||||
|
|
||||||
### Can you make a mobile app?
|
### Can you make a mobile app?
|
||||||
|
|
||||||
No, at the moment this is out of scope, sorry.
|
No, at the moment this is out of scope, sorry.
|
||||||
|
|
||||||
### Can you add a feature for naming blocks and/or adding tags? [(#44)](https://github.com/heyman/heynote/issues/44)
|
|
||||||
|
|
||||||
Currently, I'm not planning on adding this. The main reason is that it goes against the scratchpadness of the program.
|
|
||||||
|
|
||||||
I can totally see the usefulness of such a feature, and it's definitely something that I would expect from a more traditional Notes app. However a large part of Heynote's appeal is it's simplicity, and if that is to remain so, I'm going to have to say no to a lot of actually useful features.
|
|
||||||
|
|
||||||
### What are the default keyboard shortcuts?
|
### What are the default keyboard shortcuts?
|
||||||
|
|
||||||
**On Mac**
|
See the [documentation](https://heynote.com/docs/#user-content-default-key-bindings).
|
||||||
|
|
||||||
```
|
|
||||||
⌘ + Enter Add new block below the current block
|
|
||||||
⌥ + Enter Add new block before the current block
|
|
||||||
⌘ + Shift + Enter Add new block at the end of the buffer
|
|
||||||
⌥ + Shift + Enter Add new block at the start of the buffer
|
|
||||||
⌘ + ⌥ + Enter Split the current block at cursor position
|
|
||||||
⌘ + L Change block language
|
|
||||||
⌘ + Down Goto next block
|
|
||||||
⌘ + Up Goto previous block
|
|
||||||
⌘ + A Select all text in a note block. Press again to select the whole buffer
|
|
||||||
⌘ + ⌥ + Up/Down Add additional cursor above/below
|
|
||||||
⌥ + Shift + F Format block content (works for JSON, JavaScript, HTML, CSS and Markdown)
|
|
||||||
```
|
|
||||||
|
|
||||||
**On Windows and Linux**
|
|
||||||
|
|
||||||
```
|
|
||||||
Ctrl + Enter Add new block below the current block
|
|
||||||
Alt + Enter Add new block before the current block
|
|
||||||
Ctrl + Shift + Enter Add new block at the end of the buffer
|
|
||||||
Alt + Shift + Enter Add new block at the start of the buffer
|
|
||||||
Ctrl + Alt + Enter Split the current block at cursor position
|
|
||||||
Ctrl + L Change block language
|
|
||||||
Ctrl + Down Goto next block
|
|
||||||
Ctrl + Up Goto previous block
|
|
||||||
Ctrl + A Select all text in a note block. Press again to select the whole buffer
|
|
||||||
Ctrl + Alt + Up/Down Add additional cursor above/below
|
|
||||||
Alt + Shift + F Format block content (works for JSON, JavaScript, HTML, CSS and Markdown)
|
|
||||||
Alt Show menu
|
|
||||||
```
|
|
||||||
|
|
||||||
## Thanks!
|
## Thanks!
|
||||||
|
|
||||||
|
80
assets/font/open-sans/open-sans.css
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
/* BEGIN Light */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
src: url("./fonts/Light/OpenSans-Light.woff2?v=1.1.0") format("woff2"), url("./fonts/Light/OpenSans-Light.woff?v=1.1.0") format("woff");
|
||||||
|
font-weight: 300;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
/* END Light */
|
||||||
|
/* BEGIN Light Italic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
src: url("./fonts/LightItalic/OpenSans-LightItalic.woff2?v=1.1.0") format("woff2"), url("./fonts/LightItalic/OpenSans-LightItalic.woff?v=1.1.0") format("woff");
|
||||||
|
font-weight: 300;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
/* END Light Italic */
|
||||||
|
/* BEGIN Regular */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
src: url("./fonts/Regular/OpenSans-Regular.woff2?v=1.1.0") format("woff2"), url("./fonts/Regular/OpenSans-Regular.woff?v=1.1.0") format("woff");
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
/* END Regular */
|
||||||
|
/* BEGIN Italic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
src: url("./fonts/Italic/OpenSans-Italic.woff2?v=1.1.0") format("woff2"), url("./fonts/Italic/OpenSans-Italic.woff?v=1.1.0") format("woff");
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
/* END Italic */
|
||||||
|
/* BEGIN Semibold */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
src: url("./fonts/Semibold/OpenSans-Semibold.woff2?v=1.1.0") format("woff2"), url("./fonts/Semibold/OpenSans-Semibold.woff?v=1.1.0") format("woff");
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
/* END Semibold */
|
||||||
|
/* BEGIN Semibold Italic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
src: url("./fonts/SemiboldItalic/OpenSans-SemiboldItalic.woff2?v=1.1.0") format("woff2"), url("./fonts/SemiboldItalic/OpenSans-SemiboldItalic.woff?v=1.1.0") format("woff");
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
/* END Semibold Italic */
|
||||||
|
/* BEGIN Bold */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
src: url("./fonts/Bold/OpenSans-Bold.woff2?v=1.1.0") format("woff2"), url("./fonts/Bold/OpenSans-Bold.woff?v=1.1.0") format("woff");
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
/* END Bold */
|
||||||
|
/* BEGIN Bold Italic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
src: url("./fonts/BoldItalic/OpenSans-BoldItalic.woff2?v=1.1.0") format("woff2"), url("./fonts/BoldItalic/OpenSans-BoldItalic.woff?v=1.1.0") format("woff");
|
||||||
|
font-weight: bold;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
/* END Bold Italic */
|
||||||
|
/* BEGIN Extrabold */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
src: url("./fonts/ExtraBold/OpenSans-ExtraBold.woff2?v=1.1.0") format("woff2"), url("./fonts/ExtraBold/OpenSans-ExtraBold.woff?v=1.1.0") format("woff");
|
||||||
|
font-weight: 800;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
/* END Extrabold */
|
||||||
|
/* BEGIN Extrabold Italic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Open Sans';
|
||||||
|
src: url("./fonts/ExtraBoldItalic/OpenSans-ExtraBoldItalic.woff2?v=1.1.0") format("woff2"), url("./fonts/ExtraBoldItalic/OpenSans-ExtraBoldItalic.woff?v=1.1.0") format("woff");
|
||||||
|
font-weight: 800;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
/* END Extrabold Italic */
|
1
assets/icons/arrow-right-black.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" ?><svg baseProfile="tiny" height="24px" version="1.2" viewBox="0 0 24 24" width="24px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="Layer_1"><path fill="#222" d="M13.293,7.293c-0.391,0.391-0.391,1.023,0,1.414L15.586,11H8c-0.552,0-1,0.448-1,1s0.448,1,1,1h7.586l-2.293,2.293 c-0.391,0.391-0.391,1.023,0,1.414C13.488,16.902,13.744,17,14,17s0.512-0.098,0.707-0.293L19.414,12l-4.707-4.707 C14.316,6.902,13.684,6.902,13.293,7.293z"/></g></svg>
|
After Width: | Height: | Size: 522 B |
1
assets/icons/arrow-right-grey.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" ?><svg baseProfile="tiny" height="24px" version="1.2" viewBox="0 0 24 24" width="24px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="Layer_1"><path fill="#ddd" d="M13.293,7.293c-0.391,0.391-0.391,1.023,0,1.414L15.586,11H8c-0.552,0-1,0.448-1,1s0.448,1,1,1h7.586l-2.293,2.293 c-0.391,0.391-0.391,1.023,0,1.414C13.488,16.902,13.744,17,14,17s0.512-0.098,0.707-0.293L19.414,12l-4.707-4.707 C14.316,6.902,13.684,6.902,13.293,7.293z"/></g></svg>
|
After Width: | Height: | Size: 522 B |
1
assets/icons/arrow-right-white.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" ?><svg baseProfile="tiny" height="24px" version="1.2" viewBox="0 0 24 24" width="24px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="Layer_1"><path fill="#fff" d="M13.293,7.293c-0.391,0.391-0.391,1.023,0,1.414L15.586,11H8c-0.552,0-1,0.448-1,1s0.448,1,1,1h7.586l-2.293,2.293 c-0.391,0.391-0.391,1.023,0,1.414C13.488,16.902,13.744,17,14,17s0.512-0.098,0.707-0.293L19.414,12l-4.707-4.707 C14.316,6.902,13.684,6.902,13.293,7.293z"/></g></svg>
|
After Width: | Height: | Size: 522 B |
1
assets/icons/caret-down-white.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" ?><svg viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"><rect fill="none" height="256" width="256"/><polyline fill="none" points="208 96 128 176 48 96" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
After Width: | Height: | Size: 266 B |
1
assets/icons/caret-down.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" ?><svg viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"><rect fill="none" height="256" width="256"/><polyline fill="none" points="208 96 128 176 48 96" stroke="#333" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
After Width: | Height: | Size: 266 B |
1
assets/icons/caret-right-white.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" ?><svg viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"><rect fill="none" height="256" width="256"/><polyline fill="none" points="96 48 176 128 96 208" stroke="#fff" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
After Width: | Height: | Size: 266 B |
1
assets/icons/caret-right.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" ?><svg viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg"><rect fill="none" height="256" width="256"/><polyline fill="none" points="96 48 176 128 96 208" stroke="#333" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
|
After Width: | Height: | Size: 266 B |
1
assets/icons/drag-vertical-dark.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg"><circle cx="4.5" cy="2.5" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="4.5" cy="4.5" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="4.5" cy="6.499" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="4.5" cy="8.499" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="4.5" cy="10.498" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="4.5" cy="12.498" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="2.5" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="4.5" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="6.499" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="8.499" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="10.498" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="12.498" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="2.5" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="4.5" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="6.499" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="8.499" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="10.498" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="12.498" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="2.5" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="4.5" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="6.499" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="8.499" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="10.498" fill="#e6e6e6" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="12.498" fill="#e6e6e6" r=".6" class="fill-000000"></circle></svg>
|
After Width: | Height: | Size: 1.9 KiB |
1
assets/icons/drag-vertical-light.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg fill="none" viewBox="0 0 15 15" xmlns="http://www.w3.org/2000/svg"><circle cx="4.5" cy="2.5" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="4.5" cy="4.5" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="4.5" cy="6.499" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="4.5" cy="8.499" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="4.5" cy="10.498" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="4.5" cy="12.498" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="2.5" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="4.5" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="6.499" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="8.499" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="10.498" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="6.5" cy="12.498" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="2.5" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="4.5" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="6.499" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="8.499" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="10.498" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="8.499" cy="12.498" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="2.5" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="4.5" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="6.499" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="8.499" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="10.498" fill="#b0b0b0" r=".6" class="fill-000000"></circle><circle cx="10.499" cy="12.498" fill="#b0b0b0" r=".6" class="fill-000000"></circle></svg>
|
After Width: | Height: | Size: 1.9 KiB |
138
docs/changelog.md
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
Here are the most notable changes in each release. For a more detailed list of changes, see the [Github Releases page](https://github.com/heyman/heynote/releases).
|
||||||
|
|
||||||
|
## 2.2.0-beta.2
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
|
||||||
|
- Added support for custom key bindings. See [the documentation](https://heynote.com/docs/#user-content-custom-key-bindings) for more info.
|
||||||
|
- Added a "command palette" that can be accessed by pressing `Ctrl/Cmd+Shift+P`, or just typing `>` in the buffer selector. The command palette allows you to discover all available commands in the app, and to quickly execute them.
|
||||||
|
- Added support for configuring the tab size.
|
||||||
|
|
||||||
|
### Other changes
|
||||||
|
|
||||||
|
- Upgraded to latest version of Electron, Vue, electron-builder and other dependencies.
|
||||||
|
|
||||||
|
## 2.1.4
|
||||||
|
|
||||||
|
- Fix issue with positioning and size of todo list checkboxes in Markdown blocks when using a non-default font size, or a non-monospaced font.
|
||||||
|
- Fix issue when pressing `Ctrl/Cmd+A` in a text input inside a modal dialog (e.g. the buffer selector). Previously the select all command would be sent to the editor.
|
||||||
|
|
||||||
|
## 2.1.3
|
||||||
|
|
||||||
|
- Fix escaping issue in buffer selector (properly this time, hopefully)
|
||||||
|
|
||||||
|
## 2.1.2 (yanked)
|
||||||
|
|
||||||
|
- Fix issue where buffer name wasn't properly escaped in buffer selector
|
||||||
|
|
||||||
|
## 2.1.1
|
||||||
|
|
||||||
|
- Fix bug on Windows causing sub directories in the note library to not work correctly
|
||||||
|
|
||||||
|
## 2.1.0
|
||||||
|
|
||||||
|
### New Features
|
||||||
|
|
||||||
|
- Added support for moving the current block to another (or new) buffer. Pressing `Ctrl/Cmd+S` will now pop up a dialog where you can search for and select another buffer to which the block will be moved. It's also possible to select to create a new buffer to which the block will be moved.
|
||||||
|
- Add right click context menu with undo/redo/cut/copy/paste/select all as well as Delete Block and Move block to another buffer.
|
||||||
|
- Add File menu item for switching buffer
|
||||||
|
- When deleting a block, the cursor will now end up at the beginning of the next block, instead of at the end of the previous block.
|
||||||
|
- Added support for the following languages:
|
||||||
|
* Elixir
|
||||||
|
* Scala
|
||||||
|
- PHP blocks no longer requires `<?php` for syntax highlighting to work
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- MacOS: Clicking the Heynote icon in the dock when using menu bar mode didn't bring up the window
|
||||||
|
- Redo from the application menu didn't work
|
||||||
|
|
||||||
|
|
||||||
|
## 2.0.0
|
||||||
|
|
||||||
|
### IMPORTANT (breaking change)
|
||||||
|
|
||||||
|
The default path of the scratch file has changed. The first time you start the new version of Heynote, your existing buffer file will be migrated to the new note library. If you're using the default buffer location, that means that the existing Scratch buffer file will be moved from `%APP_DIR%/buffer.txt` to `%APP_DIR%/notes/scratch.txt`. If you are using a custom buffer location the existing scratch file will be moved from `%CUSTOM_DIR%/buffer.txt` to `%CUSTOM_DIR%/scratch.txt`. Before the migration, the existing buffer file will be backed up to `%APP_DIR%/buffer.txt.bak` or `%CUSTOM_DIR%/buffer.txt.bak`.
|
||||||
|
|
||||||
|
If you are running a previous version of Heynote with the buffer file synchronized across multiple machines using a file synching service such as Dropbox or OneDrive, you should make sure to upgrade all machines to Heynote 2.0 at the same time (closing Heynote before) in order for the file to stay in sync, since the file path for the buffer file has changed.
|
||||||
|
|
||||||
|
### Support for multiple note buffers.
|
||||||
|
|
||||||
|
Apart from the default Scratch buffer, you can now create and switch between multiple note buffers. `Ctrl/Cmd+N` opens up a dialog for creating a new buffer. By pressing `Ctrl/Cmd+S` you can create a new note from the current block (the current block will be moved into the new note). New note buffers are saved to the note library which is basically a directory (with sub dirs) on the disk with a `.txt` file for each buffer. You switch between buffers by pressing `Ctrl/Cmd+P`.
|
||||||
|
|
||||||
|
### Other changes
|
||||||
|
|
||||||
|
- The file format for the buffer files has been updated to include some JSON metadata at the top of the file.
|
||||||
|
- The cursor(s) location is saved between sessions.
|
||||||
|
- Improvements when using a file syncing service (e.g. Dropbox, OneDrive) to sync the note library between machines.
|
||||||
|
- The setting for changing the color theme is now located in the program settings, instead of in the status bar.
|
||||||
|
- Improvements to the language selector's search feature (it's now possible to search for languages by their file extension).
|
||||||
|
|
||||||
|
## 1.8.0
|
||||||
|
|
||||||
|
- Performance optimizations
|
||||||
|
- Add default redo cmd that works on all Platforms. Mod+Shift+Z
|
||||||
|
- Fix bug causing editing to break for empty blocks in some cases
|
||||||
|
- Add setting for configuring the default block language
|
||||||
|
- Vue language support
|
||||||
|
- Dart Syntax
|
||||||
|
- Fix error on startup for large buffers
|
||||||
|
|
||||||
|
## 1.7.1
|
||||||
|
|
||||||
|
- Update to latest version of Electron. Fixes crash on MacOS 15 Developer Preview
|
||||||
|
|
||||||
|
## 1.7.0
|
||||||
|
|
||||||
|
- Fix "white flash" effect when resizing window in dark mode
|
||||||
|
- Add prev variable to Math blocks that holds the previous value
|
||||||
|
- Add settings button to status bar
|
||||||
|
- Add version number to settings dialog
|
||||||
|
- Persist window location when opening the app
|
||||||
|
- Copy whole current line(s) when selection(s) are empty
|
||||||
|
- Fix block corruption when deleting block content using deleteLine command
|
||||||
|
- Add PowerShell and Diff language modes
|
||||||
|
- "Always on top" setting which makes Heynote stay on top of other programs
|
||||||
|
|
||||||
|
## 1.6.0
|
||||||
|
|
||||||
|
- Added support for having Heynote in the Mac Menu Bar / Tray icon
|
||||||
|
- Ability to specify file system location of Heynote's buffer file. The buffer will automatically be reloaded if changed on disk, so this should make it possible to have the buffer automatically synced between machines using a file-syncing service such as Dropbox.
|
||||||
|
- Custom font and font size support.
|
||||||
|
- More key-binding for creating new blocks
|
||||||
|
- Syntax hightlighting support for new languages:
|
||||||
|
* Swift
|
||||||
|
* Kotlin
|
||||||
|
* Groovy
|
||||||
|
- Auto-close brackets functionality that can be turned on in settings
|
||||||
|
- Ability to change how calculations are formatted in Math blocks. See the [Docs](https://heynote.com/docs/#user-content-changing-how-the-results-of-math-blocks-are-formatted) for info on how to do this.
|
||||||
|
- There's now a Heynote webapp at [app.heynote.com](https://app.heynote.com). It's still work-in-progress, but should be usable. The buffer is stored in localStorage.
|
||||||
|
- Multiple bug fixes and minor improvement.
|
||||||
|
|
||||||
|
|
||||||
|
## 1.5.0
|
||||||
|
|
||||||
|
- Add support for the following languages
|
||||||
|
* TypeScript
|
||||||
|
* JSX
|
||||||
|
* TSX
|
||||||
|
* TOML
|
||||||
|
* C#
|
||||||
|
* Clojure
|
||||||
|
* Erlang
|
||||||
|
* Golang
|
||||||
|
* Lezer
|
||||||
|
* Ruby
|
||||||
|
* Shell
|
||||||
|
* YAML
|
||||||
|
- Various bug fixes and improvements
|
||||||
|
|
||||||
|
## 1.4.1
|
||||||
|
|
||||||
|
- Fixed issue that would sometimes cause auto formatting to freeze the app for long periods.
|
||||||
|
|
||||||
|
## 1.4.0
|
||||||
|
|
||||||
|
- Added ability to set a global hotkey for showing/hiding Heynote.
|
174
docs/index.md
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
# Heynote Documentation
|
||||||
|
|
||||||
|
[Changelog](/docs/changelog/)
|
||||||
|
|
||||||
|
Heynote is a dedicated scratchpad for developers. It functions as a large persistent text buffer where you can write down anything you like. Works great for that Slack message you don't want to accidentally send, a JSON response from an API you're working with, notes from a meeting, your daily to-do list, etc.
|
||||||
|
|
||||||
|
The Heynote buffer is divided into blocks, and each block can have its own Language set (e.g. JavaScript, JSON, Markdown, etc.). This gives you syntax highlighting and lets you auto-format that JSON response.
|
||||||
|
|
||||||
|
Available for Mac, Windows, and Linux.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Persistent text buffer
|
||||||
|
- Block-based
|
||||||
|
- Syntax highlighting:
|
||||||
|
|
||||||
|
C++, C#, Clojure, CSS, Elixir, Erlang, Dart, Go, Groovy, HTML, Java, JavaScript, JSX, Kotlin, TypeScript, TOML, TSX, JSON, Lezer, Markdown, PHP, Python, Ruby, Rust, Scala, Shell, SQL, Swift, Vue, XML, YAML
|
||||||
|
|
||||||
|
- Language auto-detection
|
||||||
|
- Auto-formatting
|
||||||
|
- Math/Calculator mode
|
||||||
|
- Currency conversion
|
||||||
|
- Multi-cursor editing
|
||||||
|
- Dark & Light themes
|
||||||
|
- Option to set a global hotkey to show/hide the app
|
||||||
|
- Default or Emacs-like key bindings
|
||||||
|
|
||||||
|
## Default Key Bindings
|
||||||
|
|
||||||
|
<!-- keyboard_shortcuts -->
|
||||||
|
|
||||||
|
**On Mac**
|
||||||
|
|
||||||
|
```
|
||||||
|
⌘ + Enter Add new block below the current block
|
||||||
|
⌥ + Enter Add new block before the current block
|
||||||
|
⌘ + Shift + Enter Add new block at the end of the buffer
|
||||||
|
⌥ + Shift + Enter Add new block at the start of the buffer
|
||||||
|
⌘ + ⌥ + Enter Split the current block at cursor position
|
||||||
|
⌘ + L Change block language
|
||||||
|
⌘ + N Create a new note buffer
|
||||||
|
⌘ + S Move the current block to another (or new) buffer
|
||||||
|
⌘ + P Open note selector
|
||||||
|
⌘ + Down Goto next block
|
||||||
|
⌘ + Up Goto previous block
|
||||||
|
⌘ + A Select all text in a note block. Press again to select the whole buffer
|
||||||
|
⌘ + ⌥ + Up/Down Add additional cursor above/below
|
||||||
|
⌥ + Shift + F Format block content (works for JSON, JavaScript, HTML, CSS and Markdown)
|
||||||
|
```
|
||||||
|
|
||||||
|
**On Windows and Linux**
|
||||||
|
|
||||||
|
```
|
||||||
|
Ctrl + Enter Add new block below the current block
|
||||||
|
Alt + Enter Add new block before the current block
|
||||||
|
Ctrl + Shift + Enter Add new block at the end of the buffer
|
||||||
|
Alt + Shift + Enter Add new block at the start of the buffer
|
||||||
|
Ctrl + Alt + Enter Split the current block at cursor position
|
||||||
|
Ctrl + L Change block language
|
||||||
|
Ctrl + N Create a new note buffer
|
||||||
|
Ctrl + S Move the current block to another (or new) buffer
|
||||||
|
Ctrl + P Open note selector
|
||||||
|
Ctrl + Down Goto next block
|
||||||
|
Ctrl + Up Goto previous block
|
||||||
|
Ctrl + A Select all text in a note block. Press again to select the whole buffer
|
||||||
|
Ctrl + Alt + Up/Down Add additional cursor above/below
|
||||||
|
Alt + Shift + F Format block content (works for JSON, JavaScript, HTML, CSS and Markdown)
|
||||||
|
Alt Show menu
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Key Bindings
|
||||||
|
|
||||||
|
Heynote supports custom key bindings which you can configure in the settings. The key bindings are evaluated from top to bottom, so a binding that comes before another one will take precedence. Most commands will stop the event from propagating, but some commands only applies in certain contexts and might not stop the event from propagating to a later key binding.
|
||||||
|
|
||||||
|
To disable one of the built in key bindings, you can add a new key binding with the same key combination for the command "Do nothing". This will stop the event from propagating to the built in key binding.
|
||||||
|
|
||||||
|
## Download/Installation
|
||||||
|
|
||||||
|
Download the appropriate (Mac, Windows or Linux) version from [heynote.com](https://heynote.com). The Windows build is not signed, so you might see some scary warning (I can not justify paying a yearly fee for a certificate just to get rid of that).
|
||||||
|
|
||||||
|
If installing Heynote on Linux in ChromeOS, see the [notes](#user-content-linux-on-chromeos) below about some packages that are needed.
|
||||||
|
|
||||||
|
On macOS, [Homebrew](https://brew.sh) users can utilize an unofficial [Homebrew Cask](https://formulae.brew.sh/cask/heynote#default): `brew install --cask heynote`
|
||||||
|
|
||||||
|
## Math Blocks
|
||||||
|
|
||||||
|
Heynote's Math blocks are powered by [Math.js expressions](https://mathjs.org/docs/expressions). Checkout their [documentation](https://mathjs.org/docs/) to see what [syntax](https://mathjs.org/docs/expressions/syntax.html), [functions](https://mathjs.org/docs/reference/functions.html), and [constants](https://mathjs.org/docs/reference/constants.html) are available.
|
||||||
|
|
||||||
|
### Accessing the previous result
|
||||||
|
|
||||||
|
The variable `prev` can be used to access the previous result. For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
128
|
||||||
|
prev * 2 # 256
|
||||||
|
```
|
||||||
|
|
||||||
|
### Changing how the results of Math blocks are formatted
|
||||||
|
|
||||||
|
You can define a custom `format` function within the Math block like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
_format = format # store reference to the built in format
|
||||||
|
format(x) = _format(x, {notation:"exponential"})
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also do something like this to show the number with your default locale or provide a [custom one](https://www.w3.org/International/articles/language-tags/):
|
||||||
|
|
||||||
|
```
|
||||||
|
format(x) = x.toLocaleString();
|
||||||
|
format(x) = x.toLocaleString('en-GB');
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [Math.js format()](https://mathjs.org/docs/reference/functions/format.html) function for more info on what's supported.
|
||||||
|
|
||||||
|
|
||||||
|
## The notes library
|
||||||
|
|
||||||
|
The notes library is a directory (with sub dirs) on the disk with a `.txt` file for each buffer. It's created the first time you start Heynote, with the default buffer file `scratch.txt` in it. The default location for the library is:
|
||||||
|
|
||||||
|
- Mac: `~/Library/Application Support/Heynote/notes/`
|
||||||
|
- Windows: `%APPDATA%\Heynote\notes\`
|
||||||
|
- Linux: `~/.config/Heynote/notes/`
|
||||||
|
|
||||||
|
You can change the path of the notes library in the settings. Heynote expects reasonably fast disk access to the notes library, so it's not recommended to use a network drive, though file syncing services like Dropbox, OneDrive, etc. should work (see below).
|
||||||
|
|
||||||
|
### Synchronizing the notes library
|
||||||
|
|
||||||
|
Heynote is built to support synchronizing the notes library (or buffer file in the case of Heynote 1.x) through file-syncing services like Dropbox, OneDrive, etc. However, note that the synchronization logic is quite simple, so editing the same buffer on two different machines at the same time might lead to conflicts and unexpected results.
|
||||||
|
|
||||||
|
When using a file synching service that support "offloading" of files in the cloud (removing them from the disk), it's recommended to mark the notes library as "always available offline".
|
||||||
|
|
||||||
|
As always, backup things that are important.
|
||||||
|
|
||||||
|
|
||||||
|
## Linux
|
||||||
|
|
||||||
|
### Linux on ChromeOS
|
||||||
|
|
||||||
|
It's been reported [(#48)](https://github.com/heyman/heynote/issues/48) that ChromeOS's Debian VM need the following packages installed to run the Heynote AppImage:
|
||||||
|
|
||||||
|
```
|
||||||
|
libfuse2
|
||||||
|
libnss3
|
||||||
|
libnspr4
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wayland
|
||||||
|
|
||||||
|
Due to [an issue in Electron](https://github.com/electron/electron/issues/38288), the global hotkey will not work in all applications running under Wayland. In KDE it is possible to work around this limitation by adding this Kwin script:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function toggleHeynote() {
|
||||||
|
var client = workspace.clientList().find((c) => c.resourceClass.toLowerCase() === 'heynote');
|
||||||
|
if (client) {
|
||||||
|
if (client.minimized) {
|
||||||
|
client.minimized = false;
|
||||||
|
workspace.activeClient = client;
|
||||||
|
} else {
|
||||||
|
if (workspace.activeClient == client) {
|
||||||
|
client.minimized = true;
|
||||||
|
} else {
|
||||||
|
workspace.activeClient = client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registerShortcut('toggleHeynote', 'Toggle Heynote', 'Ctrl+Shift+H', toggleHeynote);
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [KWin scripting tutorial](https://develop.kde.org/docs/plasma/kwin/) for instructions on how to install the script.
|
||||||
|
|
||||||
|
Remember to enable the script in the KDE System Settings. It may also be necessary to go into the KDE System Settings and bind the "Toggle Heynote" key manually.
|
||||||
|
|
@ -13,7 +13,6 @@
|
|||||||
"dist-electron",
|
"dist-electron",
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"afterSign": "electron-builder-notarize",
|
|
||||||
"mac": {
|
"mac": {
|
||||||
"artifactName": "${productName}_${version}_${arch}.${ext}",
|
"artifactName": "${productName}_${version}_${arch}.${ext}",
|
||||||
"target": [
|
"target": [
|
||||||
|
@ -24,6 +24,19 @@ const schema = {
|
|||||||
properties: {
|
properties: {
|
||||||
"keymap": { "enum": ["default", "emacs"], default:"default" },
|
"keymap": { "enum": ["default", "emacs"], default:"default" },
|
||||||
"emacsMetaKey": { "enum": [null, "alt", "meta"], default: null },
|
"emacsMetaKey": { "enum": [null, "alt", "meta"], default: null },
|
||||||
|
"keyBindings": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["key", "command"],
|
||||||
|
"properties": {
|
||||||
|
"key": { "type": "string" },
|
||||||
|
"command": { "type": "string" }
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
"showLineNumberGutter": {type: "boolean", default:true},
|
"showLineNumberGutter": {type: "boolean", default:true},
|
||||||
"showFoldGutter": {type: "boolean", default:true},
|
"showFoldGutter": {type: "boolean", default:true},
|
||||||
"autoUpdate": {type: "boolean", default: true},
|
"autoUpdate": {type: "boolean", default: true},
|
||||||
@ -35,6 +48,9 @@ const schema = {
|
|||||||
"showInMenu": {type: "boolean", default: false},
|
"showInMenu": {type: "boolean", default: false},
|
||||||
"alwaysOnTop": {type: "boolean", default: false},
|
"alwaysOnTop": {type: "boolean", default: false},
|
||||||
"bracketClosing": {type: "boolean", default: false},
|
"bracketClosing": {type: "boolean", default: false},
|
||||||
|
"tabSize": {type: "integer", default: 4},
|
||||||
|
"defaultBlockLanguage": {type: "string"},
|
||||||
|
"defaultBlockLanguageAutoDetect": {type: "boolean"},
|
||||||
|
|
||||||
// when default font settings are used, fontFamily and fontSize is not specified in the
|
// when default font settings are used, fontFamily and fontSize is not specified in the
|
||||||
// settings file, so that it's possible for us to change the default settings in the
|
// settings file, so that it's possible for us to change the default settings in the
|
||||||
@ -59,6 +75,7 @@ const defaults = {
|
|||||||
settings: {
|
settings: {
|
||||||
keymap: "default",
|
keymap: "default",
|
||||||
emacsMetaKey: isMac ? "meta" : "alt",
|
emacsMetaKey: isMac ? "meta" : "alt",
|
||||||
|
keyBindings: [],
|
||||||
showLineNumberGutter: true,
|
showLineNumberGutter: true,
|
||||||
showFoldGutter: true,
|
showFoldGutter: true,
|
||||||
autoUpdate: true,
|
autoUpdate: true,
|
||||||
@ -70,6 +87,7 @@ const defaults = {
|
|||||||
showInMenu: false,
|
showInMenu: false,
|
||||||
alwaysOnTop: false,
|
alwaysOnTop: false,
|
||||||
bracketClosing: false,
|
bracketClosing: false,
|
||||||
|
tabSize: 4,
|
||||||
},
|
},
|
||||||
theme: "system",
|
theme: "system",
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
import os from "os";
|
import os from "os";
|
||||||
import { keyHelpStr } from "../shared-utils/key-helper";
|
import { keyHelpStr } from "../shared-utils/key-helper";
|
||||||
|
|
||||||
export const eraseInitialContent = !!process.env.ERASE_INITIAL_CONTENT
|
|
||||||
|
|
||||||
export const initialContent = `
|
export const initialContent = `
|
||||||
∞∞∞markdown
|
{"formatVersion":"1.0.0","name":"Scratch"}
|
||||||
|
∞∞∞text
|
||||||
Welcome to Heynote! 👋
|
Welcome to Heynote! 👋
|
||||||
|
|
||||||
${keyHelpStr(os.platform())}
|
${keyHelpStr(os.platform())}
|
||||||
|
∞∞∞markdown
|
||||||
|
Read full documentation at https://heynote.com/docs
|
||||||
∞∞∞math
|
∞∞∞math
|
||||||
This is a Math block. Here, rows are evaluated as math expressions.
|
This is a Math block. Here, rows are evaluated as math expressions.
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
UPDATE_START_DOWNLOAD,
|
UPDATE_START_DOWNLOAD,
|
||||||
UPDATE_INSTALL_AND_RESTART,
|
UPDATE_INSTALL_AND_RESTART,
|
||||||
UPDATE_CHECK_FOR_UPDATES,
|
UPDATE_CHECK_FOR_UPDATES,
|
||||||
} from '../constants'
|
} from '@/src/common/constants'
|
||||||
import { setForceQuit } from "./index";
|
import { setForceQuit } from "./index";
|
||||||
|
|
||||||
|
|
@ -1,166 +0,0 @@
|
|||||||
import fs from "fs"
|
|
||||||
import os from "node:os"
|
|
||||||
import { join, dirname, basename } from "path"
|
|
||||||
import { app, ipcMain, dialog } from "electron"
|
|
||||||
import * as jetpack from "fs-jetpack";
|
|
||||||
|
|
||||||
import CONFIG from "../config"
|
|
||||||
import { isDev } from "../detect-platform"
|
|
||||||
import { win } from "./index"
|
|
||||||
import { eraseInitialContent, initialContent, initialDevContent } from '../initial-content'
|
|
||||||
|
|
||||||
const untildify = (pathWithTilde) => {
|
|
||||||
const homeDirectory = os.homedir();
|
|
||||||
return homeDirectory
|
|
||||||
? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory)
|
|
||||||
: pathWithTilde;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function constructBufferFilePath(directoryPath) {
|
|
||||||
return join(untildify(directoryPath), isDev ? "buffer-dev.txt" : "buffer.txt")
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getBufferFilePath() {
|
|
||||||
let defaultPath = app.getPath("userData")
|
|
||||||
let configPath = CONFIG.get("settings.bufferPath")
|
|
||||||
let bufferPath = configPath.length ? configPath : defaultPath
|
|
||||||
let bufferFilePath = constructBufferFilePath(bufferPath)
|
|
||||||
try {
|
|
||||||
// use realpathSync to resolve a potential symlink
|
|
||||||
return fs.realpathSync(bufferFilePath)
|
|
||||||
} catch (err) {
|
|
||||||
// realpathSync will fail if the file does not exist, but that doesn't matter since the file will be created
|
|
||||||
if (err.code !== "ENOENT") {
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
return bufferFilePath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export class Buffer {
|
|
||||||
constructor({filePath, onChange}) {
|
|
||||||
this.filePath = filePath
|
|
||||||
this.onChange = onChange
|
|
||||||
this.watcher = null
|
|
||||||
this.setupWatcher()
|
|
||||||
this._lastSavedContent = null
|
|
||||||
}
|
|
||||||
|
|
||||||
async load() {
|
|
||||||
const content = await jetpack.read(this.filePath, 'utf8')
|
|
||||||
this.setupWatcher()
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
|
|
||||||
async save(content) {
|
|
||||||
this._lastSavedContent = content
|
|
||||||
const saveResult = await jetpack.write(this.filePath, content, {
|
|
||||||
atomic: true,
|
|
||||||
mode: '600',
|
|
||||||
})
|
|
||||||
return saveResult
|
|
||||||
}
|
|
||||||
|
|
||||||
exists() {
|
|
||||||
return jetpack.exists(this.filePath) === "file"
|
|
||||||
}
|
|
||||||
|
|
||||||
setupWatcher() {
|
|
||||||
if (!this.watcher && this.exists()) {
|
|
||||||
this.watcher = fs.watch(
|
|
||||||
dirname(this.filePath),
|
|
||||||
{
|
|
||||||
persistent: true,
|
|
||||||
recursive: false,
|
|
||||||
encoding: "utf8",
|
|
||||||
},
|
|
||||||
async (eventType, filename) => {
|
|
||||||
if (filename !== basename(this.filePath)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// read the file content and compare it to the last saved content
|
|
||||||
// (if the content is the same, then we can ignore the event)
|
|
||||||
const content = await jetpack.read(this.filePath, 'utf8')
|
|
||||||
|
|
||||||
if (this._lastSavedContent !== content) {
|
|
||||||
// file has changed on disk, trigger onChange
|
|
||||||
this.onChange(content)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
close() {
|
|
||||||
if (this.watcher) {
|
|
||||||
this.watcher.close()
|
|
||||||
this.watcher = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Buffer
|
|
||||||
let buffer
|
|
||||||
export function loadBuffer() {
|
|
||||||
if (buffer) {
|
|
||||||
buffer.close()
|
|
||||||
}
|
|
||||||
buffer = new Buffer({
|
|
||||||
filePath: getBufferFilePath(),
|
|
||||||
onChange: (content) => {
|
|
||||||
win?.webContents.send("buffer-content:change", content)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
ipcMain.handle('buffer-content:load', async () => {
|
|
||||||
if (buffer.exists() && !(eraseInitialContent && isDev)) {
|
|
||||||
return await buffer.load()
|
|
||||||
} else {
|
|
||||||
return isDev ? initialDevContent : initialContent
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
async function save(content) {
|
|
||||||
return await buffer.save(content)
|
|
||||||
}
|
|
||||||
|
|
||||||
ipcMain.handle('buffer-content:save', async (event, content) => {
|
|
||||||
return await save(content)
|
|
||||||
});
|
|
||||||
|
|
||||||
export let contentSaved = false
|
|
||||||
ipcMain.handle('buffer-content:saveAndQuit', async (event, content) => {
|
|
||||||
await save(content)
|
|
||||||
contentSaved = true
|
|
||||||
app.quit()
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.handle("buffer-content:selectLocation", async () => {
|
|
||||||
let result = await dialog.showOpenDialog({
|
|
||||||
title: "Select directory to store buffer",
|
|
||||||
properties: [
|
|
||||||
"openDirectory",
|
|
||||||
"createDirectory",
|
|
||||||
"noResolveAliases",
|
|
||||||
],
|
|
||||||
})
|
|
||||||
if (result.canceled) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const filePath = result.filePaths[0]
|
|
||||||
if (fs.existsSync(constructBufferFilePath(filePath))) {
|
|
||||||
if (dialog.showMessageBoxSync({
|
|
||||||
type: "question",
|
|
||||||
message: "The selected directory already contains a buffer file. It will be loaded. Do you want to continue?",
|
|
||||||
buttons: ["Cancel", "Continue"],
|
|
||||||
}) === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filePath
|
|
||||||
})
|
|
394
electron/main/file-library.js
Normal file
@ -0,0 +1,394 @@
|
|||||||
|
import fs from "fs"
|
||||||
|
import os from "node:os"
|
||||||
|
import { join, basename } from "path"
|
||||||
|
|
||||||
|
import * as jetpack from "fs-jetpack";
|
||||||
|
import { app, ipcMain, dialog } from "electron"
|
||||||
|
|
||||||
|
import CONFIG from "../config"
|
||||||
|
import { SCRATCH_FILE_NAME } from "../../src/common/constants"
|
||||||
|
import { NoteFormat } from "../../src/common/note-format"
|
||||||
|
import { isDev } from '../detect-platform';
|
||||||
|
import { initialContent, initialDevContent } from '../initial-content'
|
||||||
|
|
||||||
|
export const NOTES_DIR_NAME = isDev ? "notes-dev" : "notes"
|
||||||
|
|
||||||
|
|
||||||
|
let library
|
||||||
|
|
||||||
|
const untildify = (pathWithTilde) => {
|
||||||
|
const homeDir = os.homedir()
|
||||||
|
return homeDir ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDir) : pathWithTilde
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readNoteMetadata(filePath) {
|
||||||
|
const chunks = []
|
||||||
|
for await (let chunk of fs.createReadStream(filePath, { start: 0, end:4000 })) {
|
||||||
|
chunks.push(chunk)
|
||||||
|
}
|
||||||
|
const headContent = Buffer.concat(chunks).toString("utf8")
|
||||||
|
const firstSeparator = headContent.indexOf("\n∞∞∞")
|
||||||
|
if (firstSeparator === -1) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const metadata = JSON.parse(headContent.slice(0, firstSeparator).trim())
|
||||||
|
return {"name": metadata.name, "tags": metadata.tags}
|
||||||
|
} catch (e) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class FileLibrary {
|
||||||
|
constructor(basePath, win) {
|
||||||
|
this.win = win
|
||||||
|
basePath = untildify(basePath)
|
||||||
|
if (jetpack.exists(basePath) !== "dir") {
|
||||||
|
throw new Error(`Path directory does not exist: ${basePath}`)
|
||||||
|
}
|
||||||
|
this.basePath = fs.realpathSync(basePath)
|
||||||
|
this.jetpack = jetpack.cwd(this.basePath)
|
||||||
|
this.files = {};
|
||||||
|
this.watcher = null;
|
||||||
|
this.contentSaved = false
|
||||||
|
this.onChangeCallback = null
|
||||||
|
this._onWindowFocus = null
|
||||||
|
|
||||||
|
// create scratch.txt if it doesn't exist
|
||||||
|
if (!this.jetpack.exists(SCRATCH_FILE_NAME)) {
|
||||||
|
this.jetpack.write(SCRATCH_FILE_NAME, isDev ? initialDevContent : initialContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async exists(path) {
|
||||||
|
return this.jetpack.exists(path) === "file"
|
||||||
|
}
|
||||||
|
|
||||||
|
async load(path) {
|
||||||
|
if (this.files[path]) {
|
||||||
|
return this.files[path].load()
|
||||||
|
}
|
||||||
|
const fullPath = fs.realpathSync(join(this.basePath, path))
|
||||||
|
this.files[path] = new NoteBuffer({fullPath, library:this})
|
||||||
|
return await this.files[path].load()
|
||||||
|
}
|
||||||
|
|
||||||
|
async save(path, content) {
|
||||||
|
if (!this.files[path]) {
|
||||||
|
throw new Error(`File not loaded: ${path}`)
|
||||||
|
}
|
||||||
|
return await this.files[path].save(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(path, content) {
|
||||||
|
if (await this.exists(path)) {
|
||||||
|
throw new Error(`File already exists: ${path}`)
|
||||||
|
}
|
||||||
|
const fullPath = join(this.basePath, path)
|
||||||
|
await this.jetpack.writeAsync(fullPath, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
async move(path, newPath) {
|
||||||
|
if (await this.exists(newPath)) {
|
||||||
|
throw new Error(`File already exists: ${newPath}`)
|
||||||
|
}
|
||||||
|
const fullOldPath = join(this.basePath, path)
|
||||||
|
const fullNewPath = join(this.basePath, newPath)
|
||||||
|
await this.jetpack.moveAsync(fullOldPath, fullNewPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(path) {
|
||||||
|
if (path === SCRATCH_FILE_NAME) {
|
||||||
|
throw new Error("Can't delete scratch file")
|
||||||
|
}
|
||||||
|
const fullPath = join(this.basePath, path)
|
||||||
|
await this.jetpack.removeAsync(fullPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getList() {
|
||||||
|
//console.log("Listing notes")
|
||||||
|
const notes = {}
|
||||||
|
const files = await this.jetpack.findAsync(".", {
|
||||||
|
matching: "*.txt",
|
||||||
|
recursive: true,
|
||||||
|
})
|
||||||
|
const promises = []
|
||||||
|
for (const file of files) {
|
||||||
|
promises.push(readNoteMetadata(join(this.basePath, file)))
|
||||||
|
}
|
||||||
|
const metadataList = await Promise.all(promises)
|
||||||
|
metadataList.forEach((metadata, i) => {
|
||||||
|
const path = files[i]
|
||||||
|
notes[path] = metadata
|
||||||
|
})
|
||||||
|
return notes
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Array<string>} List of path to all directories, but not the root directory.
|
||||||
|
*/
|
||||||
|
async getDirectoryList() {
|
||||||
|
const directories = await this.jetpack.findAsync("", {
|
||||||
|
files: false,
|
||||||
|
directories: true,
|
||||||
|
recursive: true,
|
||||||
|
})
|
||||||
|
return directories
|
||||||
|
}
|
||||||
|
|
||||||
|
setupWatcher() {
|
||||||
|
if (!this.watcher) {
|
||||||
|
this.watcher = fs.watch(
|
||||||
|
this.basePath,
|
||||||
|
{
|
||||||
|
persistent: true,
|
||||||
|
recursive: true,
|
||||||
|
encoding: "utf8",
|
||||||
|
},
|
||||||
|
async (eventType, changedPath) => {
|
||||||
|
//console.log("File changed", eventType, changedPath)
|
||||||
|
for (const [path, buffer] of Object.entries(this.files)) {
|
||||||
|
if (changedPath === basename(path)) {
|
||||||
|
const content = await buffer.loadIfChanged()
|
||||||
|
if (content !== null) {
|
||||||
|
this.win.webContents.send("buffer:change", path, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// fs.watch() is unreliable in some cases, e.g. OneDrive on Windows. Therefor we'll load the open buffer files
|
||||||
|
// and check for changes when the window gets focus.
|
||||||
|
this._onWindowFocus = async (event) => {
|
||||||
|
for (const [path, buffer] of Object.entries(this.files)) {
|
||||||
|
const content = await buffer.loadIfChanged()
|
||||||
|
if (content !== null) {
|
||||||
|
this.win.webContents.send("buffer:change", path, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.win.on("focus", this._onWindowFocus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeFile(path) {
|
||||||
|
if (this.files[path]) {
|
||||||
|
delete this.files[path]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
for (const buffer of Object.values(this.files)) {
|
||||||
|
this.closeFile(buffer.filePath)
|
||||||
|
}
|
||||||
|
this.stopWatcher()
|
||||||
|
}
|
||||||
|
|
||||||
|
stopWatcher() {
|
||||||
|
if (this.watcher) {
|
||||||
|
this.watcher.close()
|
||||||
|
this.watcher = null
|
||||||
|
}
|
||||||
|
if (this._onWindowFocus) {
|
||||||
|
this.win.off("focus", this._onWindowFocus)
|
||||||
|
this._onWindowFocus = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export class NoteBuffer {
|
||||||
|
constructor({fullPath, library}) {
|
||||||
|
this.fullPath = fullPath
|
||||||
|
this._lastKnownContent = null
|
||||||
|
this.library = library
|
||||||
|
}
|
||||||
|
|
||||||
|
async read() {
|
||||||
|
return await this.library.jetpack.read(this.fullPath, 'utf8')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* load() assumes that the actual note buffer is actually updated with the new content, otherwise
|
||||||
|
* _lastKnownContent will be out of sync. If you just want to read the content, use read() instead.
|
||||||
|
*/
|
||||||
|
async load() {
|
||||||
|
const content = await this.read()
|
||||||
|
this._lastKnownContent = content
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* loadIfChanged() will only return the content if it has changed since the last time it was loaded.
|
||||||
|
* If content is returned, the note buffer must be updated with the new content in order to keep the
|
||||||
|
* _lastKnownContent in sync.
|
||||||
|
*/
|
||||||
|
async loadIfChanged() {
|
||||||
|
const content = await this.read()
|
||||||
|
// if the file was removed (e.g. during an atomic save) the content will be undefined
|
||||||
|
if (content !== undefined && this._lastKnownContent !== content) {
|
||||||
|
this._lastKnownContent = content
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async save(content) {
|
||||||
|
this._lastKnownContent = content
|
||||||
|
const saveResult = await this.library.jetpack.write(this.fullPath, content, {
|
||||||
|
atomic: true,
|
||||||
|
mode: '600',
|
||||||
|
})
|
||||||
|
return saveResult
|
||||||
|
}
|
||||||
|
|
||||||
|
exists() {
|
||||||
|
return jetpack.exists(this.fullPath) === "file"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setCurrentFileLibrary(lib) {
|
||||||
|
library = lib
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setupFileLibraryEventHandlers() {
|
||||||
|
ipcMain.handle('buffer:load', async (event, path) => {
|
||||||
|
//console.log("buffer:load", path)
|
||||||
|
return await library.load(path)
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
ipcMain.handle('buffer:save', async (event, path, content) => {
|
||||||
|
return await library.save(path, content)
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('buffer:create', async (event, path, content) => {
|
||||||
|
return await library.create(path, content)
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('buffer:getList', async (event) => {
|
||||||
|
return await library.getList()
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('buffer:getDirectoryList', async (event) => {
|
||||||
|
return await library.getDirectoryList()
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('buffer:exists', async (event, path) => {
|
||||||
|
return await library.exists(path)
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('buffer:close', async (event, path) => {
|
||||||
|
return await library.closeFile(path)
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('buffer:saveAndQuit', async (event, contents) => {
|
||||||
|
library.stopWatcher()
|
||||||
|
for (const [path, content] of contents) {
|
||||||
|
await library.save(path, content)
|
||||||
|
}
|
||||||
|
library.contentSaved = true
|
||||||
|
app.quit()
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('buffer:move', async (event, path, newPath) => {
|
||||||
|
return await library.move(path, newPath)
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('buffer:delete', async (event, path) => {
|
||||||
|
return await library.delete(path)
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("library:selectLocation", async () => {
|
||||||
|
let result = await dialog.showOpenDialog({
|
||||||
|
title: "Select directory to store buffer",
|
||||||
|
properties: [
|
||||||
|
"openDirectory",
|
||||||
|
"createDirectory",
|
||||||
|
"noResolveAliases",
|
||||||
|
],
|
||||||
|
})
|
||||||
|
if (result.canceled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const filePath = result.filePaths[0]
|
||||||
|
return filePath
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function migrateBufferFileToLibrary(app) {
|
||||||
|
async function ensureBufferFileMetadata(filePath) {
|
||||||
|
const metadata = await readNoteMetadata(filePath)
|
||||||
|
//console.log("Metadata", metadata)
|
||||||
|
if (!metadata || !metadata.name) {
|
||||||
|
console.log("Adding metadata to", filePath)
|
||||||
|
const note = NoteFormat.load(jetpack.read(filePath))
|
||||||
|
note.metadata.name = "Scratch"
|
||||||
|
jetpack.write(filePath, note.serialize())
|
||||||
|
} else {
|
||||||
|
console.log("Metadata already exists for", filePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBackupFile(filePath) {
|
||||||
|
// Get a backup file path by adding a .bak suffix. If the file already exists, add a number suffix.
|
||||||
|
let backupFile = filePath + ".bak";
|
||||||
|
for (let i = 1; i < 1000; i++) {
|
||||||
|
if (jetpack.exists(backupFile) !== "file") {
|
||||||
|
return backupFile;
|
||||||
|
}
|
||||||
|
backupFile = `${filePath}.bak.${i}`;
|
||||||
|
}
|
||||||
|
throw new Error(`Unable to find an available file path after 1000 attempts for base path: ${filePath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultLibraryPath = join(app.getPath("userData"), NOTES_DIR_NAME)
|
||||||
|
const customBufferPath = CONFIG.get("settings.bufferPath")
|
||||||
|
const oldBufferFile = isDev ? "buffer-dev.txt" : "buffer.txt"
|
||||||
|
if (customBufferPath) {
|
||||||
|
// if the new buffer file exists, no need to migrate
|
||||||
|
if (jetpack.exists(join(customBufferPath, SCRATCH_FILE_NAME)) === "file") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const oldBufferFileFullPath = join(customBufferPath, oldBufferFile)
|
||||||
|
const backupFile = getBackupFile(oldBufferFileFullPath)
|
||||||
|
if (jetpack.exists(oldBufferFileFullPath) === "file") {
|
||||||
|
// make a backup copy of the old buffer file
|
||||||
|
console.log(`Taking backup of ${oldBufferFileFullPath} to ${backupFile}`)
|
||||||
|
jetpack.copy(oldBufferFileFullPath, backupFile)
|
||||||
|
|
||||||
|
// rename buffer file to scratch.txt
|
||||||
|
const newFileFullPath = join(customBufferPath, SCRATCH_FILE_NAME);
|
||||||
|
console.log(`Migrating file ${oldBufferFileFullPath} to ${newFileFullPath}`)
|
||||||
|
jetpack.move(oldBufferFileFullPath, newFileFullPath)
|
||||||
|
// add metadata to scratch.txt (just to be sure, we'll double check that it's needed first)
|
||||||
|
await ensureBufferFileMetadata(newFileFullPath)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if the new buffer file exists, no need to migrate
|
||||||
|
if (jetpack.exists(join(defaultLibraryPath, SCRATCH_FILE_NAME)) === "file") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// check if the old buffer file exists, while the default *library* path doesn't exist
|
||||||
|
const oldBufferFileFullPath = join(app.getPath("userData"), oldBufferFile)
|
||||||
|
const backupFile = getBackupFile(oldBufferFileFullPath)
|
||||||
|
if (jetpack.exists(oldBufferFileFullPath) === "file" && jetpack.exists(defaultLibraryPath) !== "dir") {
|
||||||
|
// make a backup copy of the old buffer file
|
||||||
|
console.log(`Taking backup of ${oldBufferFileFullPath} to ${backupFile}`)
|
||||||
|
jetpack.copy(oldBufferFileFullPath, backupFile)
|
||||||
|
|
||||||
|
const newFileFullPath = join(defaultLibraryPath, SCRATCH_FILE_NAME);
|
||||||
|
console.log(`Migrating buffer file ${oldBufferFileFullPath} to ${newFileFullPath}`)
|
||||||
|
// create the default library path
|
||||||
|
jetpack.dir(defaultLibraryPath)
|
||||||
|
// move the buffer file to the library path
|
||||||
|
jetpack.move(oldBufferFileFullPath, newFileFullPath)
|
||||||
|
// add metadata to scratch.txt
|
||||||
|
await ensureBufferFileMetadata(newFileFullPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,13 +3,20 @@ import { release } from 'node:os'
|
|||||||
import { join } from 'node:path'
|
import { join } from 'node:path'
|
||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
|
|
||||||
import { menu, getTrayMenu } from './menu'
|
import { WINDOW_CLOSE_EVENT, SETTINGS_CHANGE_EVENT } from '@/src/common/constants'
|
||||||
import { WINDOW_CLOSE_EVENT, SETTINGS_CHANGE_EVENT } from '../constants';
|
|
||||||
|
import { menu, getTrayMenu, getEditorContextMenu } from './menu'
|
||||||
import CONFIG from "../config"
|
import CONFIG from "../config"
|
||||||
import { isDev, isLinux, isMac, isWindows } from '../detect-platform';
|
import { isDev, isLinux, isMac, isWindows } from '../detect-platform';
|
||||||
import { initializeAutoUpdate, checkForUpdates } from './auto-update';
|
import { initializeAutoUpdate, checkForUpdates } from './auto-update';
|
||||||
import { fixElectronCors } from './cors';
|
import { fixElectronCors } from './cors';
|
||||||
import { loadBuffer, contentSaved } from './buffer';
|
import {
|
||||||
|
FileLibrary,
|
||||||
|
setupFileLibraryEventHandlers,
|
||||||
|
setCurrentFileLibrary,
|
||||||
|
migrateBufferFileToLibrary,
|
||||||
|
NOTES_DIR_NAME
|
||||||
|
} from './file-library';
|
||||||
|
|
||||||
|
|
||||||
// The built directory structure
|
// The built directory structure
|
||||||
@ -49,14 +56,14 @@ Menu.setApplicationMenu(menu)
|
|||||||
// process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'
|
// process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true'
|
||||||
|
|
||||||
export let win: BrowserWindow | null = null
|
export let win: BrowserWindow | null = null
|
||||||
|
let fileLibrary: FileLibrary | null = null
|
||||||
let tray: Tray | null = null;
|
let tray: Tray | null = null;
|
||||||
|
let initErrors: string[] = []
|
||||||
// Here, you can also use other preload
|
// Here, you can also use other preload
|
||||||
const preload = join(__dirname, '../preload/index.js')
|
const preload = join(__dirname, '../preload/index.js')
|
||||||
const url = process.env.VITE_DEV_SERVER_URL
|
const url = process.env.VITE_DEV_SERVER_URL
|
||||||
const indexHtml = join(process.env.DIST, 'index.html')
|
const indexHtml = join(process.env.DIST, 'index.html')
|
||||||
|
|
||||||
let currentKeymap = CONFIG.get("settings.keymap")
|
|
||||||
|
|
||||||
// if this version is a beta version, set the release channel to beta
|
// if this version is a beta version, set the release channel to beta
|
||||||
const isBetaVersion = app.getVersion().includes("beta")
|
const isBetaVersion = app.getVersion().includes("beta")
|
||||||
if (isBetaVersion) {
|
if (isBetaVersion) {
|
||||||
@ -76,8 +83,8 @@ export function quit() {
|
|||||||
async function createWindow() {
|
async function createWindow() {
|
||||||
// read any stored window settings from config, or use defaults
|
// read any stored window settings from config, or use defaults
|
||||||
let windowConfig = {
|
let windowConfig = {
|
||||||
width: CONFIG.get("windowConfig.width", 900) as number,
|
width: CONFIG.get("windowConfig.width", 940) as number,
|
||||||
height: CONFIG.get("windowConfig.height", 680) as number,
|
height: CONFIG.get("windowConfig.height", 720) as number,
|
||||||
isMaximized: CONFIG.get("windowConfig.isMaximized", false) as boolean,
|
isMaximized: CONFIG.get("windowConfig.isMaximized", false) as boolean,
|
||||||
isFullScreen: CONFIG.get("windowConfig.isFullScreen", false) as boolean,
|
isFullScreen: CONFIG.get("windowConfig.isFullScreen", false) as boolean,
|
||||||
x: CONFIG.get("windowConfig.x"),
|
x: CONFIG.get("windowConfig.x"),
|
||||||
@ -106,9 +113,17 @@ async function createWindow() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pngSystems: NodeJS.Platform[] = ["linux", "freebsd", "openbsd", "netbsd"]
|
||||||
|
const icon = join(
|
||||||
|
process.env.PUBLIC,
|
||||||
|
pngSystems.includes(process.platform)
|
||||||
|
? "favicon-linux.png"
|
||||||
|
: "favicon.ico",
|
||||||
|
)
|
||||||
|
|
||||||
win = new BrowserWindow(Object.assign({
|
win = new BrowserWindow(Object.assign({
|
||||||
title: 'heynote',
|
title: 'heynote',
|
||||||
icon: join(process.env.PUBLIC, 'favicon.ico'),
|
icon,
|
||||||
backgroundColor: nativeTheme.shouldUseDarkColors ? '#262B37' : '#FFFFFF',
|
backgroundColor: nativeTheme.shouldUseDarkColors ? '#262B37' : '#FFFFFF',
|
||||||
//titleBarStyle: 'customButtonsOnHover',
|
//titleBarStyle: 'customButtonsOnHover',
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
@ -138,7 +153,7 @@ async function createWindow() {
|
|||||||
}
|
}
|
||||||
// Prevent the window from closing, and send a message to the renderer which will in turn
|
// Prevent the window from closing, and send a message to the renderer which will in turn
|
||||||
// send a message to the main process to save the current buffer and close the window.
|
// send a message to the main process to save the current buffer and close the window.
|
||||||
if (!contentSaved) {
|
if (!!fileLibrary && !fileLibrary.contentSaved) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
win?.webContents.send(WINDOW_CLOSE_EVENT)
|
win?.webContents.send(WINDOW_CLOSE_EVENT)
|
||||||
} else {
|
} else {
|
||||||
@ -233,6 +248,11 @@ function registerGlobalHotkey() {
|
|||||||
// if alwaysOnTop is on, calling app.hide() won't hide the window
|
// if alwaysOnTop is on, calling app.hide() won't hide the window
|
||||||
win.hide()
|
win.hide()
|
||||||
}
|
}
|
||||||
|
} else if (isLinux) {
|
||||||
|
win.blur()
|
||||||
|
// If we don't hide the window, it will stay on top of the stack even though it's not visible
|
||||||
|
// and pressing the hotkey again won't do anything
|
||||||
|
win.hide()
|
||||||
} else {
|
} else {
|
||||||
win.blur()
|
win.blur()
|
||||||
if (CONFIG.get("settings.showInMenu") || CONFIG.get("settings.alwaysOnTop")) {
|
if (CONFIG.get("settings.showInMenu") || CONFIG.get("settings.alwaysOnTop")) {
|
||||||
@ -302,6 +322,9 @@ function registerAlwaysOnTop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.whenReady().then(createWindow).then(async () => {
|
app.whenReady().then(createWindow).then(async () => {
|
||||||
|
initFileLibrary(win).then(() => {
|
||||||
|
setupFileLibraryEventHandlers()
|
||||||
|
})
|
||||||
initializeAutoUpdate(win)
|
initializeAutoUpdate(win)
|
||||||
registerGlobalHotkey()
|
registerGlobalHotkey()
|
||||||
registerShowInDock()
|
registerShowInDock()
|
||||||
@ -327,10 +350,14 @@ app.on('second-instance', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
app.on('activate', () => {
|
app.on('activate', (event, hasVisibleWindows) => {
|
||||||
const allWindows = BrowserWindow.getAllWindows()
|
const allWindows = BrowserWindow.getAllWindows()
|
||||||
if (allWindows.length) {
|
if (allWindows.length) {
|
||||||
allWindows[0].focus()
|
allWindows[0].focus()
|
||||||
|
// show the window if it's hidden (e.g. the window was closed with "show in menu bar" setting turned on)
|
||||||
|
if (!allWindows[0].isVisible()) {
|
||||||
|
allWindows[0].show()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
createWindow()
|
createWindow()
|
||||||
}
|
}
|
||||||
@ -343,14 +370,43 @@ ipcMain.handle('dark-mode:set', (event, mode) => {
|
|||||||
|
|
||||||
ipcMain.handle('dark-mode:get', () => nativeTheme.themeSource)
|
ipcMain.handle('dark-mode:get', () => nativeTheme.themeSource)
|
||||||
|
|
||||||
// load buffer on app start
|
ipcMain.handle("setWindowTitle", (event, title) => {
|
||||||
loadBuffer()
|
win?.setTitle(title)
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle("showEditorContextMenu", () => {
|
||||||
|
getEditorContextMenu(win).popup({window:win});
|
||||||
|
})
|
||||||
|
|
||||||
|
// Initialize note/file library
|
||||||
|
async function initFileLibrary(win) {
|
||||||
|
await migrateBufferFileToLibrary(app)
|
||||||
|
|
||||||
|
const customLibraryPath = CONFIG.get("settings.bufferPath")
|
||||||
|
const defaultLibraryPath = join(app.getPath("userData"), NOTES_DIR_NAME)
|
||||||
|
const libraryPath = customLibraryPath ? customLibraryPath : defaultLibraryPath
|
||||||
|
//console.log("libraryPath", libraryPath)
|
||||||
|
|
||||||
|
// if we're using the default library path, and it doesn't exist (e.g. first time run), create it
|
||||||
|
if (!customLibraryPath && !fs.existsSync(defaultLibraryPath)) {
|
||||||
|
fs.mkdirSync(defaultLibraryPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
fileLibrary = new FileLibrary(libraryPath, win)
|
||||||
|
fileLibrary.setupWatcher()
|
||||||
|
} catch (error) {
|
||||||
|
initErrors.push(`Error: ${error.message}`)
|
||||||
|
}
|
||||||
|
setCurrentFileLibrary(fileLibrary)
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle("getInitErrors", () => {
|
||||||
|
return initErrors
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
ipcMain.handle('settings:set', async (event, settings) => {
|
ipcMain.handle('settings:set', async (event, settings) => {
|
||||||
if (settings.keymap !== CONFIG.get("settings.keymap")) {
|
|
||||||
currentKeymap = settings.keymap
|
|
||||||
}
|
|
||||||
let globalHotkeyChanged = settings.enableGlobalHotkey !== CONFIG.get("settings.enableGlobalHotkey") || settings.globalHotkey !== CONFIG.get("settings.globalHotkey")
|
let globalHotkeyChanged = settings.enableGlobalHotkey !== CONFIG.get("settings.enableGlobalHotkey") || settings.globalHotkey !== CONFIG.get("settings.globalHotkey")
|
||||||
let showInDockChanged = settings.showInDock !== CONFIG.get("settings.showInDock");
|
let showInDockChanged = settings.showInDock !== CONFIG.get("settings.showInDock");
|
||||||
let showInMenuChanged = settings.showInMenu !== CONFIG.get("settings.showInMenu");
|
let showInMenuChanged = settings.showInMenu !== CONFIG.get("settings.showInMenu");
|
||||||
@ -373,9 +429,10 @@ ipcMain.handle('settings:set', async (event, settings) => {
|
|||||||
registerAlwaysOnTop()
|
registerAlwaysOnTop()
|
||||||
}
|
}
|
||||||
if (bufferPathChanged) {
|
if (bufferPathChanged) {
|
||||||
const buffer = loadBuffer()
|
console.log("bufferPath changed, closing existing file library")
|
||||||
if (buffer.exists()) {
|
fileLibrary.close()
|
||||||
win?.webContents.send("buffer-content:change", await buffer.load())
|
console.log("initializing new file library")
|
||||||
}
|
initFileLibrary(win)
|
||||||
|
await win.webContents.send("library:pathChanged")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,10 +1,59 @@
|
|||||||
const { app, Menu } = require("electron")
|
const { app, Menu } = require("electron")
|
||||||
import { OPEN_SETTINGS_EVENT } from "../constants";
|
import { OPEN_SETTINGS_EVENT, UNDO_EVENT, REDO_EVENT, MOVE_BLOCK_EVENT, DELETE_BLOCK_EVENT, CHANGE_BUFFER_EVENT, SELECT_ALL_EVENT } from '@/src/common/constants'
|
||||||
import { openAboutWindow } from "./about";
|
import { openAboutWindow } from "./about";
|
||||||
import { quit } from "./index"
|
import { quit } from "./index"
|
||||||
|
|
||||||
const isMac = process.platform === "darwin"
|
const isMac = process.platform === "darwin"
|
||||||
|
|
||||||
|
|
||||||
|
const undoMenuItem = {
|
||||||
|
label: 'Undo',
|
||||||
|
accelerator: 'CommandOrControl+z',
|
||||||
|
click: (menuItem, window, event) => {
|
||||||
|
window?.webContents.send(UNDO_EVENT)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const redoMenuItem = {
|
||||||
|
label: 'Redo',
|
||||||
|
accelerator: 'CommandOrControl+Shift+z',
|
||||||
|
click: (menuItem, window, event) => {
|
||||||
|
window?.webContents.send(REDO_EVENT)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectAllMenuItem = {
|
||||||
|
label: 'Select All',
|
||||||
|
accelerator: 'CommandOrControl+a',
|
||||||
|
click: (menuItem, window, event) => {
|
||||||
|
window?.webContents.send(SELECT_ALL_EVENT)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteBlockMenuItem = {
|
||||||
|
label: 'Delete block',
|
||||||
|
accelerator: 'CommandOrControl+Shift+D',
|
||||||
|
click: (menuItem, window, event) => {
|
||||||
|
window?.webContents.send(DELETE_BLOCK_EVENT)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const moveBlockMenuItem = {
|
||||||
|
label: 'Move block to another buffer…',
|
||||||
|
accelerator: 'CommandOrControl+S',
|
||||||
|
click: (menuItem, window, event) => {
|
||||||
|
window?.webContents.send(MOVE_BLOCK_EVENT)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeBufferMenuItem = {
|
||||||
|
label: 'Switch buffer…',
|
||||||
|
accelerator: 'CommandOrControl+P',
|
||||||
|
click: (menuItem, window, event) => {
|
||||||
|
window?.webContents.send(CHANGE_BUFFER_EVENT)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
const template = [
|
const template = [
|
||||||
// { role: 'appMenu' }
|
// { role: 'appMenu' }
|
||||||
...(isMac ? [{
|
...(isMac ? [{
|
||||||
@ -18,6 +67,7 @@ const template = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
|
changeBufferMenuItem,
|
||||||
{
|
{
|
||||||
label: 'Settings',
|
label: 'Settings',
|
||||||
click: (menuItem, window, event) => {
|
click: (menuItem, window, event) => {
|
||||||
@ -37,6 +87,7 @@ const template = [
|
|||||||
}] : [{
|
}] : [{
|
||||||
role: 'fileMenu',
|
role: 'fileMenu',
|
||||||
submenu: [
|
submenu: [
|
||||||
|
changeBufferMenuItem,
|
||||||
{
|
{
|
||||||
label: 'Settings',
|
label: 'Settings',
|
||||||
click: (menuItem, window, event) => {
|
click: (menuItem, window, event) => {
|
||||||
@ -62,8 +113,11 @@ const template = [
|
|||||||
{
|
{
|
||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
submenu: [
|
submenu: [
|
||||||
{ role: 'undo' },
|
undoMenuItem,
|
||||||
{ role: 'redo' },
|
redoMenuItem,
|
||||||
|
{ type: 'separator' },
|
||||||
|
deleteBlockMenuItem,
|
||||||
|
moveBlockMenuItem,
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{ role: 'cut' },
|
{ role: 'cut' },
|
||||||
{ role: 'copy' },
|
{ role: 'copy' },
|
||||||
@ -71,7 +125,7 @@ const template = [
|
|||||||
...(isMac ? [
|
...(isMac ? [
|
||||||
{ role: 'pasteAndMatchStyle' },
|
{ role: 'pasteAndMatchStyle' },
|
||||||
{ role: 'delete' },
|
{ role: 'delete' },
|
||||||
{ role: 'selectAll' },
|
selectAllMenuItem,
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
label: 'Speech',
|
label: 'Speech',
|
||||||
@ -83,7 +137,7 @@ const template = [
|
|||||||
] : [
|
] : [
|
||||||
{ role: 'delete' },
|
{ role: 'delete' },
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{ role: 'selectAll' }
|
selectAllMenuItem,
|
||||||
])
|
])
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -131,7 +185,14 @@ const template = [
|
|||||||
role: 'help',
|
role: 'help',
|
||||||
submenu: [
|
submenu: [
|
||||||
{
|
{
|
||||||
label: 'Learn More',
|
label: 'Documentation',
|
||||||
|
click: async () => {
|
||||||
|
const { shell } = require('electron')
|
||||||
|
await shell.openExternal('https://heynote.com/docs/')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Website',
|
||||||
click: async () => {
|
click: async () => {
|
||||||
const { shell } = require('electron')
|
const { shell } = require('electron')
|
||||||
await shell.openExternal('https://heynote.com')
|
await shell.openExternal('https://heynote.com')
|
||||||
@ -164,3 +225,18 @@ export function getTrayMenu(win) {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getEditorContextMenu(win) {
|
||||||
|
return Menu.buildFromTemplate([
|
||||||
|
undoMenuItem,
|
||||||
|
redoMenuItem,
|
||||||
|
{type: 'separator'},
|
||||||
|
{role: 'cut'},
|
||||||
|
{role: 'copy'},
|
||||||
|
{role: 'paste'},
|
||||||
|
{type: 'separator'},
|
||||||
|
selectAllMenuItem,
|
||||||
|
{type: 'separator'},
|
||||||
|
deleteBlockMenuItem,
|
||||||
|
moveBlockMenuItem,
|
||||||
|
])
|
||||||
|
}
|
@ -1,11 +1,9 @@
|
|||||||
const { contextBridge } = require('electron')
|
const { contextBridge } = require('electron')
|
||||||
|
import { sep } from "path"
|
||||||
import themeMode from "./theme-mode"
|
import themeMode from "./theme-mode"
|
||||||
import { isMac, isWindows, isLinux } from "../detect-platform"
|
import { isMac, isWindows, isLinux, isDev } from "../detect-platform"
|
||||||
import { ipcRenderer } from "electron"
|
import { ipcRenderer } from "electron"
|
||||||
import {
|
import {
|
||||||
WINDOW_CLOSE_EVENT,
|
|
||||||
OPEN_SETTINGS_EVENT,
|
|
||||||
SETTINGS_CHANGE_EVENT,
|
|
||||||
UPDATE_AVAILABLE_EVENT,
|
UPDATE_AVAILABLE_EVENT,
|
||||||
UPDATE_ERROR,
|
UPDATE_ERROR,
|
||||||
UPDATE_DOWNLOAD_PROGRESS,
|
UPDATE_DOWNLOAD_PROGRESS,
|
||||||
@ -14,7 +12,7 @@ import {
|
|||||||
UPDATE_INSTALL_AND_RESTART,
|
UPDATE_INSTALL_AND_RESTART,
|
||||||
UPDATE_DOWNLOADED,
|
UPDATE_DOWNLOADED,
|
||||||
UPDATE_CHECK_FOR_UPDATES,
|
UPDATE_CHECK_FOR_UPDATES,
|
||||||
} from "../constants"
|
} from "@/src/common/constants"
|
||||||
import CONFIG from "../config"
|
import CONFIG from "../config"
|
||||||
import getCurrencyData from "./currency"
|
import getCurrencyData from "./currency"
|
||||||
|
|
||||||
@ -30,41 +28,97 @@ contextBridge.exposeInMainWorld("heynote", {
|
|||||||
isWebApp: false,
|
isWebApp: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isDev: isDev,
|
||||||
themeMode: themeMode,
|
themeMode: themeMode,
|
||||||
|
|
||||||
quit() {
|
init() {
|
||||||
console.log("quitting")
|
ipcRenderer.on("buffer:change", (event, path, content) => {
|
||||||
//ipcRenderer.invoke("app_quit")
|
// called on all changes to open buffer files
|
||||||
|
// go through all registered callbacks for this path and call them
|
||||||
|
if (this.buffer._onChangeCallbacks[path]) {
|
||||||
|
this.buffer._onChangeCallbacks[path].forEach(callback => callback(content))
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
onWindowClose(callback) {
|
mainProcess: {
|
||||||
ipcRenderer.on(WINDOW_CLOSE_EVENT, callback)
|
on(event, callback) {
|
||||||
|
ipcRenderer.on(event, callback)
|
||||||
},
|
},
|
||||||
|
|
||||||
onOpenSettings(callback) {
|
off(event, callback) {
|
||||||
ipcRenderer.on(OPEN_SETTINGS_EVENT, callback)
|
ipcRenderer.off(event, callback)
|
||||||
|
},
|
||||||
|
|
||||||
|
invoke(event, ...args) {
|
||||||
|
return ipcRenderer.invoke(event, ...args)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
buffer: {
|
buffer: {
|
||||||
async load() {
|
async exists(path) {
|
||||||
return await ipcRenderer.invoke("buffer-content:load")
|
return await ipcRenderer.invoke("buffer:exists", path)
|
||||||
},
|
},
|
||||||
|
|
||||||
async save(content) {
|
async getList() {
|
||||||
return await ipcRenderer.invoke("buffer-content:save", content)
|
return await ipcRenderer.invoke("buffer:getList")
|
||||||
},
|
},
|
||||||
|
|
||||||
async saveAndQuit(content) {
|
async getDirectoryList() {
|
||||||
return await ipcRenderer.invoke("buffer-content:saveAndQuit", content)
|
return await ipcRenderer.invoke("buffer:getDirectoryList")
|
||||||
},
|
},
|
||||||
|
|
||||||
onChangeCallback(callback) {
|
async load(path) {
|
||||||
ipcRenderer.on("buffer-content:change", callback)
|
return await ipcRenderer.invoke("buffer:load", path)
|
||||||
|
},
|
||||||
|
|
||||||
|
async save(path, content) {
|
||||||
|
return await ipcRenderer.invoke("buffer:save", path, content)
|
||||||
|
},
|
||||||
|
|
||||||
|
async delete(path) {
|
||||||
|
return await ipcRenderer.invoke("buffer:delete", path)
|
||||||
|
},
|
||||||
|
|
||||||
|
async move(path, newPath) {
|
||||||
|
return await ipcRenderer.invoke("buffer:move", path, newPath)
|
||||||
|
},
|
||||||
|
|
||||||
|
async create(path, content) {
|
||||||
|
return await ipcRenderer.invoke("buffer:create", path, content)
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveAndQuit(contents) {
|
||||||
|
return await ipcRenderer.invoke("buffer:saveAndQuit", contents)
|
||||||
|
},
|
||||||
|
|
||||||
|
async close(path) {
|
||||||
|
return await ipcRenderer.invoke("buffer:close", path)
|
||||||
|
},
|
||||||
|
|
||||||
|
_onChangeCallbacks: {},
|
||||||
|
addOnChangeCallback(path, callback) {
|
||||||
|
// register a callback to be called when the buffer content changes for a specific file
|
||||||
|
if (!this._onChangeCallbacks[path]) {
|
||||||
|
this._onChangeCallbacks[path] = []
|
||||||
|
}
|
||||||
|
this._onChangeCallbacks[path].push(callback)
|
||||||
|
},
|
||||||
|
removeOnChangeCallback(path, callback) {
|
||||||
|
if (this._onChangeCallbacks[path]) {
|
||||||
|
this._onChangeCallbacks[path] = this._onChangeCallbacks[path].filter(cb => cb !== callback)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async selectLocation() {
|
async selectLocation() {
|
||||||
return await ipcRenderer.invoke("buffer-content:selectLocation")
|
return await ipcRenderer.invoke("library:selectLocation")
|
||||||
}
|
},
|
||||||
|
|
||||||
|
setLibraryPathChangeCallback(callback) {
|
||||||
|
ipcRenderer.on("library:pathChanged", callback)
|
||||||
|
},
|
||||||
|
|
||||||
|
pathSeparator: sep,
|
||||||
},
|
},
|
||||||
|
|
||||||
settings: CONFIG.get("settings"),
|
settings: CONFIG.get("settings"),
|
||||||
@ -77,10 +131,6 @@ contextBridge.exposeInMainWorld("heynote", {
|
|||||||
return await getCurrencyData()
|
return await getCurrencyData()
|
||||||
},
|
},
|
||||||
|
|
||||||
onSettingsChange(callback) {
|
|
||||||
ipcRenderer.on(SETTINGS_CHANGE_EVENT, (event, settings) => callback(settings))
|
|
||||||
},
|
|
||||||
|
|
||||||
autoUpdate: {
|
autoUpdate: {
|
||||||
callbacks(callbacks) {
|
callbacks(callbacks) {
|
||||||
ipcRenderer.on(UPDATE_AVAILABLE_EVENT, (event, info) => callbacks?.updateAvailable(info))
|
ipcRenderer.on(UPDATE_AVAILABLE_EVENT, (event, info) => callbacks?.updateAvailable(info))
|
||||||
@ -103,11 +153,19 @@ contextBridge.exposeInMainWorld("heynote", {
|
|||||||
|
|
||||||
async getVersion() {
|
async getVersion() {
|
||||||
return await ipcRenderer.invoke("getVersion")
|
return await ipcRenderer.invoke("getVersion")
|
||||||
}
|
},
|
||||||
|
|
||||||
|
async getInitErrors() {
|
||||||
|
return await ipcRenderer.invoke("getInitErrors")
|
||||||
|
},
|
||||||
|
|
||||||
|
setWindowTitle(title) {
|
||||||
|
ipcRenderer.invoke("setWindowTitle", title)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
function domReady(condition: DocumentReadyState[] = ['complete', 'interactive']) {
|
function domReady(condition=['complete', 'interactive']) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (condition.includes(document.readyState)) {
|
if (condition.includes(document.readyState)) {
|
||||||
resolve(true)
|
resolve(true)
|
||||||
@ -122,12 +180,12 @@ function domReady(condition: DocumentReadyState[] = ['complete', 'interactive'])
|
|||||||
}
|
}
|
||||||
|
|
||||||
const safeDOM = {
|
const safeDOM = {
|
||||||
append(parent: HTMLElement, child: HTMLElement) {
|
append(parent, child) {
|
||||||
if (!Array.from(parent.children).find(e => e === child)) {
|
if (!Array.from(parent.children).find(e => e === child)) {
|
||||||
return parent.appendChild(child)
|
return parent.appendChild(child)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
remove(parent: HTMLElement, child: HTMLElement) {
|
remove(parent, child) {
|
||||||
if (Array.from(parent.children).find(e => e === child)) {
|
if (Array.from(parent.children).find(e => e === child)) {
|
||||||
return parent.removeChild(child)
|
return parent.removeChild(child)
|
||||||
}
|
}
|
7281
package-lock.json
generated
46
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Heynote",
|
"name": "Heynote",
|
||||||
"version": "1.7.0",
|
"version": "2.2.0-beta.2",
|
||||||
"main": "dist-electron/main/index.js",
|
"main": "dist-electron/main/index.js",
|
||||||
"description": "A dedicated scratch pad",
|
"description": "A dedicated scratch pad",
|
||||||
"author": "Jonatan Heyman (https://heyman.info)",
|
"author": "Jonatan Heyman (https://heyman.info)",
|
||||||
@ -32,7 +32,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@codemirror/autocomplete": "^6.11.1",
|
"@codemirror/autocomplete": "^6.11.1",
|
||||||
"@codemirror/commands": "^6.3.2",
|
"@codemirror/commands": "^6.8.1",
|
||||||
"@codemirror/lang-cpp": "^6.0.2",
|
"@codemirror/lang-cpp": "^6.0.2",
|
||||||
"@codemirror/lang-html": "^6.4.7",
|
"@codemirror/lang-html": "^6.4.7",
|
||||||
"@codemirror/lang-java": "^6.0.1",
|
"@codemirror/lang-java": "^6.0.1",
|
||||||
@ -44,39 +44,47 @@
|
|||||||
"@codemirror/lang-python": "^6.1.3",
|
"@codemirror/lang-python": "^6.1.3",
|
||||||
"@codemirror/lang-rust": "^6.0.1",
|
"@codemirror/lang-rust": "^6.0.1",
|
||||||
"@codemirror/lang-sql": "^6.5.4",
|
"@codemirror/lang-sql": "^6.5.4",
|
||||||
|
"@codemirror/lang-vue": "^0.1.3",
|
||||||
"@codemirror/lang-xml": "^6.0.2",
|
"@codemirror/lang-xml": "^6.0.2",
|
||||||
"@codemirror/language": "^6.9.3",
|
"@codemirror/language": "^6.11.0",
|
||||||
"@codemirror/legacy-modes": "^6.3.3",
|
"@codemirror/legacy-modes": "^6.5.1",
|
||||||
"@codemirror/lint": "^6.4.2",
|
"@codemirror/lint": "^6.4.2",
|
||||||
"@codemirror/search": "^6.5.5",
|
"@codemirror/search": "^6.5.10",
|
||||||
"@codemirror/state": "^6.3.3",
|
"@codemirror/state": "^6.5.2",
|
||||||
"@codemirror/view": "^6.22.2",
|
"@codemirror/view": "^6.36.5",
|
||||||
"@electron/asar": "^3.2.2",
|
"@electron/asar": "^3.2.2",
|
||||||
"@lezer/generator": "^1.5.1",
|
"@lezer/generator": "^1.5.1",
|
||||||
"@lezer/markdown": "^1.1.2",
|
"@lezer/markdown": "^1.1.2",
|
||||||
"@playwright/test": "^1.40.1",
|
"@playwright/test": "^1.51.1",
|
||||||
"@replit/codemirror-lang-csharp": "^6.2.0",
|
"@replit/codemirror-lang-csharp": "^6.2.0",
|
||||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||||
"@types/node": "^20.10.5",
|
"@types/node": "^20.10.5",
|
||||||
"@vitejs/plugin-vue": "^4.0.0",
|
"@vitejs/plugin-vue": "^5.2.3",
|
||||||
|
"codemirror-lang-elixir": "^4.0.0",
|
||||||
"debounce": "^1.2.1",
|
"debounce": "^1.2.1",
|
||||||
"electron": "^28.0.0",
|
"electron": "^35.2.0",
|
||||||
"electron-builder": "^23.6.0",
|
"electron-builder": "^26.0.12",
|
||||||
"electron-builder-notarize": "^1.5.1",
|
|
||||||
"electron-store": "^8.1.0",
|
"electron-store": "^8.1.0",
|
||||||
"electron-updater": "^6.1.7",
|
"electron-updater": "^6.6.2",
|
||||||
"fs-jetpack": "^5.1.0",
|
"fs-jetpack": "^5.1.0",
|
||||||
"prettier": "^3.1.1",
|
"lezer-elixir": "^1.1.2",
|
||||||
|
"prettier": "^3.3.2",
|
||||||
|
"primevue": "^4.3.3",
|
||||||
"rollup-plugin-license": "^3.0.1",
|
"rollup-plugin-license": "^3.0.1",
|
||||||
"sass": "^1.57.1",
|
"sass-embedded": "^1.87.0",
|
||||||
"typescript": "^4.9.4",
|
"typescript": "^4.9.4",
|
||||||
"vite": "^4.5.2",
|
"vite": "^6.3.2",
|
||||||
"vite-plugin-electron": "^0.11.1",
|
"vite-plugin-electron": "^0.11.1",
|
||||||
"vite-plugin-electron-renderer": "^0.11.4",
|
"vite-plugin-electron-renderer": "^0.11.4",
|
||||||
"vue": "^3.2.45",
|
"vue": "^3.5.13",
|
||||||
"vue-tsc": "^1.0.16"
|
"vue-tsc": "^1.0.16",
|
||||||
|
"vuedraggable": "^4.1.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"electron-log": "^5.0.1"
|
"@sindresorhus/slugify": "^2.2.1",
|
||||||
|
"electron-log": "^5.0.1",
|
||||||
|
"fuzzysort": "^3.0.2",
|
||||||
|
"pinia": "^2.1.7",
|
||||||
|
"semver": "^7.6.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { defineConfig, devices } from '@playwright/test';
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
process.env["HEYNOTE_TESTS"] = "1"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read environment variables from file.
|
* Read environment variables from file.
|
||||||
* https://github.com/motdotla/dotenv
|
* https://github.com/motdotla/dotenv
|
||||||
@ -18,7 +20,8 @@ export default defineConfig({
|
|||||||
/* Retry on CI only */
|
/* Retry on CI only */
|
||||||
retries: process.env.CI ? 2 : 0,
|
retries: process.env.CI ? 2 : 0,
|
||||||
/* Opt out of parallel tests on CI. */
|
/* Opt out of parallel tests on CI. */
|
||||||
workers: process.env.CI ? 1 : undefined,
|
//workers: process.env.CI ? 1 : undefined,
|
||||||
|
workers: process.env.CI ? undefined : undefined,
|
||||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
reporter: process.env.CI ? [['github'], ['html']] : 'list',
|
reporter: process.env.CI ? [['github'], ['html']] : 'list',
|
||||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
importScripts("guesslang.min.js")
|
importScripts("guesslang.min.js")
|
||||||
|
|
||||||
GUESSLANG_LANGUAGES = ["json","py","html","sql","md","java","php","css","xml","cpp","rs","cs","rb","sh","yaml","toml","go","clj","erl","js","ts","swift","kt","groovy","ps1"]
|
GUESSLANG_LANGUAGES = ["json","py","html","sql","md","java","php","css","xml","cpp","rs","cs","rb","sh","yaml","toml","go","clj","ex","erl","js","ts","swift","kt","groovy","ps1","dart","scala"]
|
||||||
|
|
||||||
const guessLang = new self.GuessLang()
|
const guessLang = new self.GuessLang()
|
||||||
|
|
||||||
@ -28,6 +28,7 @@ onmessage = (event) => {
|
|||||||
},
|
},
|
||||||
content: content,
|
content: content,
|
||||||
idx: event.data.idx,
|
idx: event.data.idx,
|
||||||
|
path: event.data.path,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -53,6 +54,7 @@ onmessage = (event) => {
|
|||||||
},
|
},
|
||||||
content: content,
|
content: content,
|
||||||
idx: event.data.idx,
|
idx: event.data.idx,
|
||||||
|
path: event.data.path,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -66,6 +68,7 @@ onmessage = (event) => {
|
|||||||
},
|
},
|
||||||
content: content,
|
content: content,
|
||||||
idx: event.data.idx,
|
idx: event.data.idx,
|
||||||
|
path: event.data.path,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
11
public/site.webmanifest
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "Heynote",
|
||||||
|
"short_name": "Heynote",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/icon.ico",
|
||||||
|
"sizes": "256x256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"display": "standalone"
|
||||||
|
}
|
@ -9,6 +9,9 @@ export const keyHelpStr = (platform: string) => {
|
|||||||
[`${altChar} + Shift + Enter`, "Add new block at the start of the buffer"],
|
[`${altChar} + Shift + Enter`, "Add new block at the start of the buffer"],
|
||||||
[`${modChar} + ${altChar} + Enter`, "Split the current block at cursor position"],
|
[`${modChar} + ${altChar} + Enter`, "Split the current block at cursor position"],
|
||||||
[`${modChar} + L`, "Change block language"],
|
[`${modChar} + L`, "Change block language"],
|
||||||
|
[`${modChar} + N`, "Create a new note buffer"],
|
||||||
|
[`${modChar} + S`, "Move the current block to another (or new) buffer"],
|
||||||
|
[`${modChar} + P`, "Open note selector"],
|
||||||
[`${modChar} + Down`, "Goto next block"],
|
[`${modChar} + Down`, "Goto next block"],
|
||||||
[`${modChar} + Up`, "Goto previous block"],
|
[`${modChar} + Up`, "Goto previous block"],
|
||||||
[`${modChar} + A`, "Select all text in a note block. Press again to select the whole buffer"],
|
[`${modChar} + A`, "Select all text in a note block. Press again to select the whole buffer"],
|
||||||
|
@ -1,6 +1,15 @@
|
|||||||
|
export const SCRATCH_FILE_NAME = "scratch.txt"
|
||||||
|
export const AUTO_SAVE_INTERVAL = 2000
|
||||||
|
|
||||||
export const WINDOW_CLOSE_EVENT = "window-close"
|
export const WINDOW_CLOSE_EVENT = "window-close"
|
||||||
export const OPEN_SETTINGS_EVENT = "open-settings"
|
export const OPEN_SETTINGS_EVENT = "open-settings"
|
||||||
export const SETTINGS_CHANGE_EVENT = "settings-change"
|
export const SETTINGS_CHANGE_EVENT = "settings-change"
|
||||||
|
export const REDO_EVENT = "redo"
|
||||||
|
export const UNDO_EVENT = "undo"
|
||||||
|
export const MOVE_BLOCK_EVENT = "move-block"
|
||||||
|
export const DELETE_BLOCK_EVENT = "delete-block"
|
||||||
|
export const CHANGE_BUFFER_EVENT = "change-buffer"
|
||||||
|
export const SELECT_ALL_EVENT = "select-all"
|
||||||
|
|
||||||
export const UPDATE_AVAILABLE_EVENT = "update-available"
|
export const UPDATE_AVAILABLE_EVENT = "update-available"
|
||||||
export const UPDATE_NOT_AVAILABLE_EVENT = "update-not-available"
|
export const UPDATE_NOT_AVAILABLE_EVENT = "update-not-available"
|
45
src/common/note-format.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { major } from "semver";
|
||||||
|
|
||||||
|
|
||||||
|
const FORMAT_VERSION = "1.0.0"
|
||||||
|
|
||||||
|
|
||||||
|
export class NoteFormat {
|
||||||
|
constructor() {
|
||||||
|
this.content = '';
|
||||||
|
this.metadata = {formatVersion: "0.0.0"};
|
||||||
|
}
|
||||||
|
|
||||||
|
static load(data) {
|
||||||
|
const note = new NoteFormat();
|
||||||
|
|
||||||
|
note.content = data
|
||||||
|
const firstSeparator = data.indexOf("\n∞∞∞")
|
||||||
|
if (firstSeparator !== -1) {
|
||||||
|
const metadataContent = data.slice(0, firstSeparator).trim()
|
||||||
|
if (metadataContent !== "") {
|
||||||
|
note.metadata = JSON.parse(metadataContent)
|
||||||
|
}
|
||||||
|
note.content = data.slice(firstSeparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (major(note.metadata.formatVersion) > major(FORMAT_VERSION)) {
|
||||||
|
throw new Error(`Unsupported Heynote format version: ${note.metadata.formatVersion}. You probably need to update Heynote.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return note
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize() {
|
||||||
|
this.metadata.formatVersion = FORMAT_VERSION
|
||||||
|
return JSON.stringify(this.metadata) + this.content
|
||||||
|
}
|
||||||
|
|
||||||
|
set cursors(cursors) {
|
||||||
|
this.metadata.cursors = cursors
|
||||||
|
}
|
||||||
|
|
||||||
|
get cursors() {
|
||||||
|
return this.metadata.cursors
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,22 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { mapState, mapActions } from 'pinia'
|
||||||
|
|
||||||
|
import { mapWritableState, mapStores } from 'pinia'
|
||||||
|
import { useHeynoteStore } from "../stores/heynote-store"
|
||||||
|
import { useErrorStore } from "../stores/error-store"
|
||||||
|
import { useSettingsStore } from "../stores/settings-store"
|
||||||
|
import { useEditorCacheStore } from '../stores/editor-cache'
|
||||||
|
|
||||||
|
import { OPEN_SETTINGS_EVENT, MOVE_BLOCK_EVENT, CHANGE_BUFFER_EVENT } from '@/src/common/constants'
|
||||||
|
|
||||||
import StatusBar from './StatusBar.vue'
|
import StatusBar from './StatusBar.vue'
|
||||||
import Editor from './Editor.vue'
|
import Editor from './Editor.vue'
|
||||||
import LanguageSelector from './LanguageSelector.vue'
|
import LanguageSelector from './LanguageSelector.vue'
|
||||||
|
import BufferSelector from './BufferSelector.vue'
|
||||||
import Settings from './settings/Settings.vue'
|
import Settings from './settings/Settings.vue'
|
||||||
|
import ErrorMessages from './ErrorMessages.vue'
|
||||||
|
import NewBuffer from './NewBuffer.vue'
|
||||||
|
import EditBuffer from './EditBuffer.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -10,99 +24,128 @@
|
|||||||
StatusBar,
|
StatusBar,
|
||||||
LanguageSelector,
|
LanguageSelector,
|
||||||
Settings,
|
Settings,
|
||||||
|
BufferSelector,
|
||||||
|
ErrorMessages,
|
||||||
|
NewBuffer,
|
||||||
|
EditBuffer,
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
line: 1,
|
|
||||||
column: 1,
|
|
||||||
selectionSize: 0,
|
|
||||||
language: "plaintext",
|
|
||||||
languageAuto: true,
|
|
||||||
theme: window.heynote.themeMode.initial,
|
|
||||||
initialTheme: window.heynote.themeMode.initial,
|
|
||||||
themeSetting: 'system',
|
|
||||||
development: window.location.href.indexOf("dev=1") !== -1,
|
development: window.location.href.indexOf("dev=1") !== -1,
|
||||||
showLanguageSelector: false,
|
|
||||||
showSettings: false,
|
showSettings: false,
|
||||||
settings: window.heynote.settings,
|
settings: window.heynote.settings,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
window.heynote.themeMode.get().then((mode) => {
|
this.settingsStore.setUp()
|
||||||
this.theme = mode.computed
|
|
||||||
this.themeSetting = mode.theme
|
window.heynote.mainProcess.on(OPEN_SETTINGS_EVENT, () => {
|
||||||
})
|
|
||||||
const onThemeChange = (theme) => {
|
|
||||||
this.theme = theme
|
|
||||||
if (theme === "system") {
|
|
||||||
document.documentElement.setAttribute("theme", window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")
|
|
||||||
} else {
|
|
||||||
document.documentElement.setAttribute("theme", theme)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onThemeChange(window.heynote.themeMode.initial)
|
|
||||||
window.heynote.themeMode.onChange(onThemeChange)
|
|
||||||
window.heynote.onSettingsChange((settings) => {
|
|
||||||
this.settings = settings
|
|
||||||
})
|
|
||||||
window.heynote.onOpenSettings(() => {
|
|
||||||
this.showSettings = true
|
this.showSettings = true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
window.heynote.mainProcess.on(MOVE_BLOCK_EVENT, (path) => {
|
||||||
|
this.openMoveToBufferSelector()
|
||||||
|
})
|
||||||
|
|
||||||
|
window.heynote.mainProcess.on(CHANGE_BUFFER_EVENT, () => {
|
||||||
|
this.openBufferSelector()
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
window.heynote.themeMode.removeListener()
|
this.settingsStore.tearDown()
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
// when a dialog is closed, we want to focus the editor
|
||||||
|
showLanguageSelector(value) { this.dialogWatcher(value) },
|
||||||
|
showBufferSelector(value) { this.dialogWatcher(value) },
|
||||||
|
showCreateBuffer(value) { this.dialogWatcher(value) },
|
||||||
|
showEditBuffer(value) { this.dialogWatcher(value) },
|
||||||
|
showMoveToBufferSelector(value) { this.dialogWatcher(value) },
|
||||||
|
showCommandPalette(value) { this.dialogWatcher(value) },
|
||||||
|
|
||||||
|
currentBufferPath() {
|
||||||
|
this.focusEditor()
|
||||||
|
},
|
||||||
|
|
||||||
|
currentBufferName() {
|
||||||
|
window.heynote.setWindowTitle(this.currentBufferName)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapStores(useSettingsStore, useEditorCacheStore),
|
||||||
|
...mapState(useHeynoteStore, [
|
||||||
|
"currentBufferPath",
|
||||||
|
"currentBufferName",
|
||||||
|
"showLanguageSelector",
|
||||||
|
"showBufferSelector",
|
||||||
|
"showCreateBuffer",
|
||||||
|
"showEditBuffer",
|
||||||
|
"showMoveToBufferSelector",
|
||||||
|
"showCommandPalette",
|
||||||
|
]),
|
||||||
|
|
||||||
|
dialogVisible() {
|
||||||
|
return this.showLanguageSelector || this.showBufferSelector || this.showCreateBuffer || this.showEditBuffer || this.showMoveToBufferSelector || this.showCommandPalette || this.showSettings
|
||||||
|
},
|
||||||
|
|
||||||
|
editorInert() {
|
||||||
|
return this.dialogVisible
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
...mapActions(useHeynoteStore, [
|
||||||
|
"openLanguageSelector",
|
||||||
|
"openBufferSelector",
|
||||||
|
"openCreateBuffer",
|
||||||
|
"closeDialog",
|
||||||
|
"closeBufferSelector",
|
||||||
|
"openBuffer",
|
||||||
|
"closeMoveToBufferSelector",
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Used as a watcher for the booleans that control the visibility of editor dialogs.
|
||||||
|
// When a dialog is closed, we want to focus the editor
|
||||||
|
dialogWatcher(value) {
|
||||||
|
if (!value) {
|
||||||
|
this.focusEditor()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
focusEditor() {
|
||||||
|
// we need to wait for the next tick for the cases when we set the inert attribute on the editor
|
||||||
|
// in which case issuing a focus() call immediately would not work
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.editor.focus()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
openSettings() {
|
openSettings() {
|
||||||
this.showSettings = true
|
this.showSettings = true
|
||||||
},
|
},
|
||||||
closeSettings() {
|
closeSettings() {
|
||||||
this.showSettings = false
|
this.showSettings = false
|
||||||
this.$refs.editor.focus()
|
this.focusEditor()
|
||||||
},
|
|
||||||
|
|
||||||
toggleTheme() {
|
|
||||||
let newTheme
|
|
||||||
// when the "system" theme is used, make sure that the first click always results in amn actual theme change
|
|
||||||
if (this.initialTheme === "light") {
|
|
||||||
newTheme = this.themeSetting === "system" ? "dark" : (this.themeSetting === "dark" ? "light" : "system")
|
|
||||||
} else {
|
|
||||||
newTheme = this.themeSetting === "system" ? "light" : (this.themeSetting === "light" ? "dark" : "system")
|
|
||||||
}
|
|
||||||
window.heynote.themeMode.set(newTheme)
|
|
||||||
this.themeSetting = newTheme
|
|
||||||
this.$refs.editor.focus()
|
|
||||||
},
|
|
||||||
|
|
||||||
onCursorChange(e) {
|
|
||||||
this.line = e.cursorLine.line
|
|
||||||
this.column = e.cursorLine.col
|
|
||||||
this.selectionSize = e.selectionSize
|
|
||||||
this.language = e.language
|
|
||||||
this.languageAuto = e.languageAuto
|
|
||||||
},
|
|
||||||
|
|
||||||
openLanguageSelector() {
|
|
||||||
this.showLanguageSelector = true
|
|
||||||
},
|
|
||||||
|
|
||||||
closeLanguageSelector() {
|
|
||||||
this.showLanguageSelector = false
|
|
||||||
this.$refs.editor.focus()
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onSelectLanguage(language) {
|
onSelectLanguage(language) {
|
||||||
this.showLanguageSelector = false
|
this.closeDialog()
|
||||||
this.$refs.editor.setLanguage(language)
|
this.$refs.editor.setLanguage(language)
|
||||||
},
|
},
|
||||||
|
|
||||||
formatCurrentBlock() {
|
formatCurrentBlock() {
|
||||||
this.$refs.editor.formatCurrentBlock()
|
this.$refs.editor.formatCurrentBlock()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onMoveCurrentBlockToOtherEditor(path) {
|
||||||
|
this.editorCacheStore.moveCurrentBlockToOtherEditor(path)
|
||||||
|
this.closeMoveToBufferSelector()
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,48 +154,61 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<Editor
|
<Editor
|
||||||
@cursorChange="onCursorChange"
|
:theme="settingsStore.theme"
|
||||||
:theme="theme"
|
|
||||||
:development="development"
|
:development="development"
|
||||||
:debugSyntaxTree="false"
|
:debugSyntaxTree="false"
|
||||||
:keymap="settings.keymap"
|
:inert="editorInert"
|
||||||
:emacsMetaKey="settings.emacsMetaKey"
|
|
||||||
:showLineNumberGutter="settings.showLineNumberGutter"
|
|
||||||
:showFoldGutter="settings.showFoldGutter"
|
|
||||||
:bracketClosing="settings.bracketClosing"
|
|
||||||
:fontFamily="settings.fontFamily"
|
|
||||||
:fontSize="settings.fontSize"
|
|
||||||
class="editor"
|
class="editor"
|
||||||
ref="editor"
|
ref="editor"
|
||||||
@openLanguageSelector="openLanguageSelector"
|
|
||||||
/>
|
/>
|
||||||
<StatusBar
|
<StatusBar
|
||||||
:line="line"
|
|
||||||
:column="column"
|
|
||||||
:selectionSize="selectionSize"
|
|
||||||
:language="language"
|
|
||||||
:languageAuto="languageAuto"
|
|
||||||
:theme="theme"
|
|
||||||
:themeSetting="themeSetting"
|
|
||||||
:autoUpdate="settings.autoUpdate"
|
:autoUpdate="settings.autoUpdate"
|
||||||
:allowBetaVersions="settings.allowBetaVersions"
|
:allowBetaVersions="settings.allowBetaVersions"
|
||||||
@toggleTheme="toggleTheme"
|
@openBufferSelector="openBufferSelector"
|
||||||
@openLanguageSelector="openLanguageSelector"
|
@openLanguageSelector="openLanguageSelector"
|
||||||
@formatCurrentBlock="formatCurrentBlock"
|
@formatCurrentBlock="formatCurrentBlock"
|
||||||
@openSettings="showSettings = true"
|
@openSettings="showSettings = true"
|
||||||
|
@click="() => {$refs.editor.focus()}"
|
||||||
class="status"
|
class="status"
|
||||||
/>
|
/>
|
||||||
<div class="overlay">
|
<div class="overlay">
|
||||||
<LanguageSelector
|
<LanguageSelector
|
||||||
v-if="showLanguageSelector"
|
v-if="showLanguageSelector"
|
||||||
@selectLanguage="onSelectLanguage"
|
@selectLanguage="onSelectLanguage"
|
||||||
@close="closeLanguageSelector"
|
@close="closeDialog"
|
||||||
|
/>
|
||||||
|
<BufferSelector
|
||||||
|
v-if="showBufferSelector || showCommandPalette"
|
||||||
|
:initialFilter="showCommandPalette ? '>' : ''"
|
||||||
|
:commandsEnabled="true"
|
||||||
|
@openBuffer="openBuffer"
|
||||||
|
@openCreateBuffer="(nameSuggestion) => openCreateBuffer('new', nameSuggestion)"
|
||||||
|
@close="closeBufferSelector"
|
||||||
|
/>
|
||||||
|
<BufferSelector
|
||||||
|
v-if="showMoveToBufferSelector"
|
||||||
|
headline="Move block to..."
|
||||||
|
:commandsEnabled="false"
|
||||||
|
@openBuffer="onMoveCurrentBlockToOtherEditor"
|
||||||
|
@openCreateBuffer="(nameSuggestion) => openCreateBuffer('currentBlock', nameSuggestion)"
|
||||||
|
@close="closeMoveToBufferSelector"
|
||||||
/>
|
/>
|
||||||
<Settings
|
<Settings
|
||||||
v-if="showSettings"
|
v-if="showSettings"
|
||||||
:initialSettings="settings"
|
:initialSettings="settingsStore.settings"
|
||||||
|
:themeSetting="settingsStore.themeSetting"
|
||||||
@closeSettings="closeSettings"
|
@closeSettings="closeSettings"
|
||||||
|
@setTheme="settingsStore.setTheme"
|
||||||
/>
|
/>
|
||||||
|
<NewBuffer
|
||||||
|
v-if="showCreateBuffer"
|
||||||
|
@close="closeDialog"
|
||||||
|
/>
|
||||||
|
<EditBuffer
|
||||||
|
v-if="showEditBuffer"
|
||||||
|
@close="closeDialog"
|
||||||
|
/>
|
||||||
|
<ErrorMessages />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
531
src/components/BufferSelector.vue
Normal file
@ -0,0 +1,531 @@
|
|||||||
|
<script>
|
||||||
|
import fuzzysort from 'fuzzysort'
|
||||||
|
|
||||||
|
import { mapState, mapActions } from 'pinia'
|
||||||
|
import { SCRATCH_FILE_NAME } from "../common/constants"
|
||||||
|
import { useHeynoteStore } from "../stores/heynote-store"
|
||||||
|
import { HEYNOTE_COMMANDS } from '../editor/commands'
|
||||||
|
|
||||||
|
const pathSep = window.heynote.buffer.pathSeparator
|
||||||
|
|
||||||
|
function escapeHTML(str) {
|
||||||
|
return str
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
headline: String,
|
||||||
|
initialFilter: String,
|
||||||
|
commandsEnabled: Boolean,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selected: 0,
|
||||||
|
actionButton: 0,
|
||||||
|
filter: this.initialFilter || "",
|
||||||
|
items: [],
|
||||||
|
SCRATCH_FILE_NAME: SCRATCH_FILE_NAME,
|
||||||
|
deleteConfirm: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
await this.updateBuffers()
|
||||||
|
this.$refs.container.focus()
|
||||||
|
this.$refs.input.focus()
|
||||||
|
this.buildItems()
|
||||||
|
if (this.items.length > 1) {
|
||||||
|
this.selected = 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapState(useHeynoteStore, [
|
||||||
|
"buffers",
|
||||||
|
"recentBufferPaths",
|
||||||
|
]),
|
||||||
|
|
||||||
|
commands() {
|
||||||
|
const commands = Object.entries(HEYNOTE_COMMANDS)
|
||||||
|
// sort array first by category, then by description
|
||||||
|
commands.sort((a, b) => {
|
||||||
|
const aCategory = a[1].category || ""
|
||||||
|
const bCategory = b[1].category || ""
|
||||||
|
if (aCategory === bCategory) {
|
||||||
|
return a[1].description.localeCompare(b[1].description)
|
||||||
|
} else {
|
||||||
|
return aCategory.localeCompare(bCategory)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return commands.map(([cmdKey, cmd]) => ({
|
||||||
|
name: `${cmd.category}: ${cmd.description}`,
|
||||||
|
cmd: cmdKey,
|
||||||
|
isCommand: true,
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
|
||||||
|
orderedItems() {
|
||||||
|
const sortKeys = Object.fromEntries(this.recentBufferPaths.map((item, idx) => [item, idx]))
|
||||||
|
const getSortScore = (item) => sortKeys[item.path] !== undefined ? sortKeys[item.path] : 1000
|
||||||
|
const compareFunc = (a, b) => {
|
||||||
|
const sortScore = getSortScore(a) - getSortScore(b)
|
||||||
|
if (sortScore !== 0) {
|
||||||
|
// sort by recency first
|
||||||
|
return sortScore
|
||||||
|
} else {
|
||||||
|
// then notes in root
|
||||||
|
const aIsRoot = a.path.indexOf(pathSep) === -1
|
||||||
|
const bIsRoot = b.path.indexOf(pathSep) === -1
|
||||||
|
if (aIsRoot && !bIsRoot) {
|
||||||
|
return -1
|
||||||
|
} else if (!aIsRoot && bIsRoot) {
|
||||||
|
return 1
|
||||||
|
} else {
|
||||||
|
// last sort by path
|
||||||
|
return a.path.localeCompare(b.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...this.items].sort(compareFunc)
|
||||||
|
},
|
||||||
|
|
||||||
|
filteredItems() {
|
||||||
|
if (this.commandsEnabled && this.filter.startsWith(">")) {
|
||||||
|
// command mode if the first character is ">"
|
||||||
|
if (this.filter.length < 2) {
|
||||||
|
return this.commands
|
||||||
|
}
|
||||||
|
const searchResults = fuzzysort.go(this.filter.slice(1), this.commands, {
|
||||||
|
keys: ["name"],
|
||||||
|
})
|
||||||
|
return searchResults.map((result) => {
|
||||||
|
const obj = {...result.obj}
|
||||||
|
const nameHighlight = result[0].highlight("<b>", "</b>")
|
||||||
|
obj.name = nameHighlight !== "" ? nameHighlight : obj.name
|
||||||
|
return obj
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
let items
|
||||||
|
if (this.filter === "") {
|
||||||
|
items = this.orderedItems
|
||||||
|
} else {
|
||||||
|
const searchResults = fuzzysort.go(this.filter, this.items, {
|
||||||
|
keys: ["name", "folder"],
|
||||||
|
})
|
||||||
|
items = searchResults.map((result) => {
|
||||||
|
const obj = {...result.obj}
|
||||||
|
const nameHighlight = result[0].highlight("<b>", "</b>")
|
||||||
|
const folderHighlight = result[1].highlight("<b>", "</b>")
|
||||||
|
obj.name = nameHighlight !== "" ? nameHighlight : obj.name
|
||||||
|
obj.folder = folderHighlight !== "" ? folderHighlight : obj.folder
|
||||||
|
return obj
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const newNoteItem = {
|
||||||
|
name: "Create new…",
|
||||||
|
createNew:true,
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
...items,
|
||||||
|
newNoteItem,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
...mapActions(useHeynoteStore, [
|
||||||
|
"updateBuffers",
|
||||||
|
"editBufferMetadata",
|
||||||
|
"deleteBuffer",
|
||||||
|
"executeCommand",
|
||||||
|
]),
|
||||||
|
|
||||||
|
buildItems() {
|
||||||
|
//console.log("buildItems", Object.entries(this.buffers))
|
||||||
|
this.items = Object.entries(this.buffers).map(([path, metadata]) => {
|
||||||
|
return {
|
||||||
|
"path": path,
|
||||||
|
"name": escapeHTML(metadata?.name || path),
|
||||||
|
"folder": escapeHTML(path.split(pathSep).slice(0, -1).join(pathSep)),
|
||||||
|
"scratch": path === SCRATCH_FILE_NAME,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
onKeydown(event) {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
event.preventDefault()
|
||||||
|
if (this.actionButton !== 0) {
|
||||||
|
this.hideActionButtons()
|
||||||
|
} else {
|
||||||
|
this.$emit("close")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.filteredItems.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = this.filteredItems[this.selected]
|
||||||
|
if (event.key === "ArrowDown") {
|
||||||
|
if (this.selected === this.filteredItems.length - 1) {
|
||||||
|
this.selected = 0
|
||||||
|
} else {
|
||||||
|
this.selected = Math.min(this.selected + 1, this.filteredItems.length - 1)
|
||||||
|
}
|
||||||
|
event.preventDefault()
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.container.querySelector(".selected").scrollIntoView({block: "nearest"})
|
||||||
|
})
|
||||||
|
this.actionButton = 0
|
||||||
|
} else if (event.key === "ArrowUp") {
|
||||||
|
if (this.selected === 0) {
|
||||||
|
this.selected = this.filteredItems.length - 1
|
||||||
|
} else {
|
||||||
|
this.selected = Math.max(this.selected - 1, 0)
|
||||||
|
}
|
||||||
|
event.preventDefault()
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.container.querySelector(".selected").scrollIntoView({block: "nearest"})
|
||||||
|
})
|
||||||
|
this.actionButton = 0
|
||||||
|
} else if (event.key === "ArrowRight" && this.itemHasActionButtons(item)) {
|
||||||
|
event.preventDefault()
|
||||||
|
this.actionButton = Math.min(2, this.actionButton + 1)
|
||||||
|
} else if (event.key === "ArrowLeft" && this.itemHasActionButtons(item)) {
|
||||||
|
event.preventDefault()
|
||||||
|
this.actionButton = Math.max(0, this.actionButton - 1)
|
||||||
|
this.deleteConfirm = false
|
||||||
|
} else if (event.key === "Enter") {
|
||||||
|
event.preventDefault()
|
||||||
|
if (this.actionButton === 1) {
|
||||||
|
//console.log("edit file:", path)
|
||||||
|
this.editBufferMetadata(item.path)
|
||||||
|
} else if (this.actionButton === 2) {
|
||||||
|
this.deleteConfirmNote(item.path)
|
||||||
|
} else {
|
||||||
|
this.selectItem(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
selectItem(item) {
|
||||||
|
if (item.createNew) {
|
||||||
|
if (this.filteredItems.length === 1) {
|
||||||
|
this.$emit("openCreateBuffer", this.filter)
|
||||||
|
} else {
|
||||||
|
this.$emit("openCreateBuffer", "")
|
||||||
|
}
|
||||||
|
} else if (item.isCommand) {
|
||||||
|
this.$emit("close")
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.executeCommand(item.cmd)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$emit("openBuffer", item.path)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
itemHasActionButtons(item) {
|
||||||
|
return !item.createNew && item.path !== SCRATCH_FILE_NAME && !item.isCommand
|
||||||
|
},
|
||||||
|
|
||||||
|
onInput(event) {
|
||||||
|
// reset selection
|
||||||
|
this.selected = 0
|
||||||
|
},
|
||||||
|
|
||||||
|
onFocusOut(event) {
|
||||||
|
let container = this.$refs.container
|
||||||
|
if (container !== event.relatedTarget && !container.contains(event.relatedTarget)) {
|
||||||
|
this.$emit("close")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getItemClass(item, idx) {
|
||||||
|
return {
|
||||||
|
"item": true,
|
||||||
|
"selected": idx === this.selected,
|
||||||
|
"action-buttons-visible": this.actionButton > 0,
|
||||||
|
"scratch": item.scratch,
|
||||||
|
"new-note": item.createNew,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
showActionButtons(idx) {
|
||||||
|
this.selected = idx
|
||||||
|
this.actionButton = 1
|
||||||
|
this.deleteConfirm = false
|
||||||
|
this.$refs.input.focus()
|
||||||
|
},
|
||||||
|
|
||||||
|
hideActionButtons() {
|
||||||
|
this.actionButton = 0
|
||||||
|
this.deleteConfirm = false
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteConfirmNote(path) {
|
||||||
|
if (this.deleteConfirm) {
|
||||||
|
//console.log("delete file:", path)
|
||||||
|
await this.deleteBuffer(path)
|
||||||
|
this.hideActionButtons()
|
||||||
|
this.buildItems()
|
||||||
|
this.selected = Math.min(this.selected, this.items.length - 1)
|
||||||
|
} else {
|
||||||
|
this.deleteConfirm = true
|
||||||
|
this.actionButton = 2
|
||||||
|
this.$refs.input.focus()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form class="note-selector" tabindex="-1" @focusout="onFocusOut" ref="container" @submit.prevent>
|
||||||
|
<div class="input-container">
|
||||||
|
<h1 v-if="headline">{{headline}}</h1>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
ref="input"
|
||||||
|
@keydown="onKeydown"
|
||||||
|
@input="onInput"
|
||||||
|
v-model="filter"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="scroller">
|
||||||
|
<ul class="items" ref="itemsContainer">
|
||||||
|
<template
|
||||||
|
v-for="item, idx in filteredItems"
|
||||||
|
:key="item.path"
|
||||||
|
>
|
||||||
|
<li v-if="item.createNew" class="line-separator"></li>
|
||||||
|
<li
|
||||||
|
:class="getItemClass(item, idx)"
|
||||||
|
@click="selectItem(item)"
|
||||||
|
ref="item"
|
||||||
|
>
|
||||||
|
<span class="name" v-html="item.name" />
|
||||||
|
<span class="path" v-html="item.folder" />
|
||||||
|
<span :class="{'action-buttons':true, 'visible':actionButton > 0 && idx === selected}">
|
||||||
|
<button
|
||||||
|
v-if="actionButton > 0 && idx === selected"
|
||||||
|
:class="{'selected':actionButton === 1}"
|
||||||
|
@click.stop.prevent="editBufferMetadata(item.path)"
|
||||||
|
>Edit</button>
|
||||||
|
<button
|
||||||
|
v-if="actionButton > 0 && idx === selected"
|
||||||
|
:class="{'delete':true, 'selected':actionButton === 2, 'confirm':deleteConfirm}"
|
||||||
|
@click.stop.prevent="deleteConfirmNote(item.path)"
|
||||||
|
>
|
||||||
|
<template v-if="deleteConfirm">
|
||||||
|
Really Delete?
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
Delete
|
||||||
|
</template>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="show-actions"
|
||||||
|
v-if="itemHasActionButtons(item) && (actionButton === 0 || idx !== selected)"
|
||||||
|
@click.stop.prevent="showActionButtons(idx)"
|
||||||
|
></button>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="sass">
|
||||||
|
.note-selector
|
||||||
|
font-size: 13px
|
||||||
|
background: #efefef
|
||||||
|
position: absolute
|
||||||
|
top: 0
|
||||||
|
left: 50%
|
||||||
|
width: 440px
|
||||||
|
transform: translateX(-50%)
|
||||||
|
max-height: 100%
|
||||||
|
box-sizing: border-box
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
border-radius: 0 0 5px 5px
|
||||||
|
box-shadow: 0 0 10px rgba(0,0,0,0.3)
|
||||||
|
+dark-mode
|
||||||
|
background: #151516
|
||||||
|
box-shadow: 0 0 10px rgba(0,0,0,0.5)
|
||||||
|
color: rgba(255,255,255, 0.7)
|
||||||
|
+webapp-mobile
|
||||||
|
max-width: calc(100% - 80px)
|
||||||
|
|
||||||
|
.input-container
|
||||||
|
padding: 10px
|
||||||
|
h1
|
||||||
|
font-weight: bold
|
||||||
|
margin-bottom: 14px
|
||||||
|
input
|
||||||
|
background: #fff
|
||||||
|
padding: 4px 5px
|
||||||
|
border: 1px solid #ccc
|
||||||
|
box-sizing: border-box
|
||||||
|
border-radius: 2px
|
||||||
|
width: 100%
|
||||||
|
&:focus
|
||||||
|
outline: none
|
||||||
|
border: 1px solid #fff
|
||||||
|
outline: 2px solid #48b57e
|
||||||
|
+dark-mode
|
||||||
|
background: #3b3b3b
|
||||||
|
color: rgba(255,255,255, 0.9)
|
||||||
|
border: 1px solid #5a5a5a
|
||||||
|
&:focus
|
||||||
|
border: 1px solid #3b3b3b
|
||||||
|
+webapp-mobile
|
||||||
|
font-size: 16px
|
||||||
|
max-width: 100%
|
||||||
|
|
||||||
|
.scroller
|
||||||
|
overflow-y: auto
|
||||||
|
padding: 0 10px 5px 10px
|
||||||
|
|
||||||
|
.items
|
||||||
|
> li.line-separator
|
||||||
|
height: 1px
|
||||||
|
background: rgba(0,0,0, 0.05)
|
||||||
|
margin-left: -10px
|
||||||
|
margin-right: -10px
|
||||||
|
margin-top: 3px
|
||||||
|
margin-bottom: 3px
|
||||||
|
+dark-mode
|
||||||
|
background: rgba(255,255,255, 0.1)
|
||||||
|
> li.item
|
||||||
|
position: relative
|
||||||
|
border-radius: 3px
|
||||||
|
padding: 3px 12px
|
||||||
|
line-height: 18px
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
scroll-margin-top: 6px
|
||||||
|
scroll-margin-bottom: 6px
|
||||||
|
&:hover
|
||||||
|
background: #e2e2e2
|
||||||
|
.action-buttons .show-actions
|
||||||
|
display: inline-block
|
||||||
|
background-image: url(@/assets/icons/arrow-right-black.svg)
|
||||||
|
&.selected .action-buttons .show-actions
|
||||||
|
background-image: url(@/assets/icons/arrow-right-white.svg)
|
||||||
|
+dark-mode
|
||||||
|
color: rgba(255,255,255, 0.65)
|
||||||
|
&:hover
|
||||||
|
background: #29292a
|
||||||
|
.action-buttons button
|
||||||
|
color: #fff
|
||||||
|
&.selected
|
||||||
|
background: #48b57e
|
||||||
|
color: #fff
|
||||||
|
&.action-buttons-visible
|
||||||
|
background: none
|
||||||
|
border: 1px solid #48b57e
|
||||||
|
padding: 2px 11px
|
||||||
|
color: #444
|
||||||
|
.action-buttons .show-actions
|
||||||
|
display: inline-block
|
||||||
|
+dark-mode
|
||||||
|
background: #1b6540
|
||||||
|
color: rgba(255,255,255, 0.87)
|
||||||
|
&.action-buttons-visible
|
||||||
|
background: none
|
||||||
|
border: 1px solid #1b6540
|
||||||
|
color: rgba(255,255,255, 0.65)
|
||||||
|
&.scratch
|
||||||
|
font-weight: 600
|
||||||
|
&.new-note
|
||||||
|
//font-size: 12px
|
||||||
|
.name
|
||||||
|
margin-right: 12px
|
||||||
|
flex-shrink: 0
|
||||||
|
overflow: hidden
|
||||||
|
text-overflow: ellipsis
|
||||||
|
text-wrap: nowrap
|
||||||
|
::v-deep(b)
|
||||||
|
font-weight: 700
|
||||||
|
.path
|
||||||
|
opacity: 0.6
|
||||||
|
font-size: 12px
|
||||||
|
flex-shrink: 1
|
||||||
|
overflow: hidden
|
||||||
|
text-overflow: ellipsis
|
||||||
|
text-wrap: nowrap
|
||||||
|
::v-deep(b)
|
||||||
|
font-weight: 700
|
||||||
|
.action-buttons
|
||||||
|
position: absolute
|
||||||
|
top: 1px
|
||||||
|
right: 0px
|
||||||
|
padding: 0 1px
|
||||||
|
&.visible
|
||||||
|
background: #efefef
|
||||||
|
+dark-mode
|
||||||
|
background: #151516
|
||||||
|
button
|
||||||
|
padding: 0 10px
|
||||||
|
height: 20px
|
||||||
|
font-size: 12px
|
||||||
|
background: none
|
||||||
|
border: none
|
||||||
|
border-radius: 2px
|
||||||
|
margin-right: 2px
|
||||||
|
cursor: pointer
|
||||||
|
&:last-child
|
||||||
|
margin-right: 0
|
||||||
|
&:hover
|
||||||
|
background: rgba(0,0,0, 0.1)
|
||||||
|
+dark-mode
|
||||||
|
&:hover
|
||||||
|
background-color: rgba(255,255,255, 0.1)
|
||||||
|
&.selected
|
||||||
|
background: #48b57e
|
||||||
|
color: #fff
|
||||||
|
&:hover
|
||||||
|
background: #3ea471
|
||||||
|
&.delete
|
||||||
|
background: #e95050
|
||||||
|
&:hover
|
||||||
|
background: #ce4848
|
||||||
|
+dark-mode
|
||||||
|
background: #1b6540
|
||||||
|
&:hover
|
||||||
|
background: #1f7449
|
||||||
|
&.delete
|
||||||
|
background: #ae1e1e
|
||||||
|
&:hover
|
||||||
|
background: #bf2222
|
||||||
|
&.confirm
|
||||||
|
font-weight: 600
|
||||||
|
&.show-actions
|
||||||
|
display: none
|
||||||
|
position: relative
|
||||||
|
top: 1px
|
||||||
|
padding: 1px 8px
|
||||||
|
//cursor: default
|
||||||
|
background-image: url(@/assets/icons/arrow-right-white.svg)
|
||||||
|
width: 22px
|
||||||
|
height: 19px
|
||||||
|
background-size: 19px
|
||||||
|
background-position: center center
|
||||||
|
background-repeat: no-repeat
|
||||||
|
+dark-mode
|
||||||
|
background-image: url(@/assets/icons/arrow-right-grey.svg)
|
||||||
|
</style>
|
303
src/components/EditBuffer.vue
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
<script>
|
||||||
|
import slugify from '@sindresorhus/slugify';
|
||||||
|
|
||||||
|
import { toRaw } from 'vue';
|
||||||
|
import { mapState, mapActions } from 'pinia'
|
||||||
|
import { useHeynoteStore } from "../stores/heynote-store"
|
||||||
|
|
||||||
|
import FolderSelector from './folder-selector/FolderSelector.vue'
|
||||||
|
|
||||||
|
const pathSep = window.heynote.buffer.pathSeparator
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
name: "",
|
||||||
|
filename: "",
|
||||||
|
tags: [],
|
||||||
|
directoryTree: null,
|
||||||
|
parentPath: "",
|
||||||
|
errors: {
|
||||||
|
name: null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
FolderSelector
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
this.$refs.nameInput.focus()
|
||||||
|
this.updateBuffers()
|
||||||
|
|
||||||
|
console.log("EditNote mounted", this.currentNote)
|
||||||
|
this.name = this.currentNote.name
|
||||||
|
|
||||||
|
// build directory tree
|
||||||
|
const directories = await window.heynote.buffer.getDirectoryList()
|
||||||
|
const rootNode = {
|
||||||
|
name: "Heynote Root",
|
||||||
|
path: "",
|
||||||
|
children: [],
|
||||||
|
open: true,
|
||||||
|
}
|
||||||
|
const getNodeFromList = (list, part) => list.find(node => node.name === part)
|
||||||
|
|
||||||
|
directories.forEach((path) => {
|
||||||
|
const parts = path.split(pathSep)
|
||||||
|
let currentLevel = rootNode
|
||||||
|
let currentParts = []
|
||||||
|
parts.forEach(part => {
|
||||||
|
currentParts.push(part)
|
||||||
|
let node = getNodeFromList(currentLevel.children, part)
|
||||||
|
if (node) {
|
||||||
|
currentLevel = node
|
||||||
|
} else {
|
||||||
|
const currentPath = currentParts.join(pathSep)
|
||||||
|
node = {
|
||||||
|
name: part,
|
||||||
|
children: [],
|
||||||
|
path: currentPath,
|
||||||
|
open: this.currentBufferPath.startsWith(currentPath),
|
||||||
|
}
|
||||||
|
currentLevel.children.push(node)
|
||||||
|
currentLevel = node
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
//console.log("tree:", rootNode)
|
||||||
|
this.directoryTree = rootNode
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapState(useHeynoteStore, [
|
||||||
|
"buffers",
|
||||||
|
"currentBufferPath",
|
||||||
|
]),
|
||||||
|
|
||||||
|
currentNote() {
|
||||||
|
return this.buffers[this.currentBufferPath]
|
||||||
|
},
|
||||||
|
|
||||||
|
currentNoteDirectory() {
|
||||||
|
return this.currentBufferPath.split(pathSep).slice(0, -1).join(pathSep)
|
||||||
|
},
|
||||||
|
|
||||||
|
nameInputClass() {
|
||||||
|
return {
|
||||||
|
"name-input": true,
|
||||||
|
"error": this.errors.name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
...mapActions(useHeynoteStore, [
|
||||||
|
"updateBuffers",
|
||||||
|
"updateBufferMetadata",
|
||||||
|
]),
|
||||||
|
|
||||||
|
onKeydown(event) {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
event.preventDefault()
|
||||||
|
this.cancel()
|
||||||
|
} if (event.key === "Enter") {
|
||||||
|
event.preventDefault()
|
||||||
|
this.submit()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onCancelKeydown(event) {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
this.cancel()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.$emit("close")
|
||||||
|
},
|
||||||
|
|
||||||
|
onInputKeydown(event) {
|
||||||
|
// support Ctrl/Cmd+A to select all
|
||||||
|
if (event.key === "a" && event[window.heynote.platform.isMac ? "metaKey" : "ctrlKey"]) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.srcElement.select()
|
||||||
|
}
|
||||||
|
|
||||||
|
// redirect arrow keys and page up/down to folder selector
|
||||||
|
const redirectKeys = ["ArrowDown", "ArrowUp", "PageDown", "PageUp"]
|
||||||
|
if (redirectKeys.includes(event.key)) {
|
||||||
|
this.$refs.folderSelect.$el.dispatchEvent(new KeyboardEvent("keydown", {key: event.key}))
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
submit() {
|
||||||
|
let slug = slugify(this.name)
|
||||||
|
if (slug === "") {
|
||||||
|
this.errors.name = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const parentPathPrefix = this.parentPath === "" ? "" : this.parentPath + pathSep
|
||||||
|
let path;
|
||||||
|
for (let i=0; i<1000; i++) {
|
||||||
|
let filename = slug + ".txt"
|
||||||
|
path = parentPathPrefix + filename
|
||||||
|
if (path === this.currentBufferPath || !this.buffers[path]) {
|
||||||
|
// file name is ok if it's the current note, or if it doesn't exist
|
||||||
|
break
|
||||||
|
}
|
||||||
|
slug = slugify(this.name + "-" + i)
|
||||||
|
}
|
||||||
|
if (path !== this.currentBufferPath && this.buffers[path]) {
|
||||||
|
console.error("Failed to edit note, path already exists", path)
|
||||||
|
this.errors.name = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log("Update note", path)
|
||||||
|
this.updateBufferMetadata(this.currentBufferPath, this.name, path)
|
||||||
|
this.$emit("close")
|
||||||
|
//this.$emit("create", this.$refs.input.value)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="fader" @keydown.stop="onKeydown" tabindex="-1">
|
||||||
|
<form class="new-note" tabindex="-1" @focusout="onFocusOut" ref="container" @submit.prevent="submit">
|
||||||
|
<div class="container">
|
||||||
|
<h1>Edit Note</h1>
|
||||||
|
<input
|
||||||
|
placeholder="Name"
|
||||||
|
type="text"
|
||||||
|
v-model="name"
|
||||||
|
:class="nameInputClass"
|
||||||
|
ref="nameInput"
|
||||||
|
@keydown="onInputKeydown"
|
||||||
|
@input="errors.name = false"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label for="folder-select">Move to</label>
|
||||||
|
<FolderSelector
|
||||||
|
v-if="directoryTree"
|
||||||
|
:directoryTree="directoryTree"
|
||||||
|
:selectedPath="currentNoteDirectory"
|
||||||
|
id="folder-select"
|
||||||
|
v-model="parentPath"
|
||||||
|
ref="folderSelect"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="bottom-bar">
|
||||||
|
<button type="submit">Update Note</button>
|
||||||
|
<button
|
||||||
|
class="cancel"
|
||||||
|
@keydown="onCancelKeydown"
|
||||||
|
@click.stop.prevent="cancel"
|
||||||
|
>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="sass">
|
||||||
|
.fader
|
||||||
|
position: fixed
|
||||||
|
top: 0
|
||||||
|
left: 0
|
||||||
|
bottom: 0
|
||||||
|
right: 0
|
||||||
|
background: rgba(0,0,0, 0.2)
|
||||||
|
.new-note
|
||||||
|
font-size: 13px
|
||||||
|
//background: #48b57e
|
||||||
|
background: #efefef
|
||||||
|
width: 420px
|
||||||
|
position: absolute
|
||||||
|
top: 0
|
||||||
|
left: 50%
|
||||||
|
transform: translateX(-50%)
|
||||||
|
border-radius: 0 0 5px 5px
|
||||||
|
box-shadow: 0 0 10px rgba(0,0,0,0.3)
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
max-height: 100%
|
||||||
|
&:focus
|
||||||
|
outline: none
|
||||||
|
+dark-mode
|
||||||
|
background: #151516
|
||||||
|
box-shadow: 0 0 10px rgba(0,0,0,0.5)
|
||||||
|
color: rgba(255,255,255, 0.7)
|
||||||
|
+webapp-mobile
|
||||||
|
max-width: calc(100% - 80px)
|
||||||
|
|
||||||
|
.container
|
||||||
|
padding: 10px
|
||||||
|
min-height: 0
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
|
||||||
|
h1
|
||||||
|
font-weight: bold
|
||||||
|
margin-bottom: 14px
|
||||||
|
|
||||||
|
label
|
||||||
|
display: block
|
||||||
|
margin-bottom: 6px
|
||||||
|
//padding-left: 2px
|
||||||
|
font-size: 12px
|
||||||
|
font-weight: 600
|
||||||
|
|
||||||
|
|
||||||
|
.name-input
|
||||||
|
width: 100%
|
||||||
|
background: #fff
|
||||||
|
padding: 4px 5px
|
||||||
|
border: 1px solid #ccc
|
||||||
|
box-sizing: border-box
|
||||||
|
border-radius: 2px
|
||||||
|
margin-bottom: 16px
|
||||||
|
&:focus
|
||||||
|
outline: none
|
||||||
|
border: 1px solid #fff
|
||||||
|
outline: 2px solid #48b57e
|
||||||
|
&.error
|
||||||
|
background: #ffe9e9
|
||||||
|
+dark-mode
|
||||||
|
background: #3b3b3b
|
||||||
|
color: rgba(255,255,255, 0.9)
|
||||||
|
border: 1px solid #5a5a5a
|
||||||
|
&:focus
|
||||||
|
border: 1px solid #3b3b3b
|
||||||
|
+webapp-mobile
|
||||||
|
font-size: 16px
|
||||||
|
max-width: 100%
|
||||||
|
|
||||||
|
.bottom-bar
|
||||||
|
border-radius: 0 0 5px 5px
|
||||||
|
//background: #e3e3e3
|
||||||
|
padding: 10px
|
||||||
|
padding-top: 0
|
||||||
|
display: flex
|
||||||
|
justify-content: space-between
|
||||||
|
button
|
||||||
|
font-size: 12px
|
||||||
|
height: 28px
|
||||||
|
border: 1px solid #c5c5c5
|
||||||
|
border-radius: 3px
|
||||||
|
padding-left: 10px
|
||||||
|
padding-right: 10px
|
||||||
|
&:focus
|
||||||
|
outline-color: #48b57e
|
||||||
|
+dark-mode
|
||||||
|
background: #444
|
||||||
|
border: none
|
||||||
|
color: rgba(255,255,255, 0.75)
|
||||||
|
&[type="submit"]
|
||||||
|
order: 1
|
||||||
|
&.cancel
|
||||||
|
order: 0
|
||||||
|
|
||||||
|
</style>
|
@ -1,34 +1,18 @@
|
|||||||
<script>
|
<script>
|
||||||
import { HeynoteEditor, LANGUAGE_SELECTOR_EVENT } from '../editor/editor.js'
|
|
||||||
import { syntaxTree } from "@codemirror/language"
|
import { syntaxTree } from "@codemirror/language"
|
||||||
|
import { toRaw } from 'vue';
|
||||||
|
import { mapState, mapWritableState, mapActions, mapStores } from 'pinia'
|
||||||
|
import { useErrorStore } from "../stores/error-store"
|
||||||
|
import { useHeynoteStore } from "../stores/heynote-store.js"
|
||||||
|
import { useEditorCacheStore } from "../stores/editor-cache"
|
||||||
|
import { REDO_EVENT, WINDOW_CLOSE_EVENT, DELETE_BLOCK_EVENT, UNDO_EVENT, SELECT_ALL_EVENT } from '@/src/common/constants';
|
||||||
|
|
||||||
|
const NUM_EDITOR_INSTANCES = 5
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
theme: String,
|
|
||||||
development: Boolean,
|
development: Boolean,
|
||||||
debugSyntaxTree: Boolean,
|
debugSyntaxTree: Boolean,
|
||||||
keymap: {
|
|
||||||
type: String,
|
|
||||||
default: "default",
|
|
||||||
},
|
|
||||||
emacsMetaKey: {
|
|
||||||
type: String,
|
|
||||||
default: "alt",
|
|
||||||
},
|
|
||||||
showLineNumberGutter: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
showFoldGutter: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
bracketClosing: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
fontFamily: String,
|
|
||||||
fontSize: Number,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
components: {},
|
components: {},
|
||||||
@ -36,59 +20,63 @@
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
syntaxTreeDebugContent: null,
|
syntaxTreeDebugContent: null,
|
||||||
|
editor: null,
|
||||||
|
onWindowClose: null,
|
||||||
|
onUndo: null,
|
||||||
|
onRedo: null,
|
||||||
|
onDeleteBlock: null,
|
||||||
|
onSelectAll: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$refs.editor.addEventListener("selectionChange", (e) => {
|
// initialize editorCacheStore (sets up watchers for settings changes, propagating them to all editors)
|
||||||
//console.log("selectionChange:", e)
|
this.editorCacheStore.setUp(this.$refs.editor);
|
||||||
this.$emit("cursorChange", {
|
|
||||||
cursorLine: e.cursorLine,
|
|
||||||
selectionSize: e.selectionSize,
|
|
||||||
language: e.language,
|
|
||||||
languageAuto: e.languageAuto,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
this.$refs.editor.addEventListener(LANGUAGE_SELECTOR_EVENT, (e) => {
|
this.loadBuffer(this.currentBufferPath)
|
||||||
this.$emit("openLanguageSelector")
|
|
||||||
})
|
|
||||||
|
|
||||||
// load buffer content and create editor
|
|
||||||
window.heynote.buffer.load().then((content) => {
|
|
||||||
let diskContent = content
|
|
||||||
this.editor = new HeynoteEditor({
|
|
||||||
element: this.$refs.editor,
|
|
||||||
content: content,
|
|
||||||
theme: this.theme,
|
|
||||||
saveFunction: (content) => {
|
|
||||||
if (content === diskContent) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
diskContent = content
|
|
||||||
window.heynote.buffer.save(content)
|
|
||||||
},
|
|
||||||
keymap: this.keymap,
|
|
||||||
emacsMetaKey: this.emacsMetaKey,
|
|
||||||
showLineNumberGutter: this.showLineNumberGutter,
|
|
||||||
showFoldGutter: this.showFoldGutter,
|
|
||||||
bracketClosing: this.bracketClosing,
|
|
||||||
fontFamily: this.fontFamily,
|
|
||||||
fontSize: this.fontSize,
|
|
||||||
})
|
|
||||||
window._heynote_editor = this.editor
|
|
||||||
window.document.addEventListener("currenciesLoaded", this.onCurrenciesLoaded)
|
|
||||||
|
|
||||||
// set up buffer change listener
|
|
||||||
window.heynote.buffer.onChangeCallback((event, content) => {
|
|
||||||
diskContent = content
|
|
||||||
this.editor.setContent(content)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
// set up window close handler that will save the buffer and quit
|
// set up window close handler that will save the buffer and quit
|
||||||
window.heynote.onWindowClose(() => {
|
this.onWindowClose = () => {
|
||||||
window.heynote.buffer.saveAndQuit(this.editor.getContent())
|
window.heynote.buffer.saveAndQuit([
|
||||||
})
|
[this.editor.path, this.editor.getContent()],
|
||||||
|
])
|
||||||
|
}
|
||||||
|
window.heynote.mainProcess.on(WINDOW_CLOSE_EVENT, this.onWindowClose)
|
||||||
|
|
||||||
|
this.onUndo = () => {
|
||||||
|
if (this.editor) {
|
||||||
|
toRaw(this.editor).undo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.heynote.mainProcess.on(UNDO_EVENT, this.onUndo)
|
||||||
|
|
||||||
|
this.onRedo = () => {
|
||||||
|
if (this.editor) {
|
||||||
|
toRaw(this.editor).redo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.heynote.mainProcess.on(REDO_EVENT, this.onRedo)
|
||||||
|
|
||||||
|
this.onDeleteBlock = () => {
|
||||||
|
if (this.editor) {
|
||||||
|
toRaw(this.editor).deleteActiveBlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.heynote.mainProcess.on(DELETE_BLOCK_EVENT, this.onDeleteBlock)
|
||||||
|
|
||||||
|
this.onSelectAll = () => {
|
||||||
|
const activeEl = document.activeElement
|
||||||
|
if (activeEl && activeEl.tagName === "INPUT") {
|
||||||
|
// if the active element is an input, select all text in it
|
||||||
|
activeEl.select()
|
||||||
|
} else if (this.editor) {
|
||||||
|
// make sure the editor is focused
|
||||||
|
if (this.$refs.editor.contains(activeEl)) {
|
||||||
|
toRaw(this.editor).selectAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.heynote.mainProcess.on(SELECT_ALL_EVENT, this.onSelectAll)
|
||||||
|
|
||||||
// if debugSyntaxTree prop is set, display syntax tree for debugging
|
// if debugSyntaxTree prop is set, display syntax tree for debugging
|
||||||
if (this.debugSyntaxTree) {
|
if (this.debugSyntaxTree) {
|
||||||
@ -111,63 +99,82 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
window.document.removeEventListener("currenciesLoaded", this.onCurrenciesLoaded)
|
window.heynote.mainProcess.off(WINDOW_CLOSE_EVENT, this.onWindowClose)
|
||||||
|
window.heynote.mainProcess.off(UNDO_EVENT, this.onUndo)
|
||||||
|
window.heynote.mainProcess.off(REDO_EVENT, this.onRedo)
|
||||||
|
window.heynote.mainProcess.off(DELETE_BLOCK_EVENT, this.onDeleteBlock)
|
||||||
|
window.heynote.mainProcess.off(SELECT_ALL_EVENT, this.onSelectAll)
|
||||||
|
this.editorCacheStore.tearDown();
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
watch: {
|
||||||
theme(newTheme) {
|
loadNewEditor() {
|
||||||
this.editor.setTheme(newTheme)
|
//console.log("currentBufferPath changed to", path)
|
||||||
|
this.loadBuffer(this.currentBufferPath)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
keymap() {
|
computed: {
|
||||||
this.editor.setKeymap(this.keymap, this.emacsMetaKey)
|
...mapStores(useEditorCacheStore),
|
||||||
},
|
...mapState(useHeynoteStore, [
|
||||||
|
"currentBufferPath",
|
||||||
|
"libraryId",
|
||||||
|
]),
|
||||||
|
...mapWritableState(useHeynoteStore, [
|
||||||
|
"currentEditor",
|
||||||
|
"currentBufferName",
|
||||||
|
]),
|
||||||
|
|
||||||
emacsMetaKey() {
|
loadNewEditor() {
|
||||||
this.editor.setKeymap(this.keymap, this.emacsMetaKey)
|
return `${this.currentBufferPath}|${this.libraryId}`
|
||||||
},
|
|
||||||
|
|
||||||
showLineNumberGutter(show) {
|
|
||||||
this.editor.setLineNumberGutter(show)
|
|
||||||
},
|
|
||||||
|
|
||||||
showFoldGutter(show) {
|
|
||||||
this.editor.setFoldGutter(show)
|
|
||||||
},
|
|
||||||
|
|
||||||
bracketClosing(value) {
|
|
||||||
this.editor.setBracketClosing(value)
|
|
||||||
},
|
|
||||||
|
|
||||||
fontFamily() {
|
|
||||||
this.editor.setFont(this.fontFamily, this.fontSize)
|
|
||||||
},
|
|
||||||
fontSize() {
|
|
||||||
this.editor.setFont(this.fontFamily, this.fontSize)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
setLanguage(language) {
|
loadBuffer(path) {
|
||||||
if (language === "auto") {
|
//console.log("loadBuffer", path)
|
||||||
this.editor.setCurrentLanguage("text", true)
|
if (this.editor) {
|
||||||
} else {
|
this.editor.hide()
|
||||||
this.editor.setCurrentLanguage(language, false)
|
|
||||||
}
|
}
|
||||||
this.editor.focus()
|
|
||||||
|
let cachedEditor = this.editorCacheStore.getEditor(path)
|
||||||
|
if (cachedEditor) {
|
||||||
|
//console.log("show cached editor")
|
||||||
|
this.editor = cachedEditor
|
||||||
|
toRaw(this.editor).show()
|
||||||
|
} else {
|
||||||
|
//console.log("create new editor")
|
||||||
|
this.editor = this.editorCacheStore.createEditor(path)
|
||||||
|
this.editorCacheStore.addEditor(path, toRaw(this.editor))
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentEditor = toRaw(this.editor)
|
||||||
|
window._heynote_editor = toRaw(this.editor)
|
||||||
|
},
|
||||||
|
|
||||||
|
setLanguage(language) {
|
||||||
|
const editor = toRaw(this.editor)
|
||||||
|
if (language === "auto") {
|
||||||
|
editor.setCurrentLanguage(null, true)
|
||||||
|
} else {
|
||||||
|
editor.setCurrentLanguage(language, false)
|
||||||
|
}
|
||||||
|
editor.focus()
|
||||||
},
|
},
|
||||||
|
|
||||||
formatCurrentBlock() {
|
formatCurrentBlock() {
|
||||||
this.editor.formatCurrentBlock()
|
const editor = toRaw(this.editor)
|
||||||
this.editor.focus()
|
editor.formatCurrentBlock()
|
||||||
},
|
editor.focus()
|
||||||
|
|
||||||
onCurrenciesLoaded() {
|
|
||||||
this.editor.currenciesLoaded()
|
|
||||||
},
|
},
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
this.editor.focus()
|
toRaw(this.editor).focus()
|
||||||
|
},
|
||||||
|
|
||||||
|
onContextMenu(event) {
|
||||||
|
event.preventDefault()
|
||||||
|
window.heynote.mainProcess.invoke("showEditorContextMenu")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -175,7 +182,7 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="editor" ref="editor"></div>
|
<div class="editor" ref="editor" @contextmenu="onContextMenu"></div>
|
||||||
<div
|
<div
|
||||||
v-if="debugSyntaxTree"
|
v-if="debugSyntaxTree"
|
||||||
v-html="syntaxTreeDebugContent"
|
v-html="syntaxTreeDebugContent"
|
||||||
|
114
src/components/ErrorMessages.vue
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
<script>
|
||||||
|
import { mapState, mapActions } from 'pinia'
|
||||||
|
import { useErrorStore } from "../stores/error-store"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
computed: {
|
||||||
|
...mapState(useErrorStore, ["errors"]),
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
...mapActions(useErrorStore, ["popError"]),
|
||||||
|
|
||||||
|
pluralize(count, singular, plural) {
|
||||||
|
return count === 1 ? singular : plural
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="error-messages"
|
||||||
|
v-if="errors && errors.length > 0"
|
||||||
|
>
|
||||||
|
<div class="dialog">
|
||||||
|
<div class="dialog-content">
|
||||||
|
<h1>Error</h1>
|
||||||
|
<p>
|
||||||
|
{{ errors[0] }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="bottom-bar">
|
||||||
|
<div style="flex-grow:1;">
|
||||||
|
<div
|
||||||
|
v-if="errors.length > 1"
|
||||||
|
class="count"
|
||||||
|
>
|
||||||
|
{{ errors.length-1 }} more {{ pluralize(errors.length-1, "error", "errors") }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="popError"
|
||||||
|
class="close"
|
||||||
|
>Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="shader"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<style lang="sass" scoped>
|
||||||
|
.error-messages
|
||||||
|
position: fixed
|
||||||
|
top: 0
|
||||||
|
left: 0
|
||||||
|
bottom: 0
|
||||||
|
right: 0
|
||||||
|
|
||||||
|
.shader
|
||||||
|
z-index: 1
|
||||||
|
position: absolute
|
||||||
|
top: 0
|
||||||
|
left: 0
|
||||||
|
bottom: 0
|
||||||
|
right: 0
|
||||||
|
background: rgba(0, 0, 0, 0.5)
|
||||||
|
|
||||||
|
.dialog
|
||||||
|
box-sizing: border-box
|
||||||
|
z-index: 2
|
||||||
|
position: absolute
|
||||||
|
left: 50%
|
||||||
|
top: 50%
|
||||||
|
transform: translate(-50%, -50%)
|
||||||
|
width: 440px
|
||||||
|
height: 200px
|
||||||
|
max-width: 100%
|
||||||
|
max-height: 100%
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
border-radius: 5px
|
||||||
|
background: #fff
|
||||||
|
color: #333
|
||||||
|
box-shadow: 0 0 25px rgba(0, 0, 0, 0.2)
|
||||||
|
overflow-y: auto
|
||||||
|
&:active, &:selected, &:focus, &:focus-visible
|
||||||
|
border: none
|
||||||
|
outline: none
|
||||||
|
+dark-mode
|
||||||
|
background: #333
|
||||||
|
color: #eee
|
||||||
|
box-shadow: 0 0 25px rgba(0, 0, 0, 0.3)
|
||||||
|
.dialog-content
|
||||||
|
flex-grow: 1
|
||||||
|
padding: 30px
|
||||||
|
h1
|
||||||
|
font-size: 14px
|
||||||
|
font-weight: 700
|
||||||
|
display: block
|
||||||
|
margin-bottom: 1em
|
||||||
|
|
||||||
|
.bottom-bar
|
||||||
|
border-radius: 0 0 5px 5px
|
||||||
|
background: #eee
|
||||||
|
padding: 10px 20px
|
||||||
|
display: flex
|
||||||
|
align-items: center
|
||||||
|
+dark-mode
|
||||||
|
background: #222
|
||||||
|
.close
|
||||||
|
height: 28px
|
||||||
|
</style>
|
@ -1,16 +1,22 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import fuzzysort from 'fuzzysort'
|
||||||
import { LANGUAGES } from '../editor/languages.js'
|
import { LANGUAGES } from '../editor/languages.js'
|
||||||
|
|
||||||
const items = LANGUAGES.map(l => {
|
const items = LANGUAGES.map(l => {
|
||||||
return {
|
return {
|
||||||
"token": l.token,
|
"token": l.token,
|
||||||
"name": l.name
|
"name": l.name,
|
||||||
|
"guesslang": l.guesslang,
|
||||||
}
|
}
|
||||||
}).sort((a, b) => {
|
}).sort((a, b) => {
|
||||||
return a.name.localeCompare(b.name)
|
return a.name.localeCompare(b.name)
|
||||||
})
|
})
|
||||||
items.unshift({token: "auto", name:"Auto-detect"})
|
items.unshift({token: "auto", name:"Auto-detect"})
|
||||||
|
|
||||||
|
items.forEach((item, idx) => {
|
||||||
|
item.preparedName = fuzzysort.prepare(item.name)
|
||||||
|
})
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -26,8 +32,18 @@
|
|||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
filteredItems() {
|
filteredItems() {
|
||||||
return items.filter((lang) => {
|
if (this.filter === "") {
|
||||||
return lang.name.toLowerCase().indexOf(this.filter.toLowerCase()) !== -1
|
return items
|
||||||
|
}
|
||||||
|
const searchResults = fuzzysort.go(this.filter, items, {
|
||||||
|
keys: ['name', 'guesslang'],
|
||||||
|
})
|
||||||
|
return searchResults.map(result => {
|
||||||
|
const highlight = result[0].highlight("<b>", "</b>")
|
||||||
|
return {
|
||||||
|
"token": result.obj.token,
|
||||||
|
"name": highlight || result.obj.name,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -96,9 +112,8 @@
|
|||||||
:class="idx === selected ? 'selected' : ''"
|
:class="idx === selected ? 'selected' : ''"
|
||||||
@click="selectItem(item.token)"
|
@click="selectItem(item.token)"
|
||||||
ref="item"
|
ref="item"
|
||||||
>
|
v-html="item.name"
|
||||||
{{ item.name }}
|
/>
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -106,12 +121,12 @@
|
|||||||
|
|
||||||
<style scoped lang="sass">
|
<style scoped lang="sass">
|
||||||
.scroller
|
.scroller
|
||||||
overflow: auto
|
//overflow: auto
|
||||||
position: fixed
|
//position: fixed
|
||||||
top: 0
|
//top: 0
|
||||||
left: 0
|
//left: 0
|
||||||
bottom: 0
|
//bottom: 0
|
||||||
right: 0
|
//right: 0
|
||||||
.language-selector
|
.language-selector
|
||||||
font-size: 13px
|
font-size: 13px
|
||||||
padding: 10px
|
padding: 10px
|
||||||
@ -121,6 +136,10 @@
|
|||||||
top: 0
|
top: 0
|
||||||
left: 50%
|
left: 50%
|
||||||
transform: translateX(-50%)
|
transform: translateX(-50%)
|
||||||
|
max-height: 100%
|
||||||
|
box-sizing: border-box
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
border-radius: 0 0 5px 5px
|
border-radius: 0 0 5px 5px
|
||||||
box-shadow: 0 0 10px rgba(0,0,0,0.3)
|
box-shadow: 0 0 10px rgba(0,0,0,0.3)
|
||||||
+dark-mode
|
+dark-mode
|
||||||
@ -135,7 +154,7 @@
|
|||||||
border: 1px solid #ccc
|
border: 1px solid #ccc
|
||||||
box-sizing: border-box
|
box-sizing: border-box
|
||||||
border-radius: 2px
|
border-radius: 2px
|
||||||
width: 400px
|
width: 300px
|
||||||
margin-bottom: 10px
|
margin-bottom: 10px
|
||||||
&:focus
|
&:focus
|
||||||
outline: none
|
outline: none
|
||||||
@ -152,6 +171,7 @@
|
|||||||
max-width: 100%
|
max-width: 100%
|
||||||
|
|
||||||
.items
|
.items
|
||||||
|
overflow-y: auto
|
||||||
> li
|
> li
|
||||||
border-radius: 3px
|
border-radius: 3px
|
||||||
padding: 5px 12px
|
padding: 5px 12px
|
||||||
@ -162,10 +182,12 @@
|
|||||||
background: #48b57e
|
background: #48b57e
|
||||||
color: #fff
|
color: #fff
|
||||||
+dark-mode
|
+dark-mode
|
||||||
color: rgba(255,255,255, 0.53)
|
color: rgba(255,255,255, 0.65)
|
||||||
&:hover
|
&:hover
|
||||||
background: #29292a
|
background: #29292a
|
||||||
&.selected
|
&.selected
|
||||||
background: #1b6540
|
background: #1b6540
|
||||||
color: rgba(255,255,255, 0.87)
|
color: rgba(255,255,255, 0.87)
|
||||||
|
::v-deep(b)
|
||||||
|
font-weight: 700
|
||||||
</style>
|
</style>
|
||||||
|
319
src/components/NewBuffer.vue
Normal file
@ -0,0 +1,319 @@
|
|||||||
|
<script>
|
||||||
|
import slugify from '@sindresorhus/slugify';
|
||||||
|
|
||||||
|
import { mapState, mapActions } from 'pinia'
|
||||||
|
import { useHeynoteStore } from "../stores/heynote-store"
|
||||||
|
|
||||||
|
import FolderSelector from './folder-selector/FolderSelector.vue'
|
||||||
|
|
||||||
|
const pathSep = window.heynote.buffer.pathSeparator
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
name: "",
|
||||||
|
filename: "",
|
||||||
|
tags: [],
|
||||||
|
directoryTree: null,
|
||||||
|
parentPath: "",
|
||||||
|
errors: {
|
||||||
|
name: null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
FolderSelector
|
||||||
|
},
|
||||||
|
|
||||||
|
async mounted() {
|
||||||
|
if (!!this.createBufferParams.name) {
|
||||||
|
this.name = this.createBufferParams.name
|
||||||
|
this.$refs.nameInput.focus()
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.$refs.nameInput.select()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$refs.nameInput.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateBuffers()
|
||||||
|
|
||||||
|
// build directory tree
|
||||||
|
const directories = await window.heynote.buffer.getDirectoryList()
|
||||||
|
const rootNode = {
|
||||||
|
name: "Heynote Root",
|
||||||
|
path: "",
|
||||||
|
children: [],
|
||||||
|
open: true,
|
||||||
|
}
|
||||||
|
const getNodeFromList = (list, part) => list.find(node => node.name === part)
|
||||||
|
|
||||||
|
directories.forEach((path) => {
|
||||||
|
const parts = path.split(pathSep)
|
||||||
|
let currentLevel = rootNode
|
||||||
|
let currentParts = []
|
||||||
|
parts.forEach(part => {
|
||||||
|
currentParts.push(part)
|
||||||
|
let node = getNodeFromList(currentLevel.children, part)
|
||||||
|
if (node) {
|
||||||
|
currentLevel = node
|
||||||
|
} else {
|
||||||
|
const currentPath = currentParts.join(pathSep)
|
||||||
|
node = {
|
||||||
|
name: part,
|
||||||
|
children: [],
|
||||||
|
path: currentPath,
|
||||||
|
open: this.currentBufferPath.startsWith(currentPath),
|
||||||
|
}
|
||||||
|
currentLevel.children.push(node)
|
||||||
|
currentLevel = node
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
//console.log("tree:", rootNode)
|
||||||
|
this.directoryTree = rootNode
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapState(useHeynoteStore, [
|
||||||
|
"buffers",
|
||||||
|
"currentBufferPath",
|
||||||
|
"createBufferParams",
|
||||||
|
]),
|
||||||
|
|
||||||
|
currentNoteDirectory() {
|
||||||
|
return this.currentBufferPath.split(pathSep).slice(0, -1).join(pathSep)
|
||||||
|
},
|
||||||
|
|
||||||
|
nameInputClass() {
|
||||||
|
return {
|
||||||
|
"name-input": true,
|
||||||
|
"error": this.errors.name,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
dialogTitle() {
|
||||||
|
return this.createBufferParams.mode === "currentBlock" ? "Move Block to New Buffer" : "New Buffer"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
...mapActions(useHeynoteStore, [
|
||||||
|
"updateBuffers",
|
||||||
|
"createNewBuffer",
|
||||||
|
"createNewBufferFromActiveBlock",
|
||||||
|
]),
|
||||||
|
|
||||||
|
onKeydown(event) {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
this.$emit("close")
|
||||||
|
event.preventDefault()
|
||||||
|
} if (event.key === "Enter") {
|
||||||
|
// without preventDefault, the editor will receive a Enter keydown event on webapp (not in Electron)
|
||||||
|
event.preventDefault()
|
||||||
|
this.submit()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onCancelKeydown(event) {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
this.cancel()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.$emit("close")
|
||||||
|
},
|
||||||
|
|
||||||
|
onInputKeydown(event) {
|
||||||
|
// support Ctrl/Cmd+A to select all
|
||||||
|
if (event.key === "a" && event[window.heynote.platform.isMac ? "metaKey" : "ctrlKey"]) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.srcElement.select()
|
||||||
|
}
|
||||||
|
|
||||||
|
// redirect arrow keys and page up/down to folder selector
|
||||||
|
const redirectKeys = ["ArrowDown", "ArrowUp", "PageDown", "PageUp"]
|
||||||
|
if (redirectKeys.includes(event.key)) {
|
||||||
|
this.$refs.folderSelect.$el.dispatchEvent(new KeyboardEvent("keydown", {key: event.key}))
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
submit() {
|
||||||
|
let slug = slugify(this.name)
|
||||||
|
if (slug === "") {
|
||||||
|
this.errors.name = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const parentPathPrefix = this.parentPath === "" ? "" : this.parentPath + pathSep
|
||||||
|
let path;
|
||||||
|
for (let i=0; i<1000; i++) {
|
||||||
|
let filename = slug + ".txt"
|
||||||
|
path = parentPathPrefix + filename
|
||||||
|
if (!this.buffers[path]) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
slug = slugify(this.name + "-" + i)
|
||||||
|
}
|
||||||
|
if (this.buffers[path]) {
|
||||||
|
console.error("Failed to create buffer, path already exists", path)
|
||||||
|
this.errors.name = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//console.log("Creating buffer", path, this.createBufferParams)
|
||||||
|
if (this.createBufferParams.mode === "currentBlock") {
|
||||||
|
this.createNewBufferFromActiveBlock(path, this.name)
|
||||||
|
} else if (this.createBufferParams.mode === "new") {
|
||||||
|
this.createNewBuffer(path, this.name)
|
||||||
|
} else {
|
||||||
|
throw new Error("Unknown createBuffer Mode: " + this.createBufferParams.mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit("close")
|
||||||
|
//this.$emit("create", this.$refs.input.value)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="fader" @keydown="onKeydown" tabindex="-1">
|
||||||
|
<form class="new-buffer" tabindex="-1" @focusout="onFocusOut" ref="container" @submit.prevent="submit">
|
||||||
|
<div class="container">
|
||||||
|
<h1>{{ dialogTitle }}</h1>
|
||||||
|
<input
|
||||||
|
placeholder="Name"
|
||||||
|
type="text"
|
||||||
|
v-model="name"
|
||||||
|
:class="nameInputClass"
|
||||||
|
ref="nameInput"
|
||||||
|
@keydown="onInputKeydown"
|
||||||
|
@input="errors.name = false"
|
||||||
|
autocomplete="off"
|
||||||
|
data-1p-ignore
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label for="folder-select">Create in</label>
|
||||||
|
<FolderSelector
|
||||||
|
v-if="directoryTree"
|
||||||
|
:directoryTree="directoryTree"
|
||||||
|
:selectedPath="currentNoteDirectory"
|
||||||
|
id="folder-select"
|
||||||
|
v-model="parentPath"
|
||||||
|
ref="folderSelect"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="bottom-bar">
|
||||||
|
<button type="submit">Create New Buffer</button>
|
||||||
|
<button
|
||||||
|
class="cancel"
|
||||||
|
@keydown="onCancelKeydown"
|
||||||
|
@click.stop.prevent="cancel"
|
||||||
|
>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="sass">
|
||||||
|
.fader
|
||||||
|
position: fixed
|
||||||
|
top: 0
|
||||||
|
left: 0
|
||||||
|
bottom: 0
|
||||||
|
right: 0
|
||||||
|
background: rgba(0,0,0, 0.2)
|
||||||
|
.new-buffer
|
||||||
|
font-size: 13px
|
||||||
|
//background: #48b57e
|
||||||
|
background: #efefef
|
||||||
|
width: 420px
|
||||||
|
position: absolute
|
||||||
|
top: 0
|
||||||
|
left: 50%
|
||||||
|
transform: translateX(-50%)
|
||||||
|
border-radius: 0 0 5px 5px
|
||||||
|
box-shadow: 0 0 10px rgba(0,0,0,0.3)
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
max-height: 100%
|
||||||
|
&:focus
|
||||||
|
outline: none
|
||||||
|
+dark-mode
|
||||||
|
background: #151516
|
||||||
|
box-shadow: 0 0 10px rgba(0,0,0,0.5)
|
||||||
|
color: rgba(255,255,255, 0.7)
|
||||||
|
+webapp-mobile
|
||||||
|
max-width: calc(100% - 80px)
|
||||||
|
|
||||||
|
.container
|
||||||
|
padding: 10px
|
||||||
|
min-height: 0
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
|
||||||
|
h1
|
||||||
|
font-weight: bold
|
||||||
|
margin-bottom: 14px
|
||||||
|
|
||||||
|
label
|
||||||
|
display: block
|
||||||
|
margin-bottom: 6px
|
||||||
|
//padding-left: 2px
|
||||||
|
font-size: 12px
|
||||||
|
font-weight: 600
|
||||||
|
|
||||||
|
|
||||||
|
.name-input
|
||||||
|
width: 100%
|
||||||
|
background: #fff
|
||||||
|
padding: 4px 5px
|
||||||
|
border: 1px solid #ccc
|
||||||
|
box-sizing: border-box
|
||||||
|
border-radius: 2px
|
||||||
|
margin-bottom: 16px
|
||||||
|
&:focus
|
||||||
|
outline: none
|
||||||
|
border: 1px solid #fff
|
||||||
|
outline: 2px solid #48b57e
|
||||||
|
&.error
|
||||||
|
background: #ffe9e9
|
||||||
|
+dark-mode
|
||||||
|
background: #3b3b3b
|
||||||
|
color: rgba(255,255,255, 0.9)
|
||||||
|
border: 1px solid #5a5a5a
|
||||||
|
&:focus
|
||||||
|
border: 1px solid #3b3b3b
|
||||||
|
+webapp-mobile
|
||||||
|
font-size: 16px
|
||||||
|
max-width: 100%
|
||||||
|
|
||||||
|
.bottom-bar
|
||||||
|
border-radius: 0 0 5px 5px
|
||||||
|
//background: #e3e3e3
|
||||||
|
padding: 10px
|
||||||
|
padding-top: 0
|
||||||
|
display: flex
|
||||||
|
justify-content: space-between
|
||||||
|
button
|
||||||
|
font-size: 12px
|
||||||
|
height: 28px
|
||||||
|
border: 1px solid #c5c5c5
|
||||||
|
border-radius: 3px
|
||||||
|
padding-left: 10px
|
||||||
|
padding-right: 10px
|
||||||
|
&:focus
|
||||||
|
outline-color: #48b57e
|
||||||
|
+dark-mode
|
||||||
|
background: #444
|
||||||
|
border: none
|
||||||
|
color: rgba(255,255,255, 0.75)
|
||||||
|
&[type="submit"]
|
||||||
|
order: 1
|
||||||
|
&.cancel
|
||||||
|
order: 0
|
||||||
|
|
||||||
|
</style>
|
@ -1,19 +1,14 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { mapState } from 'pinia'
|
||||||
import UpdateStatusItem from './UpdateStatusItem.vue'
|
import UpdateStatusItem from './UpdateStatusItem.vue'
|
||||||
import { LANGUAGES } from '../editor/languages.js'
|
import { LANGUAGES } from '../editor/languages.js'
|
||||||
|
import { useHeynoteStore } from "../stores/heynote-store"
|
||||||
|
|
||||||
const LANGUAGE_MAP = Object.fromEntries(LANGUAGES.map(l => [l.token, l]))
|
const LANGUAGE_MAP = Object.fromEntries(LANGUAGES.map(l => [l.token, l]))
|
||||||
const LANGUAGE_NAMES = Object.fromEntries(LANGUAGES.map(l => [l.token, l.name]))
|
const LANGUAGE_NAMES = Object.fromEntries(LANGUAGES.map(l => [l.token, l.name]))
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: [
|
props: [
|
||||||
"line",
|
|
||||||
"column",
|
|
||||||
"selectionSize",
|
|
||||||
"language",
|
|
||||||
"languageAuto",
|
|
||||||
"theme",
|
|
||||||
"themeSetting",
|
|
||||||
"autoUpdate",
|
"autoUpdate",
|
||||||
"allowBetaVersions",
|
"allowBetaVersions",
|
||||||
],
|
],
|
||||||
@ -33,8 +28,17 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapState(useHeynoteStore, [
|
||||||
|
"currentBufferName",
|
||||||
|
"currentCursorLine",
|
||||||
|
"currentLanguage",
|
||||||
|
"currentSelectionSize",
|
||||||
|
"currentLanguage",
|
||||||
|
"currentLanguageAuto",
|
||||||
|
]),
|
||||||
|
|
||||||
languageName() {
|
languageName() {
|
||||||
return LANGUAGE_NAMES[this.language] || this.language
|
return LANGUAGE_NAMES[this.currentLanguage] || this.currentLanguage
|
||||||
},
|
},
|
||||||
|
|
||||||
className() {
|
className() {
|
||||||
@ -42,7 +46,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
supportsFormat() {
|
supportsFormat() {
|
||||||
const lang = LANGUAGE_MAP[this.language]
|
const lang = LANGUAGE_MAP[this.currentLanguage]
|
||||||
return !!lang ? lang.supportsFormat : false
|
return !!lang ? lang.supportsFormat : false
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -54,6 +58,10 @@
|
|||||||
return `Format Block Content (Alt + Shift + F)`
|
return `Format Block Content (Alt + Shift + F)`
|
||||||
},
|
},
|
||||||
|
|
||||||
|
changeNoteTitle() {
|
||||||
|
return `Change Note (${this.cmdKey} + P)`
|
||||||
|
},
|
||||||
|
|
||||||
changeLanguageTitle() {
|
changeLanguageTitle() {
|
||||||
return `Change language for current block (${this.cmdKey} + L)`
|
return `Change language for current block (${this.cmdKey} + L)`
|
||||||
},
|
},
|
||||||
@ -68,24 +76,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="className">
|
<div :class="className">
|
||||||
<div class="status-block line-number">
|
<div class="status-block line-number">
|
||||||
Ln <span class="num">{{ line }}</span>
|
Ln <span class="num">{{ currentCursorLine?.line }}</span>
|
||||||
Col <span class="num">{{ column }}</span>
|
Col <span class="num">{{ currentCursorLine?.col }}</span>
|
||||||
<template v-if="selectionSize > 0">
|
<template v-if="currentSelectionSize > 0">
|
||||||
Sel <span class="num">{{ selectionSize }}</span>
|
Sel <span class="num">{{ currentSelectionSize }}</span>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<div
|
<div
|
||||||
@click="$emit('openLanguageSelector')"
|
@click.stop="$emit('openBufferSelector')"
|
||||||
|
class="status-block note clickable"
|
||||||
|
:title="changeNoteTitle"
|
||||||
|
>
|
||||||
|
{{ currentBufferName }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
@click.stop="$emit('openLanguageSelector')"
|
||||||
class="status-block lang clickable"
|
class="status-block lang clickable"
|
||||||
:title="changeLanguageTitle"
|
:title="changeLanguageTitle"
|
||||||
>
|
>
|
||||||
{{ languageName }}
|
{{ languageName }}
|
||||||
<span v-if="languageAuto" class="auto">(auto)</span>
|
<span v-if="currentLanguageAuto" class="auto">(auto)</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="supportsFormat"
|
v-if="supportsFormat"
|
||||||
@click="$emit('formatCurrentBlock')"
|
@click.stop="$emit('formatCurrentBlock')"
|
||||||
class="status-block format clickable"
|
class="status-block format clickable"
|
||||||
:title="formatBlockTitle"
|
:title="formatBlockTitle"
|
||||||
>
|
>
|
||||||
@ -96,11 +111,8 @@
|
|||||||
:autoUpdate="autoUpdate"
|
:autoUpdate="autoUpdate"
|
||||||
:allowBetaVersions="allowBetaVersions"
|
:allowBetaVersions="allowBetaVersions"
|
||||||
/>
|
/>
|
||||||
<div class="status-block theme clickable" @click="$emit('toggleTheme')" title="Toggle dark/light mode">
|
|
||||||
<span :class="'icon ' + themeSetting"></span>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
@click="$emit('openSettings')"
|
@click.stop="$emit('openSettings')"
|
||||||
class="status-block settings clickable"
|
class="status-block settings clickable"
|
||||||
title="Settings"
|
title="Settings"
|
||||||
>
|
>
|
||||||
@ -159,19 +171,6 @@
|
|||||||
color: rgba(255, 255, 255, 0.7)
|
color: rgba(255, 255, 255, 0.7)
|
||||||
+dark-mode
|
+dark-mode
|
||||||
color: rgba(255, 255, 255, 0.55)
|
color: rgba(255, 255, 255, 0.55)
|
||||||
.theme
|
|
||||||
padding-top: 0
|
|
||||||
padding-bottom: 0
|
|
||||||
.icon
|
|
||||||
background-size: 14px
|
|
||||||
background-repeat: no-repeat
|
|
||||||
background-position: center center
|
|
||||||
&.dark
|
|
||||||
background-image: url("@/assets/icons/dark-mode.png")
|
|
||||||
&.light
|
|
||||||
background-image: url("@/assets/icons/light-mode.png")
|
|
||||||
&.system
|
|
||||||
background-image: url("@/assets/icons/both-mode.png")
|
|
||||||
|
|
||||||
.format
|
.format
|
||||||
padding-top: 0
|
padding-top: 0
|
||||||
|
124
src/components/folder-selector/FolderItem.vue
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
name: String,
|
||||||
|
level: Number,
|
||||||
|
selected: Boolean,
|
||||||
|
newFolder: Boolean,
|
||||||
|
open: Boolean,
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
selected() {
|
||||||
|
if (this.selected) {
|
||||||
|
// scrollIntoViewIfNeeded is not supported in all browsers
|
||||||
|
// See: https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoViewIfNeeded
|
||||||
|
if (this.$el.scrollIntoViewIfNeeded) {
|
||||||
|
this.$el.scrollIntoViewIfNeeded({
|
||||||
|
behavior: "auto",
|
||||||
|
block: "nearest",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.$el.scrollIntoView({
|
||||||
|
behavior: "auto",
|
||||||
|
block: "nearest",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
className() {
|
||||||
|
return {
|
||||||
|
folder: true,
|
||||||
|
selected: this.selected,
|
||||||
|
new: this.newFolder,
|
||||||
|
open: this.open,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
style() {
|
||||||
|
return {
|
||||||
|
"--indent-level": this.level,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="className"
|
||||||
|
:style="style"
|
||||||
|
>
|
||||||
|
<span class="name">{{ name }}</span>
|
||||||
|
<button class="new-folder" tabindex="-1" @click.stop.prevent="$emit('new-folder')">New folder (+)</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="sass" scoped>
|
||||||
|
.folder
|
||||||
|
padding: 3px 6px
|
||||||
|
font-size: 13px
|
||||||
|
padding-left: calc(18px + var(--indent-level) * 16px)
|
||||||
|
display: flex
|
||||||
|
scroll-margin-top: 5px
|
||||||
|
scroll-margin-bottom: 5px
|
||||||
|
background-image: url('@/assets/icons/caret-right.svg')
|
||||||
|
background-size: 13px
|
||||||
|
background-repeat: no-repeat
|
||||||
|
background-position-y: 5px
|
||||||
|
background-position-x: calc(2px + var(--indent-level) * 16px)
|
||||||
|
+dark-mode
|
||||||
|
background-image: url('@/assets/icons/caret-right-white.svg')
|
||||||
|
color: rgba(255,255,255, 0.87)
|
||||||
|
&:hover
|
||||||
|
background-color: #f1f1f1
|
||||||
|
+dark-mode
|
||||||
|
background-color: #39393a
|
||||||
|
&.open
|
||||||
|
background-image: url('@/assets/icons/caret-down.svg')
|
||||||
|
+dark-mode
|
||||||
|
background-image: url('@/assets/icons/caret-down-white.svg')
|
||||||
|
&.selected
|
||||||
|
background-color: #48b57e
|
||||||
|
color: #fff
|
||||||
|
background-image: url('@/assets/icons/caret-right-white.svg')
|
||||||
|
&.open
|
||||||
|
background-image: url('@/assets/icons/caret-down-white.svg')
|
||||||
|
&:hover
|
||||||
|
background-color: #40a773
|
||||||
|
+dark-mode
|
||||||
|
background-color: #1b6540
|
||||||
|
color: rgba(255,255,255, 0.87)
|
||||||
|
&:hover
|
||||||
|
background-color: #1f6f47
|
||||||
|
.new-folder
|
||||||
|
display: block
|
||||||
|
color: rgba(255,255,255, 0.9)
|
||||||
|
&.new
|
||||||
|
font-style: italic
|
||||||
|
color: rgba(0,0,0, 0.5)
|
||||||
|
&.selected
|
||||||
|
color: rgba(255,255,255, 0.8)
|
||||||
|
+dark-mode
|
||||||
|
color: rgba(255,255,255, 0.5)
|
||||||
|
&.selected
|
||||||
|
color: rgba(255,255,255, 0.8)
|
||||||
|
|
||||||
|
|
||||||
|
.name
|
||||||
|
flex-grow: 1
|
||||||
|
overflow: hidden
|
||||||
|
text-overflow: ellipsis
|
||||||
|
white-space: nowrap
|
||||||
|
.new-folder
|
||||||
|
background: rgba(0,0,0, 0.15)
|
||||||
|
border: none
|
||||||
|
border-radius: 2px
|
||||||
|
font-size: 10px
|
||||||
|
display: none
|
||||||
|
flex-shrink: 0
|
||||||
|
cursor: pointer
|
||||||
|
</style>
|
253
src/components/folder-selector/FolderSelector.vue
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
<script>
|
||||||
|
import FolderItem from "./FolderItem.vue"
|
||||||
|
import NewFolderItem from "./NewFolderItem.vue"
|
||||||
|
|
||||||
|
const pathSep = window.heynote.buffer.pathSeparator
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
directoryTree: Object,
|
||||||
|
selectedPath: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
FolderItem,
|
||||||
|
NewFolderItem,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
tree: this.directoryTree,
|
||||||
|
selected: 0,
|
||||||
|
filter: "",
|
||||||
|
filterSearchStart: 0,
|
||||||
|
filterTimeout: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.selected = this.listItems.findIndex(item => item.path === this.selectedPath)
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
directoryTree(newVal) {
|
||||||
|
this.tree = newVal
|
||||||
|
},
|
||||||
|
|
||||||
|
selected() {
|
||||||
|
this.$emit("update:modelValue", this.listItems[this.selected].path)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
listItems() {
|
||||||
|
const items = []
|
||||||
|
const getListItems = (node, level) => {
|
||||||
|
items.push({
|
||||||
|
name: node.name,
|
||||||
|
level: level,
|
||||||
|
path: node.path,
|
||||||
|
type: "folder",
|
||||||
|
createNewFolder: node.createNewFolder,
|
||||||
|
newFolder: node.newFolder,
|
||||||
|
open: node.open,
|
||||||
|
})
|
||||||
|
if (node.createNewFolder) {
|
||||||
|
items.push({
|
||||||
|
level: level + 1,
|
||||||
|
type: "new-folder",
|
||||||
|
path: node.path,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (node.open && node.children) {
|
||||||
|
for (const child of node.children) {
|
||||||
|
getListItems(child, level + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getListItems(this.tree, 0)
|
||||||
|
return items
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onKeyDown(event) {
|
||||||
|
//console.log("Keydown", event.key)
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.preventDefault()
|
||||||
|
this.$emit("click")
|
||||||
|
} else if (event.key === "ArrowDown") {
|
||||||
|
event.preventDefault()
|
||||||
|
this.selected = Math.min(this.selected + 1, this.listItems.length - 1)
|
||||||
|
} else if (event.key === "ArrowUp") {
|
||||||
|
event.preventDefault()
|
||||||
|
this.selected = Math.max(this.selected - 1, 0)
|
||||||
|
} else if (event.key === "ArrowRight") {
|
||||||
|
event.preventDefault()
|
||||||
|
const node = this.getNode(this.listItems[this.selected].path)
|
||||||
|
node.open = true
|
||||||
|
} else if (event.key === "ArrowLeft") {
|
||||||
|
event.preventDefault()
|
||||||
|
const node = this.getNode(this.listItems[this.selected].path)
|
||||||
|
node.open = false
|
||||||
|
} else if (event.key === "+") {
|
||||||
|
event.preventDefault()
|
||||||
|
this.newFolderDialog(this.listItems[this.selected].path)
|
||||||
|
} else if (event.key === "-") {
|
||||||
|
event.preventDefault()
|
||||||
|
this.removeNewFolder(this.listItems[this.selected].path)
|
||||||
|
} else if (event.key === "PageDown") {
|
||||||
|
event.preventDefault()
|
||||||
|
this.selected = Math.min(this.selected + this.pageCount(), this.listItems.length - 1)
|
||||||
|
} else if (event.key === "PageUp") {
|
||||||
|
event.preventDefault()
|
||||||
|
this.selected = Math.max(this.selected - this.pageCount(), 0)
|
||||||
|
} else {
|
||||||
|
if (event.key.length === 1) {
|
||||||
|
this.filter += event.key
|
||||||
|
if (this.filter === "") {
|
||||||
|
this.filterSearchStart = this.selected
|
||||||
|
}
|
||||||
|
let idx = this.listItems.findIndex((item, idx) => idx > this.filterSearchStart && item.name.toLowerCase().startsWith(this.filter))
|
||||||
|
if (idx === -1) {
|
||||||
|
idx = this.listItems.findIndex((item, idx) => idx < this.filterSearchStart && item.name.toLowerCase().startsWith(this.filter))
|
||||||
|
}
|
||||||
|
if (idx !== -1) {
|
||||||
|
this.selected = idx
|
||||||
|
this.scheduleFilterReset()
|
||||||
|
} else {
|
||||||
|
this.filter = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
scheduleFilterReset() {
|
||||||
|
if (this.filterTimeout) {
|
||||||
|
clearTimeout(this.filterTimeout)
|
||||||
|
this.filterTimeout = null
|
||||||
|
}
|
||||||
|
this.filterTimeout = setTimeout(() => {
|
||||||
|
this.filter = ""
|
||||||
|
this.filterSearchStart = 0
|
||||||
|
}, 1000)
|
||||||
|
},
|
||||||
|
|
||||||
|
newFolderDialog(parentPath) {
|
||||||
|
//console.log("Create new folder in", parentPath)
|
||||||
|
const node = this.getNode(parentPath)
|
||||||
|
node.createNewFolder = true
|
||||||
|
node.open = true
|
||||||
|
},
|
||||||
|
|
||||||
|
createNewFolder(parentPath, name) {
|
||||||
|
//console.log("Create new folder", name, "in", parentPath)
|
||||||
|
const node = this.getNode(parentPath)
|
||||||
|
node.createNewFolder = false
|
||||||
|
node.children.unshift({
|
||||||
|
name: name,
|
||||||
|
path: parentPath === "" ? name : parentPath + pathSep + name,
|
||||||
|
children: [],
|
||||||
|
newFolder: true,
|
||||||
|
})
|
||||||
|
this.selected++
|
||||||
|
this.$refs.container.focus()
|
||||||
|
},
|
||||||
|
|
||||||
|
cancelNewFolder(path) {
|
||||||
|
//console.log("Cancel new folder in", path)
|
||||||
|
const node = this.getNode(path)
|
||||||
|
node.createNewFolder = false
|
||||||
|
this.$refs.container.focus()
|
||||||
|
},
|
||||||
|
|
||||||
|
removeNewFolder(path) {
|
||||||
|
//console.log("Remove newly created folder:", path)
|
||||||
|
const node = this.getNode(path)
|
||||||
|
if (node.newFolder && path) {
|
||||||
|
const parentPath = path.split(pathSep).slice(0, -1).join(pathSep)
|
||||||
|
const parent = this.getNode(parentPath)
|
||||||
|
parent.children = parent.children.filter(child => child.path !== path)
|
||||||
|
this.selected--
|
||||||
|
}
|
||||||
|
this.$refs.container.focus()
|
||||||
|
},
|
||||||
|
|
||||||
|
getNode(path) {
|
||||||
|
const getNodeFromList = (list, part) => list.find(node => node.name === part)
|
||||||
|
const parts = path.split(pathSep)
|
||||||
|
let currentLevel = this.tree
|
||||||
|
for (const part of parts) {
|
||||||
|
const node = getNodeFromList(currentLevel.children, part)
|
||||||
|
if (!node) {
|
||||||
|
return currentLevel
|
||||||
|
}
|
||||||
|
currentLevel = node
|
||||||
|
}
|
||||||
|
return currentLevel
|
||||||
|
},
|
||||||
|
|
||||||
|
pageCount() {
|
||||||
|
return Math.max(1, Math.floor(this.$refs.container.clientHeight / 24) - 1)
|
||||||
|
},
|
||||||
|
|
||||||
|
folderClick(idx) {
|
||||||
|
const node = this.getNode(this.listItems[idx].path)
|
||||||
|
node.open = !node.open
|
||||||
|
this.selected = idx
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<button
|
||||||
|
class="folder-select-container"
|
||||||
|
ref="container"
|
||||||
|
@keydown="onKeyDown"
|
||||||
|
@click.stop.prevent="()=>{}"
|
||||||
|
>
|
||||||
|
<!--<div class="folder root selected">
|
||||||
|
Heynote Root
|
||||||
|
</div>
|
||||||
|
<div class="folder indent">New Folder…</div>-->
|
||||||
|
|
||||||
|
<template v-for="(item, idx) in listItems">
|
||||||
|
<FolderItem
|
||||||
|
v-if="item.type === 'folder'"
|
||||||
|
:name="item.name"
|
||||||
|
:level="item.level"
|
||||||
|
:selected="idx === selected && !item.createNewFolder"
|
||||||
|
:newFolder="item.newFolder"
|
||||||
|
:open="item.open"
|
||||||
|
@click="folderClick(idx)"
|
||||||
|
@new-folder="newFolderDialog(item.path)"
|
||||||
|
/>
|
||||||
|
<NewFolderItem
|
||||||
|
v-else-if="item.type === 'new-folder'"
|
||||||
|
:parentPath="item.path"
|
||||||
|
:level="item.level"
|
||||||
|
@cancel="() => cancelNewFolder(item.path)"
|
||||||
|
@create-folder="createNewFolder"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="sass" scoped>
|
||||||
|
.folder-select-container
|
||||||
|
width: 100%
|
||||||
|
overflow-y: auto
|
||||||
|
background: #fff
|
||||||
|
border: 1px solid #ccc
|
||||||
|
border-radius: 2px
|
||||||
|
padding: 5px 5px
|
||||||
|
text-align: left
|
||||||
|
&:focus, &:focus-within
|
||||||
|
outline: none
|
||||||
|
border: 1px solid #fff
|
||||||
|
outline: 2px solid #48b57e
|
||||||
|
+dark-mode
|
||||||
|
background: #262626
|
||||||
|
border: 1px solid #363636
|
||||||
|
</style>
|
110
src/components/folder-selector/NewFolderItem.vue
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<script>
|
||||||
|
import sanitizeFilename from "./sanitize-filename.js"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
parentPath: String,
|
||||||
|
level: Number,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
name: "",
|
||||||
|
eventTriggered: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.$refs.input.focus()
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
className() {
|
||||||
|
return {
|
||||||
|
folder: true,
|
||||||
|
selected: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
style() {
|
||||||
|
return {
|
||||||
|
"--indent-level": this.level,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onKeyDown(event) {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
this.finish()
|
||||||
|
} else if (event.key === "Escape") {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
this.name = ""
|
||||||
|
this.finish()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
finish() {
|
||||||
|
if (this.eventTriggered) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.eventTriggered = true
|
||||||
|
if (this.name === "") {
|
||||||
|
this.$emit("cancel")
|
||||||
|
} else {
|
||||||
|
this.$emit("create-folder", this.parentPath, sanitizeFilename(this.name, "_"))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="className"
|
||||||
|
:style="style"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
v-model="name"
|
||||||
|
ref="input"
|
||||||
|
placeholder="New folder name"
|
||||||
|
maxlength="60"
|
||||||
|
@keydown.stop="onKeyDown"
|
||||||
|
@blur="finish"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="sass" scoped>
|
||||||
|
.folder
|
||||||
|
padding: 3px 6px
|
||||||
|
font-size: 13px
|
||||||
|
padding-left: calc(0px + var(--indent-level) * 16px)
|
||||||
|
display: flex
|
||||||
|
background: #f1f1f1
|
||||||
|
+dark-mode
|
||||||
|
background-color: #39393a
|
||||||
|
|
||||||
|
|
||||||
|
input
|
||||||
|
width: 100%
|
||||||
|
background: #fff
|
||||||
|
border: none
|
||||||
|
border-radius: 2px
|
||||||
|
font-size: 13px
|
||||||
|
height: 16px
|
||||||
|
padding: 2px 4px
|
||||||
|
font-style: italic
|
||||||
|
border: 2px solid #48b57e
|
||||||
|
&:focus
|
||||||
|
outline: none
|
||||||
|
&::placeholder
|
||||||
|
font-size: 12px
|
||||||
|
+dark-mode
|
||||||
|
background: #3b3b3b
|
||||||
|
|
||||||
|
</style>
|
14
src/components/folder-selector/sanitize-filename.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
const illegalRe = /[\/\?<>\\:\*\|"]/g;
|
||||||
|
const controlRe = /[\x00-\x1f\x80-\x9f]/g;
|
||||||
|
const reservedRe = /^\.+$/;
|
||||||
|
const windowsReservedRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i;
|
||||||
|
const windowsTrailingRe = /[\. ]+$/;
|
||||||
|
|
||||||
|
export default function sanitizeFilename(input, replacement) {
|
||||||
|
return input.trim()
|
||||||
|
.replace(illegalRe, replacement)
|
||||||
|
.replace(controlRe, replacement)
|
||||||
|
.replace(reservedRe, replacement)
|
||||||
|
.replace(windowsReservedRe, replacement)
|
||||||
|
.replace(windowsTrailingRe, replacement)
|
||||||
|
}
|
211
src/components/settings/AddKeyBind.vue
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
<script>
|
||||||
|
import fuzzysort from 'fuzzysort'
|
||||||
|
import AutoComplete from 'primevue/autocomplete'
|
||||||
|
|
||||||
|
import { HEYNOTE_COMMANDS } from '@/src/editor/commands'
|
||||||
|
import RecordKeyInput from './RecordKeyInput.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "AddKeyBind",
|
||||||
|
components: {
|
||||||
|
AutoComplete,
|
||||||
|
RecordKeyInput,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
key: "",
|
||||||
|
command: "",
|
||||||
|
commandSuggestions: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
commands() {
|
||||||
|
|
||||||
|
return Object.entries(HEYNOTE_COMMANDS).map(([key, cmd]) => {
|
||||||
|
const description = cmd.category + ": " + cmd.description
|
||||||
|
return {
|
||||||
|
name: key,
|
||||||
|
category: cmd.category,
|
||||||
|
description: description,
|
||||||
|
key: cmd.description,
|
||||||
|
label: description,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
window.addEventListener("keydown", this.onKeyDown)
|
||||||
|
this.$refs.keys.$el.focus()
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
window.removeEventListener("keydown", this.onKeyDown)
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onKeyDown(event) {
|
||||||
|
if (event.key === "Escape" && document.activeElement !== this.$refs.keys.$el) {
|
||||||
|
this.$emit("close")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onCommandSearch(event) {
|
||||||
|
if (event.query === "") {
|
||||||
|
this.commandSuggestions = [...this.commands]
|
||||||
|
} else {
|
||||||
|
const searchResults = fuzzysort.go(event.query, this.commands, {
|
||||||
|
keys: ["description", "name"],
|
||||||
|
})
|
||||||
|
this.commandSuggestions = searchResults.map((result) => {
|
||||||
|
const obj = {...result.obj}
|
||||||
|
const nameHighlight = result[0].highlight("<b>", "</b>")
|
||||||
|
obj.label = nameHighlight !== "" ? nameHighlight : obj.description
|
||||||
|
return obj
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onSave() {
|
||||||
|
if (this.key === "" || this.command === "") {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$emit("save", {
|
||||||
|
key: this.key,
|
||||||
|
command: this.command.name,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
focusCommandSelector() {
|
||||||
|
this.$refs.autocomplete.$el.querySelector("input").focus()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="container add-key-binding-dialog">
|
||||||
|
<div class="dialog">
|
||||||
|
<div class="dialog-content">
|
||||||
|
<h3>Add key binding</h3>
|
||||||
|
<div class="form">
|
||||||
|
<div class="field">
|
||||||
|
<label>Key</label>
|
||||||
|
<RecordKeyInput
|
||||||
|
v-model="key"
|
||||||
|
@enter="focusCommandSelector"
|
||||||
|
@close="$emit('close')"
|
||||||
|
ref="keys"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Command</label>
|
||||||
|
<AutoComplete
|
||||||
|
dropdown
|
||||||
|
forceSelection
|
||||||
|
v-model="command"
|
||||||
|
:suggestions="commandSuggestions"
|
||||||
|
:autoOptionFocus="true"
|
||||||
|
optionLabel="key"
|
||||||
|
:delay="0"
|
||||||
|
@complete="onCommandSearch"
|
||||||
|
ref="autocomplete"
|
||||||
|
emptySearchMessage="No commands found"
|
||||||
|
class="command-autocomplete"
|
||||||
|
>
|
||||||
|
<template #option="slotProps">
|
||||||
|
<div class="command-option">
|
||||||
|
<span v-html="slotProps.option.label" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</AutoComplete>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<button
|
||||||
|
@click="onSave"
|
||||||
|
class="save"
|
||||||
|
>Save</button>
|
||||||
|
<button
|
||||||
|
@click="$emit('close')"
|
||||||
|
class="cancel"
|
||||||
|
>Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="sass" scoped>
|
||||||
|
.container
|
||||||
|
position: absolute
|
||||||
|
top: 0
|
||||||
|
left: 0
|
||||||
|
right: 0
|
||||||
|
bottom: 0
|
||||||
|
background: rgba(255,255,255, 0.7)
|
||||||
|
+dark-mode
|
||||||
|
background: rgba(51,51,51, 0.7)
|
||||||
|
|
||||||
|
.dialog
|
||||||
|
width: 400px
|
||||||
|
position: absolute
|
||||||
|
top: 50%
|
||||||
|
left: 50%
|
||||||
|
transform: translate(-50%, -50%)
|
||||||
|
display: flex
|
||||||
|
flex-direction: column
|
||||||
|
background: #fff
|
||||||
|
border-radius: 5px
|
||||||
|
box-shadow: 0 5px 30px rgba(0,0,0, 0.3)
|
||||||
|
border: 2px solid #c0c0c0
|
||||||
|
+dark-mode
|
||||||
|
background: #333
|
||||||
|
box-shadow: 0 5px 30px rgba(0,0,0, 0.7)
|
||||||
|
border: 2px solid #555
|
||||||
|
.dialog-content
|
||||||
|
flex-grow: 1
|
||||||
|
padding: 20px
|
||||||
|
h3
|
||||||
|
font-size: 14px
|
||||||
|
font-weight: 600
|
||||||
|
text-align: center
|
||||||
|
margin: 0
|
||||||
|
margin-bottom: 30px
|
||||||
|
.form
|
||||||
|
//display: flex
|
||||||
|
.field
|
||||||
|
//width: 50%
|
||||||
|
margin-bottom: 20px
|
||||||
|
&:last-child
|
||||||
|
margin-bottom: 0
|
||||||
|
label
|
||||||
|
display: block
|
||||||
|
margin-bottom: 8px
|
||||||
|
input.keys
|
||||||
|
width: 100%
|
||||||
|
padding: 4px
|
||||||
|
border-radius: 2px
|
||||||
|
border: 1px solid #ccc
|
||||||
|
&:focus
|
||||||
|
border: 1px solid rgba(0,0,0, 0)
|
||||||
|
outline: 2px solid var(--highlight-color)
|
||||||
|
//border: 1px solid var(--highlight-color)
|
||||||
|
+dark-mode
|
||||||
|
background: #202020
|
||||||
|
color: #fff
|
||||||
|
border: 1px solid #5a5a5a
|
||||||
|
&:focus
|
||||||
|
border: 1px solid rgba(0,0,0, 0)
|
||||||
|
.command-autocomplete
|
||||||
|
width: 100%
|
||||||
|
|
||||||
|
.footer
|
||||||
|
padding: 10px
|
||||||
|
background: #f1f1f1
|
||||||
|
border-radius: 0 0 5px 5px
|
||||||
|
text-align: right
|
||||||
|
+dark-mode
|
||||||
|
background: #2c2c2c
|
||||||
|
.cancel
|
||||||
|
float: left
|
||||||
|
</style>
|
105
src/components/settings/KeyBindRow.vue
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<script>
|
||||||
|
import { HEYNOTE_COMMANDS } from '@/src/editor/commands'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: [
|
||||||
|
"keys",
|
||||||
|
"command",
|
||||||
|
"isDefault",
|
||||||
|
"source",
|
||||||
|
],
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
formattedKeys() {
|
||||||
|
return this.keys.replaceAll(
|
||||||
|
"Mod",
|
||||||
|
window.heynote.platform.isMac ? "⌘" : "Ctrl",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
commandLabel() {
|
||||||
|
const cmd = HEYNOTE_COMMANDS[this.command]
|
||||||
|
if (cmd) {
|
||||||
|
return `${cmd.category}: ${cmd.description}`
|
||||||
|
}
|
||||||
|
return HEYNOTE_COMMANDS[this.command]?.description ||this.command
|
||||||
|
},
|
||||||
|
|
||||||
|
className() {
|
||||||
|
return this.isDefault ? "keybind-default" : "keybind-user"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<tr :class="className">
|
||||||
|
<td class="source">
|
||||||
|
{{ source }}
|
||||||
|
</td>
|
||||||
|
<td class="key">
|
||||||
|
<template v-if="keys">
|
||||||
|
{{ formattedKeys }}
|
||||||
|
</template>
|
||||||
|
</td>
|
||||||
|
<td class="command">
|
||||||
|
<span class="command-name">{{ commandLabel }}</span>
|
||||||
|
</td>
|
||||||
|
<td class="actions">
|
||||||
|
<button
|
||||||
|
v-if="!isDefault"
|
||||||
|
@click="$emit('delete')"
|
||||||
|
class="delete"
|
||||||
|
>Delete</button>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td v-if="!isDefault" class="drag-handle"></td>
|
||||||
|
<td v-else></td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="sass" scoped>
|
||||||
|
tr
|
||||||
|
&.overridden
|
||||||
|
text-decoration: line-through
|
||||||
|
color: rgba(0,0,0, 0.4)
|
||||||
|
+dark-mode
|
||||||
|
color: rgba(255,255,255, 0.4)
|
||||||
|
td
|
||||||
|
&.key
|
||||||
|
//letter-spacing: 1px
|
||||||
|
&.command
|
||||||
|
//
|
||||||
|
&.drag-handle
|
||||||
|
width: 24px
|
||||||
|
padding: 0
|
||||||
|
cursor: ns-resize
|
||||||
|
background-color: rgba(0,0,0, 0.02)
|
||||||
|
background-size: 20px
|
||||||
|
background-repeat: no-repeat
|
||||||
|
background-position: center center
|
||||||
|
background-image: url(@/assets/icons/drag-vertical-light.svg)
|
||||||
|
+dark-mode
|
||||||
|
background-color: rgba(0,0,0, 0.08)
|
||||||
|
background-image: url(@/assets/icons/drag-vertical-dark.svg)
|
||||||
|
&:hover
|
||||||
|
background-color: rgba(0,0,0, 0.05)
|
||||||
|
+dark-mode
|
||||||
|
background-color: rgba(0,0,0, 0.25)
|
||||||
|
button.delete
|
||||||
|
padding: 0 10px
|
||||||
|
height: 22px
|
||||||
|
font-size: 12px
|
||||||
|
background: none
|
||||||
|
border: none
|
||||||
|
border-radius: 2px
|
||||||
|
cursor: pointer
|
||||||
|
background: #ddd
|
||||||
|
&:hover
|
||||||
|
background: #ccc
|
||||||
|
+dark-mode
|
||||||
|
background: #555
|
||||||
|
color: #fff
|
||||||
|
&:hover
|
||||||
|
background: #666
|
||||||
|
</style>
|
200
src/components/settings/KeyboardBindings.vue
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
<script>
|
||||||
|
import { mapState} from 'pinia'
|
||||||
|
import draggable from 'vuedraggable'
|
||||||
|
|
||||||
|
import { DEFAULT_KEYMAP, EMACS_KEYMAP } from "@/src/editor/keymap"
|
||||||
|
import { useSettingsStore } from "@/src/stores/settings-store"
|
||||||
|
import KeyBindRow from "./KeyBindRow.vue"
|
||||||
|
import AddKeyBind from "./AddKeyBind.vue"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: [
|
||||||
|
"userKeys",
|
||||||
|
"modelValue",
|
||||||
|
],
|
||||||
|
components: {
|
||||||
|
draggable,
|
||||||
|
KeyBindRow,
|
||||||
|
AddKeyBind,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
keymap: this.modelValue,
|
||||||
|
addKeyBinding: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
addKeyBinding(newValue) {
|
||||||
|
this.$emit("addKeyBindingDialogVisible", newValue)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
...mapState(useSettingsStore, [
|
||||||
|
"settings",
|
||||||
|
]),
|
||||||
|
|
||||||
|
fixedKeymap() {
|
||||||
|
const defaultKeymap = (this.settings.keymap === "emacs" ? EMACS_KEYMAP : []).map((km) => ({
|
||||||
|
key: km.key,
|
||||||
|
command: km.command,
|
||||||
|
isDefault: true,
|
||||||
|
source: "Emacs",
|
||||||
|
}))
|
||||||
|
return defaultKeymap.concat(
|
||||||
|
DEFAULT_KEYMAP.map((km) => ({
|
||||||
|
key: km.key,
|
||||||
|
command: km.command,
|
||||||
|
isDefault: true,
|
||||||
|
source: "Default",
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onDragEnd(event) {
|
||||||
|
this.$emit("update:modelValue", this.keymap)
|
||||||
|
},
|
||||||
|
|
||||||
|
onSaveKeyBinding(event) {
|
||||||
|
this.keymap = [
|
||||||
|
{
|
||||||
|
key: event.key,
|
||||||
|
command: event.command,
|
||||||
|
},
|
||||||
|
...(this.keymap ? this.keymap : []),
|
||||||
|
]
|
||||||
|
//console.log("keymap", this.keymap)
|
||||||
|
this.$emit("update:modelValue", this.keymap)
|
||||||
|
this.addKeyBinding = false
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteKeyBinding(index) {
|
||||||
|
this.keymap = this.keymap.toSpliced(index, 1)
|
||||||
|
this.$emit("update:modelValue", this.keymap)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header" :inert="addKeyBinding">
|
||||||
|
<h2>Keyboard Bindings</h2>
|
||||||
|
<!--<p>User key bindings can be reordered. Bindings that appear first take precedence</p>-->
|
||||||
|
<div class="button-container">
|
||||||
|
<button
|
||||||
|
class="add-keybinding"
|
||||||
|
@click="addKeyBinding = !addKeyBinding"
|
||||||
|
>Add Keybinding</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AddKeyBind
|
||||||
|
v-if="addKeyBinding"
|
||||||
|
@close="addKeyBinding = false"
|
||||||
|
@save="onSaveKeyBinding"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<table :inert="addKeyBinding">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Source</th>
|
||||||
|
<th>Key</th>
|
||||||
|
<th>Command</th>
|
||||||
|
<th></th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<draggable
|
||||||
|
v-model="keymap"
|
||||||
|
tag="tbody"
|
||||||
|
group="keymap"
|
||||||
|
ghost-class="ghost"
|
||||||
|
handle=".drag-handle"
|
||||||
|
@start="drag=true"
|
||||||
|
@end="onDragEnd"
|
||||||
|
item-key="key"
|
||||||
|
>
|
||||||
|
<template #item="{element, index}">
|
||||||
|
<KeyBindRow
|
||||||
|
:keys="element.key"
|
||||||
|
:command="element.command"
|
||||||
|
:isDefault="element.isDefault"
|
||||||
|
:index="index"
|
||||||
|
@delete="deleteKeyBinding(index)"
|
||||||
|
source="User"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
|
<tbody>
|
||||||
|
<KeyBindRow
|
||||||
|
v-for="key in fixedKeymap"
|
||||||
|
:key="key.source + '_' + key.key"
|
||||||
|
:keys="key.key"
|
||||||
|
:command="key.command"
|
||||||
|
:isDefault="key.isDefault"
|
||||||
|
:source="key.source"
|
||||||
|
/>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="sass" scoped>
|
||||||
|
.header
|
||||||
|
display: flex
|
||||||
|
margin-bottom: 12px
|
||||||
|
h2
|
||||||
|
flex-grow: 1
|
||||||
|
font-weight: 600
|
||||||
|
font-size: 14px
|
||||||
|
margin: 0
|
||||||
|
.button-container
|
||||||
|
.add-keybinding
|
||||||
|
font-size: 12px
|
||||||
|
height: 26px
|
||||||
|
cursor: pointer
|
||||||
|
|
||||||
|
|
||||||
|
table
|
||||||
|
width: 100%
|
||||||
|
background: #f1f1f1
|
||||||
|
border: 2px solid #f1f1f1
|
||||||
|
+dark-mode
|
||||||
|
background: #3c3c3c
|
||||||
|
background: #333
|
||||||
|
border: 2px solid #3c3c3c
|
||||||
|
::v-deep(tr)
|
||||||
|
background: #fff
|
||||||
|
border-bottom: 2px solid #f1f1f1
|
||||||
|
+dark-mode
|
||||||
|
background: #333
|
||||||
|
border-bottom: 2px solid #3c3c3c
|
||||||
|
&.ghost
|
||||||
|
background: #48b57e
|
||||||
|
color: #fff
|
||||||
|
+dark-mode
|
||||||
|
background: #1b6540
|
||||||
|
th
|
||||||
|
text-align: left
|
||||||
|
font-weight: 600
|
||||||
|
th, td
|
||||||
|
padding: 8px
|
||||||
|
&.actions
|
||||||
|
padding: 6px
|
||||||
|
button
|
||||||
|
height: 20px
|
||||||
|
font-size: 11px
|
||||||
|
|
||||||
|
tbody
|
||||||
|
margin-bottom: 20px
|
||||||
|
|
||||||
|
</style>
|
88
src/components/settings/RecordKeyInput.vue
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<script>
|
||||||
|
import { keyName, base } from "w3c-keyname"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: [
|
||||||
|
"modelValue",
|
||||||
|
],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
keys: this.modelValue ? this.modelValue.split(" ") : [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
key() {
|
||||||
|
return this.keys.join(" ")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
modelValue(newValue) {
|
||||||
|
this.keys = this.modelValue ? this.modelValue.split(" ") : []
|
||||||
|
},
|
||||||
|
key(newValue) {
|
||||||
|
this.$emit("update:model-value", newValue)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onKeyDown(event) {
|
||||||
|
event.preventDefault()
|
||||||
|
//console.log("event", event, event.code, keyName(event))
|
||||||
|
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
this.$emit("enter")
|
||||||
|
} else if (event.key === "Escape") {
|
||||||
|
if (this.keys.length > 0) {
|
||||||
|
this.keys = []
|
||||||
|
} else {
|
||||||
|
// setTimeout is used to ensure that the settings dialog's keydown listener
|
||||||
|
// doesn't close the whole settings dialog
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$emit("close")
|
||||||
|
}, 0)
|
||||||
|
}
|
||||||
|
} else if (["Alt", "Control", "Meta", "Shift"].includes(event.key)) {
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if (this.keys.length >= 2) {
|
||||||
|
this.keys = []
|
||||||
|
}
|
||||||
|
let keyCombo = ""
|
||||||
|
if (event.altKey) {
|
||||||
|
keyCombo += "Alt-"
|
||||||
|
}
|
||||||
|
if (event.ctrlKey) {
|
||||||
|
keyCombo += "Control-"
|
||||||
|
}
|
||||||
|
if (event.metaKey) {
|
||||||
|
keyCombo += "Meta-"
|
||||||
|
}
|
||||||
|
if (event.shiftKey) {
|
||||||
|
keyCombo += "Shift-"
|
||||||
|
}
|
||||||
|
let key = base[event.keyCode]
|
||||||
|
if (key) {
|
||||||
|
if (key === " ") {
|
||||||
|
key = "Space"
|
||||||
|
}
|
||||||
|
keyCombo += key
|
||||||
|
this.keys.push(keyCombo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
:value="key"
|
||||||
|
@keydown.prevent="onKeyDown"
|
||||||
|
class="keys"
|
||||||
|
readonly
|
||||||
|
>
|
||||||
|
</template>
|
@ -1,29 +1,39 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { toRaw} from 'vue';
|
||||||
|
import { LANGUAGES } from '../../editor/languages.js'
|
||||||
|
|
||||||
import KeyboardHotkey from "./KeyboardHotkey.vue"
|
import KeyboardHotkey from "./KeyboardHotkey.vue"
|
||||||
import TabListItem from "./TabListItem.vue"
|
import TabListItem from "./TabListItem.vue"
|
||||||
import TabContent from "./TabContent.vue"
|
import TabContent from "./TabContent.vue"
|
||||||
|
import KeyboardBindings from './KeyboardBindings.vue'
|
||||||
|
|
||||||
const defaultFontFamily = window.heynote.defaultFontFamily
|
const defaultFontFamily = window.heynote.defaultFontFamily
|
||||||
const defaultFontSize = window.heynote.defaultFontSize
|
const defaultFontSize = window.heynote.defaultFontSize
|
||||||
|
const defaultDefaultBlockLanguage = "text"
|
||||||
|
const defaultDefaultBlockLanguageAutoDetect = true
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
initialKeymap: String,
|
initialKeymap: String,
|
||||||
initialSettings: Object,
|
initialSettings: Object,
|
||||||
|
themeSetting: String,
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
KeyboardHotkey,
|
KeyboardHotkey,
|
||||||
TabListItem,
|
TabListItem,
|
||||||
TabContent,
|
TabContent,
|
||||||
|
KeyboardBindings,
|
||||||
},
|
},
|
||||||
|
|
||||||
data() {
|
data() {
|
||||||
|
//console.log("settings:", this.initialSettings)
|
||||||
return {
|
return {
|
||||||
keymaps: [
|
keymaps: [
|
||||||
{ name: "Default", value: "default" },
|
{ name: "Default", value: "default" },
|
||||||
{ name: "Emacs", value: "emacs" },
|
{ name: "Emacs", value: "emacs" },
|
||||||
],
|
],
|
||||||
keymap: this.initialSettings.keymap,
|
keymap: this.initialSettings.keymap,
|
||||||
|
keyBindings: this.initialSettings.keyBindings,
|
||||||
metaKey: this.initialSettings.emacsMetaKey,
|
metaKey: this.initialSettings.emacsMetaKey,
|
||||||
isMac: window.heynote.platform.isMac,
|
isMac: window.heynote.platform.isMac,
|
||||||
showLineNumberGutter: this.initialSettings.showLineNumberGutter,
|
showLineNumberGutter: this.initialSettings.showLineNumberGutter,
|
||||||
@ -35,10 +45,21 @@
|
|||||||
showInMenu: this.initialSettings.showInMenu,
|
showInMenu: this.initialSettings.showInMenu,
|
||||||
alwaysOnTop: this.initialSettings.alwaysOnTop,
|
alwaysOnTop: this.initialSettings.alwaysOnTop,
|
||||||
bracketClosing: this.initialSettings.bracketClosing,
|
bracketClosing: this.initialSettings.bracketClosing,
|
||||||
|
tabSize: this.initialSettings.tabSize || 4,
|
||||||
autoUpdate: this.initialSettings.autoUpdate,
|
autoUpdate: this.initialSettings.autoUpdate,
|
||||||
bufferPath: this.initialSettings.bufferPath,
|
bufferPath: this.initialSettings.bufferPath,
|
||||||
fontFamily: this.initialSettings.fontFamily || defaultFontFamily,
|
fontFamily: this.initialSettings.fontFamily || defaultFontFamily,
|
||||||
fontSize: this.initialSettings.fontSize || defaultFontSize,
|
fontSize: this.initialSettings.fontSize || defaultFontSize,
|
||||||
|
languageOptions: LANGUAGES.map(l => {
|
||||||
|
return {
|
||||||
|
"value": l.token,
|
||||||
|
"name": l.token == "text" ? l.name + " (default)" : l.name,
|
||||||
|
}
|
||||||
|
}).sort((a, b) => {
|
||||||
|
return a.name.localeCompare(b.name)
|
||||||
|
}),
|
||||||
|
defaultBlockLanguage: this.initialSettings.defaultBlockLanguage || defaultDefaultBlockLanguage,
|
||||||
|
defaultBlockLanguageAutoDetect: this.initialSettings.defaultBlockLanguageAutoDetect === false ? false : defaultDefaultBlockLanguageAutoDetect,
|
||||||
|
|
||||||
activeTab: "general",
|
activeTab: "general",
|
||||||
isWebApp: window.heynote.platform.isWebApp,
|
isWebApp: window.heynote.platform.isWebApp,
|
||||||
@ -46,28 +67,37 @@
|
|||||||
systemFonts: [[defaultFontFamily, defaultFontFamily + " (default)"]],
|
systemFonts: [[defaultFontFamily, defaultFontFamily + " (default)"]],
|
||||||
defaultFontSize: defaultFontSize,
|
defaultFontSize: defaultFontSize,
|
||||||
appVersion: "",
|
appVersion: "",
|
||||||
|
theme: this.themeSetting,
|
||||||
|
|
||||||
|
// tracks if the add key binding dialog is visible (so that we can set inert on the save button)
|
||||||
|
addKeyBindingDialogVisible: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
|
window.addEventListener("keydown", this.onKeyDown);
|
||||||
|
|
||||||
|
this.appVersion = await window.heynote.getVersion()
|
||||||
|
|
||||||
if (window.queryLocalFonts !== undefined) {
|
if (window.queryLocalFonts !== undefined) {
|
||||||
let localFonts = [... new Set((await window.queryLocalFonts()).map(f => f.family))].filter(f => f !== "Hack")
|
let localFonts = [... new Set((await window.queryLocalFonts()).map(f => f.family))].filter(f => f !== "Hack")
|
||||||
localFonts = [...new Set(localFonts)].map(f => [f, f])
|
localFonts = [...new Set(localFonts)].map(f => [f, f])
|
||||||
this.systemFonts = [[defaultFontFamily, defaultFontFamily + " (default)"], ...localFonts]
|
this.systemFonts = [[defaultFontFamily, defaultFontFamily + " (default)"], ...localFonts]
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("keydown", this.onKeyDown);
|
|
||||||
this.$refs.keymapSelector.focus()
|
|
||||||
|
|
||||||
this.appVersion = await window.heynote.getVersion()
|
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
window.removeEventListener("keydown", this.onKeyDown);
|
window.removeEventListener("keydown", this.onKeyDown);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
keyBindings(newKeyBindings) {
|
||||||
|
this.updateSettings()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
onKeyDown(event) {
|
onKeyDown(event) {
|
||||||
if (event.key === "Escape") {
|
if (event.key === "Escape" && !this.addKeyBindingDialogVisible) {
|
||||||
this.$emit("closeSettings")
|
this.$emit("closeSettings")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -77,6 +107,7 @@
|
|||||||
showLineNumberGutter: this.showLineNumberGutter,
|
showLineNumberGutter: this.showLineNumberGutter,
|
||||||
showFoldGutter: this.showFoldGutter,
|
showFoldGutter: this.showFoldGutter,
|
||||||
keymap: this.keymap,
|
keymap: this.keymap,
|
||||||
|
keyBindings: this.keyBindings.map((kb) => toRaw(kb)),
|
||||||
emacsMetaKey: window.heynote.platform.isMac ? this.metaKey : "alt",
|
emacsMetaKey: window.heynote.platform.isMac ? this.metaKey : "alt",
|
||||||
allowBetaVersions: this.allowBetaVersions,
|
allowBetaVersions: this.allowBetaVersions,
|
||||||
enableGlobalHotkey: this.enableGlobalHotkey,
|
enableGlobalHotkey: this.enableGlobalHotkey,
|
||||||
@ -86,13 +117,19 @@
|
|||||||
alwaysOnTop: this.alwaysOnTop,
|
alwaysOnTop: this.alwaysOnTop,
|
||||||
autoUpdate: this.autoUpdate,
|
autoUpdate: this.autoUpdate,
|
||||||
bracketClosing: this.bracketClosing,
|
bracketClosing: this.bracketClosing,
|
||||||
|
tabSize: this.tabSize,
|
||||||
bufferPath: this.bufferPath,
|
bufferPath: this.bufferPath,
|
||||||
fontFamily: this.fontFamily === defaultFontFamily ? undefined : this.fontFamily,
|
fontFamily: this.fontFamily === defaultFontFamily ? undefined : this.fontFamily,
|
||||||
fontSize: this.fontSize === defaultFontSize ? undefined : this.fontSize,
|
fontSize: this.fontSize === defaultFontSize ? undefined : this.fontSize,
|
||||||
|
defaultBlockLanguage: this.defaultBlockLanguage === "text" ? undefined : this.defaultBlockLanguage,
|
||||||
|
defaultBlockLanguageAutoDetect: this.defaultBlockLanguageAutoDetect === true ? undefined : this.defaultBlockLanguageAutoDetect,
|
||||||
})
|
})
|
||||||
if (!this.showInDock) {
|
if (!this.showInDock) {
|
||||||
this.showInMenu = true
|
this.showInMenu = true
|
||||||
}
|
}
|
||||||
|
if (this.theme != this.themeSetting) {
|
||||||
|
this.$emit("setTheme", this.theme)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async selectBufferLocation() {
|
async selectBufferLocation() {
|
||||||
@ -138,6 +175,12 @@
|
|||||||
:activeTab="activeTab"
|
:activeTab="activeTab"
|
||||||
@click="activeTab = 'appearance'"
|
@click="activeTab = 'appearance'"
|
||||||
/>
|
/>
|
||||||
|
<TabListItem
|
||||||
|
name="Key Bindings"
|
||||||
|
tab="keyboard-bindings"
|
||||||
|
:activeTab="activeTab"
|
||||||
|
@click="activeTab = 'keyboard-bindings'"
|
||||||
|
/>
|
||||||
<TabListItem
|
<TabListItem
|
||||||
:name="isWebApp ? 'Version' : 'Updates'"
|
:name="isWebApp ? 'Version' : 'Updates'"
|
||||||
tab="updates"
|
tab="updates"
|
||||||
@ -148,23 +191,6 @@
|
|||||||
</nav>
|
</nav>
|
||||||
<div class="settings-content">
|
<div class="settings-content">
|
||||||
<TabContent tab="general" :activeTab="activeTab">
|
<TabContent tab="general" :activeTab="activeTab">
|
||||||
<div class="row">
|
|
||||||
<div class="entry">
|
|
||||||
<h2>Keymap</h2>
|
|
||||||
<select ref="keymapSelector" v-model="keymap" @change="updateSettings" class="keymap">
|
|
||||||
<template v-for="km in keymaps" :key="km.value">
|
|
||||||
<option :selected="km.value === keymap" :value="km.value">{{ km.name }}</option>
|
|
||||||
</template>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="entry" v-if="keymap === 'emacs' && isMac">
|
|
||||||
<h2>Meta Key</h2>
|
|
||||||
<select v-model="metaKey" @change="updateSettings" class="metaKey">
|
|
||||||
<option :selected="metaKey === 'meta'" value="meta">Command</option>
|
|
||||||
<option :selected="metaKey === 'alt'" value="alt">Option</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row" v-if="!isWebApp">
|
<div class="row" v-if="!isWebApp">
|
||||||
<div class="entry">
|
<div class="entry">
|
||||||
<h2>Global Keyboard Shortcut</h2>
|
<h2>Global Keyboard Shortcut</h2>
|
||||||
@ -221,14 +247,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row" v-if="!isWebApp">
|
<div class="row" v-if="!isWebApp">
|
||||||
<div class="entry buffer-location">
|
<div class="entry buffer-location">
|
||||||
<h2>Buffer File Path</h2>
|
<h2>Buffer Files Path</h2>
|
||||||
<label class="keyboard-shortcut-label">
|
<label class="keyboard-shortcut-label">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
v-model="customBufferLocation"
|
v-model="customBufferLocation"
|
||||||
@change="onCustomBufferLocationChange"
|
@change="onCustomBufferLocationChange"
|
||||||
/>
|
/>
|
||||||
Use custom buffer file location
|
Use custom location for the buffer files
|
||||||
</label>
|
</label>
|
||||||
<div class="file-path">
|
<div class="file-path">
|
||||||
<button
|
<button
|
||||||
@ -255,9 +281,51 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="entry">
|
||||||
|
<h2>Tab Size</h2>
|
||||||
|
<select v-model="tabSize" @change="updateSettings" class="tab-size">
|
||||||
|
<option
|
||||||
|
v-for="size in [1, 2, 3, 4, 5, 6, 7, 8]"
|
||||||
|
:key="size"
|
||||||
|
:selected="tabSize === size"
|
||||||
|
:value="size"
|
||||||
|
>{{ size }} {{ size === 1 ? 'space' : 'spaces' }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="entry">
|
||||||
|
<h2>Default Block Language</h2>
|
||||||
|
<select v-model="defaultBlockLanguage" @change="updateSettings" class="block-language">
|
||||||
|
<template v-for="lang in languageOptions" :key="lang.value">
|
||||||
|
<option :selected="lang.value === defaultBlockLanguage" :value="lang.value">{{ lang.name }}</option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
v-model="defaultBlockLanguageAutoDetect"
|
||||||
|
@change="updateSettings"
|
||||||
|
class="language-auto-detect"
|
||||||
|
/>
|
||||||
|
Auto-detection (default: on)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</TabContent>
|
</TabContent>
|
||||||
|
|
||||||
<TabContent tab="appearance" :activeTab="activeTab">
|
<TabContent tab="appearance" :activeTab="activeTab">
|
||||||
|
<div class="row">
|
||||||
|
<div class="entry">
|
||||||
|
<h2>Color Theme</h2>
|
||||||
|
<select v-model="theme" @change="updateSettings" class="theme">
|
||||||
|
<option :selected="theme === 'system'" value="system">System</option>
|
||||||
|
<option :selected="theme === 'light'" value="light">Light</option>
|
||||||
|
<option :selected="theme === 'dark'" value="dark">Dark</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="entry">
|
<div class="entry">
|
||||||
<h2>Gutters</h2>
|
<h2>Gutters</h2>
|
||||||
@ -304,6 +372,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</TabContent>
|
</TabContent>
|
||||||
|
|
||||||
|
<TabContent tab="keyboard-bindings" :activeTab="activeTab">
|
||||||
|
<div class="row">
|
||||||
|
<div class="entry">
|
||||||
|
<h2>Keymap</h2>
|
||||||
|
<select v-model="keymap" @change="updateSettings" class="keymap">
|
||||||
|
<template v-for="km in keymaps" :key="km.value">
|
||||||
|
<option :selected="km.value === keymap" :value="km.value">{{ km.name }}</option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="entry" v-if="keymap === 'emacs' && isMac">
|
||||||
|
<h2>Meta Key</h2>
|
||||||
|
<select v-model="metaKey" @change="updateSettings" class="metaKey">
|
||||||
|
<option :selected="metaKey === 'meta'" value="meta">Command</option>
|
||||||
|
<option :selected="metaKey === 'alt'" value="alt">Option</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<KeyboardBindings
|
||||||
|
:userKeys="keyBindings ? keyBindings : {}"
|
||||||
|
v-model="keyBindings"
|
||||||
|
@addKeyBindingDialogVisible="addKeyBindingDialogVisible = $event"
|
||||||
|
/>
|
||||||
|
</TabContent>
|
||||||
|
|
||||||
<TabContent tab="updates" :activeTab="activeTab">
|
<TabContent tab="updates" :activeTab="activeTab">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="entry">
|
<div class="entry">
|
||||||
@ -342,7 +435,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bottom-bar">
|
<div class="bottom-bar" :inert="addKeyBindingDialogVisible">
|
||||||
<button
|
<button
|
||||||
@click="$emit('closeSettings')"
|
@click="$emit('closeSettings')"
|
||||||
class="close"
|
class="close"
|
||||||
@ -371,14 +464,16 @@
|
|||||||
background: rgba(0, 0, 0, 0.5)
|
background: rgba(0, 0, 0, 0.5)
|
||||||
|
|
||||||
.dialog
|
.dialog
|
||||||
|
--dialog-height: 600px
|
||||||
|
--bottom-bar-height: 48px
|
||||||
box-sizing: border-box
|
box-sizing: border-box
|
||||||
z-index: 2
|
z-index: 2
|
||||||
position: absolute
|
position: absolute
|
||||||
left: 50%
|
left: 50%
|
||||||
top: 50%
|
top: 50%
|
||||||
transform: translate(-50%, -50%)
|
transform: translate(-50%, -50%)
|
||||||
width: 700px
|
width: 820px
|
||||||
height: 560px
|
height: var(--dialog-height)
|
||||||
max-width: 100%
|
max-width: 100%
|
||||||
max-height: 100%
|
max-height: 100%
|
||||||
display: flex
|
display: flex
|
||||||
@ -398,6 +493,7 @@
|
|||||||
.dialog-content
|
.dialog-content
|
||||||
flex-grow: 1
|
flex-grow: 1
|
||||||
display: flex
|
display: flex
|
||||||
|
height: calc(var(--dialog-height) - var(--bottom-bar-height))
|
||||||
.sidebar
|
.sidebar
|
||||||
box-sizing: border-box
|
box-sizing: border-box
|
||||||
width: 140px
|
width: 140px
|
||||||
@ -415,8 +511,10 @@
|
|||||||
flex-grow: 1
|
flex-grow: 1
|
||||||
padding: 40px
|
padding: 40px
|
||||||
overflow-y: auto
|
overflow-y: auto
|
||||||
|
position: relative
|
||||||
select
|
select
|
||||||
height: 22px
|
height: 22px
|
||||||
|
margin: 4px 0
|
||||||
.row
|
.row
|
||||||
display: flex
|
display: flex
|
||||||
.entry
|
.entry
|
||||||
@ -470,6 +568,8 @@
|
|||||||
background: #222
|
background: #222
|
||||||
color: #aaa
|
color: #aaa
|
||||||
.bottom-bar
|
.bottom-bar
|
||||||
|
box-sizing: border-box
|
||||||
|
height: var(--bottom-bar-height)
|
||||||
border-radius: 0 0 5px 5px
|
border-radius: 0 0 5px 5px
|
||||||
background: #eee
|
background: #eee
|
||||||
text-align: right
|
text-align: right
|
||||||
@ -478,4 +578,5 @@
|
|||||||
background: #222
|
background: #222
|
||||||
.close
|
.close
|
||||||
height: 28px
|
height: 28px
|
||||||
|
cursor: pointer
|
||||||
</style>
|
</style>
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
li
|
li
|
||||||
padding: 9px 20px
|
padding: 9px 20px
|
||||||
font-size: 13px
|
font-size: 13px
|
||||||
|
line-height: 1.3
|
||||||
user-select: none
|
user-select: none
|
||||||
cursor: pointer
|
cursor: pointer
|
||||||
&:hover
|
&:hover
|
||||||
@ -25,10 +26,11 @@
|
|||||||
+dark-mode
|
+dark-mode
|
||||||
background: #292929
|
background: #292929
|
||||||
&.active
|
&.active
|
||||||
background: #48b57e
|
background: var(--highlight-color)
|
||||||
color: #fff
|
color: #fff
|
||||||
cursor: default
|
cursor: default
|
||||||
+dark-mode
|
+dark-mode
|
||||||
background: #1b6540
|
// needed for specificity (to not be overridden by :hover in dark mode)
|
||||||
|
background: var(--highlight-color)
|
||||||
|
|
||||||
</style>
|
</style>
|
@ -1,3 +1,4 @@
|
|||||||
@import "reset"
|
@use "reset"
|
||||||
@import "font"
|
@use "font"
|
||||||
@import "base"
|
@use "base"
|
||||||
|
@use "autocomplete"
|
||||||
|
45
src/css/autocomplete.sass
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
@use "@/src/css/include.sass" as *
|
||||||
|
|
||||||
|
|
||||||
|
.p-component
|
||||||
|
--p-inputtext-background: #fff
|
||||||
|
--p-inputtext-border-color: #ccc
|
||||||
|
--p-inputtext-border-radius: 3px
|
||||||
|
--p-inputtext-padding-y: 4px
|
||||||
|
--p-inputtext-padding-x: 4px
|
||||||
|
--p-inputtext-focus-border-color: var(--p-inputtext-border-color)
|
||||||
|
--p-inputtext-hover-border-color: var(--p-inputtext-border-color)
|
||||||
|
--p-inputtext-active-border-color: var(--p-inputtext-border-color)
|
||||||
|
|
||||||
|
--p-autocomplete-dropdown-border-color: var(--p-inputtext-border-color)
|
||||||
|
--p-autocomplete-dropdown-border-radius: var(--p-inputtext-border-radius)
|
||||||
|
--p-autocomplete-dropdown-hover-border-color: var(--p-inputtext-hover-border-color)
|
||||||
|
--p-autocomplete-dropdown-active-border-color: var(--p-inputtext-active-border-color)
|
||||||
|
--p-autocomplete-dropdown-hover-background: #f1f1f1
|
||||||
|
--p-autocomplete-option-focus-background: var(--highlight-color)
|
||||||
|
--p-autocomplete-option-focus-color: #fff
|
||||||
|
|
||||||
|
+dark-mode
|
||||||
|
--p-inputtext-background: #202020
|
||||||
|
--p-inputtext-border-color: #444
|
||||||
|
--p-autocomplete-dropdown-hover-background: #2a2a2a
|
||||||
|
|
||||||
|
.p-inputtext.p-inputtext
|
||||||
|
font-size: 12px
|
||||||
|
|
||||||
|
.p-autocomplete-list-container
|
||||||
|
background: #fff
|
||||||
|
border: 1px solid #f1f1f1
|
||||||
|
padding: 6px 0
|
||||||
|
border-radius: 3px
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1)
|
||||||
|
+dark-mode
|
||||||
|
color: rgba(255,255,255, 0.8)
|
||||||
|
background: #333
|
||||||
|
border: 1px solid #2a2a2a
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3)
|
||||||
|
|
||||||
|
.p-autocomplete-list > li
|
||||||
|
padding: 6px 10px
|
||||||
|
b
|
||||||
|
font-weight: bold
|
@ -1,10 +1,14 @@
|
|||||||
|
@use "include" as *
|
||||||
|
|
||||||
:root[theme='light']
|
:root[theme='light']
|
||||||
--status-bar-background: #48b57e
|
--status-bar-background: #48b57e
|
||||||
--status-bar-color: #fff
|
--status-bar-color: #fff
|
||||||
|
--highlight-color: #48b57e
|
||||||
|
|
||||||
:root[theme='dark']
|
:root[theme='dark']
|
||||||
--status-bar-background: #0e1217
|
--status-bar-background: #0e1217
|
||||||
--status-bar-color: rgba(255, 255, 255, 0.75)
|
--status-bar-color: rgba(255, 255, 255, 0.75)
|
||||||
|
--highlight-color: #1b6540
|
||||||
|
|
||||||
html
|
html
|
||||||
margin: 0
|
margin: 0
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
font-weight: 400
|
font-weight: 400
|
||||||
font-style: normal
|
font-style: normal
|
||||||
|
|
||||||
|
|
||||||
@font-face
|
@font-face
|
||||||
font-family: 'Hack'
|
font-family: 'Hack'
|
||||||
src: url('@/assets/font/hack/hack-bold.woff2') format('woff2'), url('@/assets/font/hack/hack-bold.woff') format('woff')
|
src: url('@/assets/font/hack/hack-bold.woff2') format('woff2'), url('@/assets/font/hack/hack-bold.woff') format('woff')
|
||||||
@ -23,5 +22,3 @@
|
|||||||
font-weight: 700
|
font-weight: 700
|
||||||
font-style: italic
|
font-style: italic
|
||||||
|
|
||||||
|
|
||||||
@import "@/assets/font/open-sans/open-sans"
|
|
||||||
|
@ -6,3 +6,13 @@ export const CURRENCIES_LOADED = "heynote-currencies-loaded"
|
|||||||
export const SET_CONTENT = "heynote-set-content"
|
export const SET_CONTENT = "heynote-set-content"
|
||||||
export const ADD_NEW_BLOCK = "heynote-add-new-block"
|
export const ADD_NEW_BLOCK = "heynote-add-new-block"
|
||||||
export const MOVE_BLOCK = "heynote-move-block"
|
export const MOVE_BLOCK = "heynote-move-block"
|
||||||
|
export const DELETE_BLOCK = "heynote-delete-block"
|
||||||
|
export const CURSOR_CHANGE = "heynote-cursor-change"
|
||||||
|
export const APPEND_BLOCK = "heynote-append-block"
|
||||||
|
export const SET_FONT = "heynote-set-font"
|
||||||
|
|
||||||
|
|
||||||
|
// This function checks if any of the transactions has the given Heynote annotation
|
||||||
|
export function transactionsHasAnnotation(transactions, annotation) {
|
||||||
|
return transactions.some(tr => tr.annotation(heynoteEvent) === annotation)
|
||||||
|
}
|
||||||
|
126
src/editor/block/block-parsing.js
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import { syntaxTree } from "@codemirror/language"
|
||||||
|
import { Note, Document, NoteDelimiter } from "../lang-heynote/parser.terms.js"
|
||||||
|
import { IterMode } from "@lezer/common";
|
||||||
|
|
||||||
|
// tracks the size of the first delimiter
|
||||||
|
export let firstBlockDelimiterSize
|
||||||
|
|
||||||
|
|
||||||
|
function startTimer() {
|
||||||
|
const timeStart = performance.now();
|
||||||
|
return function () {
|
||||||
|
return Math.round(performance.now() - timeStart);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a list of blocks in the document from the syntax tree.
|
||||||
|
* syntaxTreeAvailable() should have been called before this function to ensure the syntax tree is available.
|
||||||
|
*/
|
||||||
|
export function getBlocksFromSyntaxTree(state) {
|
||||||
|
//const timer = startTimer()
|
||||||
|
const blocks = [];
|
||||||
|
const tree = syntaxTree(state, state.doc.length)
|
||||||
|
if (tree) {
|
||||||
|
tree.iterate({
|
||||||
|
enter: (type) => {
|
||||||
|
if (type.type.id == Document || type.type.id == Note) {
|
||||||
|
return true
|
||||||
|
} else if (type.type.id === NoteDelimiter) {
|
||||||
|
const langNode = type.node.getChild("NoteLanguage")
|
||||||
|
const language = state.doc.sliceString(langNode.from, langNode.to)
|
||||||
|
const isAuto = !!type.node.getChild("Auto")
|
||||||
|
const contentNode = type.node.nextSibling
|
||||||
|
blocks.push({
|
||||||
|
language: {
|
||||||
|
name: language,
|
||||||
|
auto: isAuto,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
from: contentNode.from,
|
||||||
|
to: contentNode.to,
|
||||||
|
},
|
||||||
|
delimiter: {
|
||||||
|
from: type.from,
|
||||||
|
to: type.to,
|
||||||
|
},
|
||||||
|
range: {
|
||||||
|
from: type.node.from,
|
||||||
|
to: contentNode.to,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
mode: IterMode.IgnoreMounts,
|
||||||
|
});
|
||||||
|
firstBlockDelimiterSize = blocks[0]?.delimiter.to
|
||||||
|
}
|
||||||
|
//console.log("getBlocksSyntaxTree took", timer(), "ms")
|
||||||
|
return blocks
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse blocks from document's string contents using String.indexOf()
|
||||||
|
*/
|
||||||
|
export function getBlocksFromString(state) {
|
||||||
|
//const timer = startTimer()
|
||||||
|
const blocks = []
|
||||||
|
const doc = state.doc
|
||||||
|
if (doc.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const content = doc.sliceString(0, doc.length)
|
||||||
|
const delim = "\n∞∞∞"
|
||||||
|
let pos = 0
|
||||||
|
while (pos < doc.length) {
|
||||||
|
const blockStart = content.indexOf(delim, pos);
|
||||||
|
if (blockStart != pos) {
|
||||||
|
console.error("Error parsing blocks, expected delimiter at", pos)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const langStart = blockStart + delim.length;
|
||||||
|
const delimiterEnd = content.indexOf("\n", langStart)
|
||||||
|
if (delimiterEnd < 0) {
|
||||||
|
console.error("Error parsing blocks. Delimiter didn't end with newline")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
const langFull = content.substring(langStart, delimiterEnd);
|
||||||
|
let auto = false;
|
||||||
|
let lang = langFull;
|
||||||
|
if (langFull.endsWith("-a")) {
|
||||||
|
auto = true;
|
||||||
|
lang = langFull.substring(0, langFull.length - 2);
|
||||||
|
}
|
||||||
|
const contentFrom = delimiterEnd + 1;
|
||||||
|
let blockEnd = content.indexOf(delim, contentFrom);
|
||||||
|
if (blockEnd < 0) {
|
||||||
|
blockEnd = doc.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const block = {
|
||||||
|
language: {
|
||||||
|
name: lang,
|
||||||
|
auto: auto,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
from: contentFrom,
|
||||||
|
to: blockEnd,
|
||||||
|
},
|
||||||
|
delimiter: {
|
||||||
|
from: blockStart,
|
||||||
|
to: delimiterEnd + 1,
|
||||||
|
},
|
||||||
|
range: {
|
||||||
|
from: blockStart,
|
||||||
|
to: blockEnd,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
blocks.push(block);
|
||||||
|
pos = blockEnd;
|
||||||
|
}
|
||||||
|
//console.log("getBlocksFromString() took", timer(), "ms")
|
||||||
|
return blocks;
|
||||||
|
}
|
@ -1,63 +1,31 @@
|
|||||||
import { ViewPlugin, EditorView, Decoration, WidgetType, lineNumbers } from "@codemirror/view"
|
import { ViewPlugin, EditorView, Decoration, WidgetType, lineNumbers } from "@codemirror/view"
|
||||||
import { layer, RectangleMarker } from "@codemirror/view"
|
import { layer, RectangleMarker } from "@codemirror/view"
|
||||||
import { EditorState, RangeSetBuilder, StateField, Facet , StateEffect, RangeSet} from "@codemirror/state";
|
import { EditorState, RangeSetBuilder, StateField, RangeSet, Transaction} from "@codemirror/state";
|
||||||
import { syntaxTree, ensureSyntaxTree } from "@codemirror/language"
|
import { syntaxTreeAvailable } from "@codemirror/language"
|
||||||
import { Note, Document, NoteDelimiter } from "../lang-heynote/parser.terms.js"
|
import { useHeynoteStore } from "../../stores/heynote-store.js"
|
||||||
import { IterMode } from "@lezer/common";
|
import { heynoteEvent, LANGUAGE_CHANGE, CURSOR_CHANGE } from "../annotation.js";
|
||||||
import { heynoteEvent, LANGUAGE_CHANGE } from "../annotation.js";
|
|
||||||
import { SelectionChangeEvent } from "../event.js"
|
|
||||||
import { mathBlock } from "./math.js"
|
import { mathBlock } from "./math.js"
|
||||||
import { emptyBlockSelected } from "./select-all.js";
|
import { emptyBlockSelected } from "./select-all.js";
|
||||||
|
import { firstBlockDelimiterSize, getBlocksFromSyntaxTree, getBlocksFromString } from "./block-parsing.js";
|
||||||
|
|
||||||
|
|
||||||
// tracks the size of the first delimiter
|
/**
|
||||||
let firstBlockDelimiterSize
|
* Get the blocks from the document state.
|
||||||
|
* If the syntax tree is available, we'll extract the blocks from that. Otherwise
|
||||||
function getBlocks(state, timeout=50) {
|
* the blocks are parsed from the string contents of the document, which is much faster
|
||||||
const blocks = [];
|
* than waiting for the tree parsing to finish.
|
||||||
const tree = ensureSyntaxTree(state, state.doc.length, timeout)
|
*/
|
||||||
if (tree) {
|
export function getBlocks(state) {
|
||||||
tree.iterate({
|
if (syntaxTreeAvailable(state, state.doc.length)) {
|
||||||
enter: (type) => {
|
return getBlocksFromSyntaxTree(state)
|
||||||
if (type.type.id == Document || type.type.id == Note) {
|
} else {
|
||||||
return true
|
return getBlocksFromString(state)
|
||||||
} else if (type.type.id === NoteDelimiter) {
|
|
||||||
const langNode = type.node.getChild("NoteLanguage")
|
|
||||||
const language = state.doc.sliceString(langNode.from, langNode.to)
|
|
||||||
const isAuto = !!type.node.getChild("Auto")
|
|
||||||
const contentNode = type.node.nextSibling
|
|
||||||
blocks.push({
|
|
||||||
language: {
|
|
||||||
name: language,
|
|
||||||
auto: isAuto,
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
from: contentNode.from,
|
|
||||||
to: contentNode.to,
|
|
||||||
},
|
|
||||||
delimiter: {
|
|
||||||
from: type.from,
|
|
||||||
to: type.to,
|
|
||||||
},
|
|
||||||
range: {
|
|
||||||
from: type.node.from,
|
|
||||||
to: contentNode.to,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
},
|
|
||||||
mode: IterMode.IgnoreMounts,
|
|
||||||
});
|
|
||||||
firstBlockDelimiterSize = blocks[0]?.delimiter.to
|
|
||||||
}
|
|
||||||
return blocks
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const blockState = StateField.define({
|
export const blockState = StateField.define({
|
||||||
create(state) {
|
create(state) {
|
||||||
return getBlocks(state, 1000);
|
return getBlocks(state);
|
||||||
},
|
},
|
||||||
update(blocks, transaction) {
|
update(blocks, transaction) {
|
||||||
// if blocks are empty it likely means we didn't get a parsed syntax tree, and then we want to update
|
// if blocks are empty it likely means we didn't get a parsed syntax tree, and then we want to update
|
||||||
@ -201,8 +169,9 @@ const blockLayer = layer({
|
|||||||
idx++;
|
idx++;
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const fromCoordsTop = view.coordsAtPos(Math.max(block.content.from, view.visibleRanges[0].from)).top
|
// view.coordsAtPos returns null if the editor is not visible
|
||||||
let toCoordsBottom = view.coordsAtPos(Math.min(block.content.to, view.visibleRanges[view.visibleRanges.length - 1].to)).bottom
|
const fromCoordsTop = view.coordsAtPos(Math.max(block.content.from, view.visibleRanges[0].from))?.top
|
||||||
|
let toCoordsBottom = view.coordsAtPos(Math.min(block.content.to, view.visibleRanges[view.visibleRanges.length - 1].to))?.bottom
|
||||||
if (idx === blocks.length - 1) {
|
if (idx === blocks.length - 1) {
|
||||||
// Calculate how much extra height we need to add to the last block
|
// Calculate how much extra height we need to add to the last block
|
||||||
let extraHeight = view.viewState.editorHeight - (
|
let extraHeight = view.viewState.editorHeight - (
|
||||||
@ -313,13 +282,24 @@ function getSelectionSize(state, sel) {
|
|||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
const emitCursorChange = (editor) => ViewPlugin.fromClass(
|
export function triggerCursorChange({state, dispatch}) {
|
||||||
|
// Trigger empty change transaction that is annotated with CURRENCIES_LOADED
|
||||||
|
// This will make Math blocks re-render so that currency conversions are applied
|
||||||
|
dispatch(state.update({
|
||||||
|
changes:{from: 0, to: 0, insert:""},
|
||||||
|
annotations: [heynoteEvent.of(CURSOR_CHANGE), Transaction.addToHistory.of(false)],
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const emitCursorChange = (editor) => {
|
||||||
|
const heynoteStore = useHeynoteStore()
|
||||||
|
return ViewPlugin.fromClass(
|
||||||
class {
|
class {
|
||||||
update(update) {
|
update(update) {
|
||||||
// if the selection changed or the language changed (can happen without selection change),
|
// if the selection changed or the language changed (can happen without selection change),
|
||||||
// emit a selection change event
|
// emit a selection change event
|
||||||
const langChange = update.transactions.some(tr => tr.annotations.some(a => a.value == LANGUAGE_CHANGE))
|
const shouldUpdate = update.transactions.some(tr => tr.annotations.some(a => a.value == LANGUAGE_CHANGE || a.value == CURSOR_CHANGE))
|
||||||
if (update.selectionSet || langChange) {
|
if (update.selectionSet || shouldUpdate) {
|
||||||
const cursorLine = getBlockLineFromPos(update.state, update.state.selection.main.head)
|
const cursorLine = getBlockLineFromPos(update.state, update.state.selection.main.head)
|
||||||
|
|
||||||
const selectionSize = update.state.selection.ranges.map(
|
const selectionSize = update.state.selection.ranges.map(
|
||||||
@ -328,17 +308,17 @@ const emitCursorChange = (editor) => ViewPlugin.fromClass(
|
|||||||
|
|
||||||
const block = getActiveNoteBlock(update.state)
|
const block = getActiveNoteBlock(update.state)
|
||||||
if (block && cursorLine) {
|
if (block && cursorLine) {
|
||||||
editor.element.dispatchEvent(new SelectionChangeEvent({
|
heynoteStore.currentCursorLine = cursorLine
|
||||||
cursorLine,
|
heynoteStore.currentSelectionSize = selectionSize
|
||||||
selectionSize,
|
heynoteStore.currentLanguage = block.language.name
|
||||||
language: block.language.name,
|
heynoteStore.currentLanguageAuto = block.language.auto
|
||||||
languageAuto: block.language.auto,
|
heynoteStore.currentBufferName = editor.name
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export const noteBlockExtension = (editor) => {
|
export const noteBlockExtension = (editor) => {
|
||||||
return [
|
return [
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { EditorSelection } from "@codemirror/state"
|
import { EditorSelection, Transaction } from "@codemirror/state"
|
||||||
import { heynoteEvent, LANGUAGE_CHANGE, CURRENCIES_LOADED, ADD_NEW_BLOCK, MOVE_BLOCK } from "../annotation.js";
|
|
||||||
|
import { heynoteEvent, LANGUAGE_CHANGE, CURRENCIES_LOADED, ADD_NEW_BLOCK, MOVE_BLOCK, DELETE_BLOCK } from "../annotation.js";
|
||||||
import { blockState, getActiveNoteBlock, getFirstNoteBlock, getLastNoteBlock, getNoteBlockFromPos } from "./block"
|
import { blockState, getActiveNoteBlock, getFirstNoteBlock, getLastNoteBlock, getNoteBlockFromPos } from "./block"
|
||||||
import { moveLineDown, moveLineUp } from "./move-lines.js";
|
import { moveLineDown, moveLineUp } from "./move-lines.js";
|
||||||
import { selectAll } from "./select-all.js";
|
import { selectAll } from "./select-all.js";
|
||||||
@ -7,7 +8,11 @@ import { selectAll } from "./select-all.js";
|
|||||||
export { moveLineDown, moveLineUp, selectAll }
|
export { moveLineDown, moveLineUp, selectAll }
|
||||||
|
|
||||||
|
|
||||||
export const insertNewBlockAtCursor = ({ state, dispatch }) => {
|
export function getBlockDelimiter(defaultToken, autoDetect) {
|
||||||
|
return `\n∞∞∞${autoDetect ? defaultToken + '-a' : defaultToken}\n`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const insertNewBlockAtCursor = (editor) => ({ state, dispatch }) => {
|
||||||
if (state.readOnly)
|
if (state.readOnly)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
@ -16,7 +21,7 @@ export const insertNewBlockAtCursor = ({ state, dispatch }) => {
|
|||||||
if (currentBlock) {
|
if (currentBlock) {
|
||||||
delimText = `\n∞∞∞${currentBlock.language.name}${currentBlock.language.auto ? "-a" : ""}\n`
|
delimText = `\n∞∞∞${currentBlock.language.name}${currentBlock.language.auto ? "-a" : ""}\n`
|
||||||
} else {
|
} else {
|
||||||
delimText = "\n∞∞∞text-a\n"
|
delimText = getBlockDelimiter(editor.defaultBlockToken, editor.defaultBlockAutoDetect)
|
||||||
}
|
}
|
||||||
dispatch(state.replaceSelection(delimText),
|
dispatch(state.replaceSelection(delimText),
|
||||||
{
|
{
|
||||||
@ -28,13 +33,12 @@ export const insertNewBlockAtCursor = ({ state, dispatch }) => {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addNewBlockBeforeCurrent = ({ state, dispatch }) => {
|
export const addNewBlockBeforeCurrent = (editor) => ({ state, dispatch }) => {
|
||||||
console.log("addNewBlockBeforeCurrent")
|
|
||||||
if (state.readOnly)
|
if (state.readOnly)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
const block = getActiveNoteBlock(state)
|
const block = getActiveNoteBlock(state)
|
||||||
const delimText = "\n∞∞∞text-a\n"
|
const delimText = getBlockDelimiter(editor.defaultBlockToken, editor.defaultBlockAutoDetect)
|
||||||
|
|
||||||
dispatch(state.update({
|
dispatch(state.update({
|
||||||
changes: {
|
changes: {
|
||||||
@ -50,12 +54,12 @@ export const addNewBlockBeforeCurrent = ({ state, dispatch }) => {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addNewBlockAfterCurrent = ({ state, dispatch }) => {
|
export const addNewBlockAfterCurrent = (editor) => ({ state, dispatch }) => {
|
||||||
if (state.readOnly)
|
if (state.readOnly)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
const block = getActiveNoteBlock(state)
|
const block = getActiveNoteBlock(state)
|
||||||
const delimText = "\n∞∞∞text-a\n"
|
const delimText = getBlockDelimiter(editor.defaultBlockToken, editor.defaultBlockAutoDetect)
|
||||||
|
|
||||||
dispatch(state.update({
|
dispatch(state.update({
|
||||||
changes: {
|
changes: {
|
||||||
@ -70,12 +74,12 @@ export const addNewBlockAfterCurrent = ({ state, dispatch }) => {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addNewBlockBeforeFirst = ({ state, dispatch }) => {
|
export const addNewBlockBeforeFirst = (editor) => ({ state, dispatch }) => {
|
||||||
if (state.readOnly)
|
if (state.readOnly)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
const block = getFirstNoteBlock(state)
|
const block = getFirstNoteBlock(state)
|
||||||
const delimText = "\n∞∞∞text-a\n"
|
const delimText = getBlockDelimiter(editor.defaultBlockToken, editor.defaultBlockAutoDetect)
|
||||||
|
|
||||||
dispatch(state.update({
|
dispatch(state.update({
|
||||||
changes: {
|
changes: {
|
||||||
@ -91,11 +95,11 @@ export const addNewBlockBeforeFirst = ({ state, dispatch }) => {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const addNewBlockAfterLast = ({ state, dispatch }) => {
|
export const addNewBlockAfterLast = (editor) => ({ state, dispatch }) => {
|
||||||
if (state.readOnly)
|
if (state.readOnly)
|
||||||
return false
|
return false
|
||||||
const block = getLastNoteBlock(state)
|
const block = getLastNoteBlock(state)
|
||||||
const delimText = "\n∞∞∞text-a\n"
|
const delimText = getBlockDelimiter(editor.defaultBlockToken, editor.defaultBlockAutoDetect)
|
||||||
|
|
||||||
dispatch(state.update({
|
dispatch(state.update({
|
||||||
changes: {
|
changes: {
|
||||||
@ -131,6 +135,10 @@ export function changeLanguageTo(state, dispatch, block, language, auto) {
|
|||||||
|
|
||||||
export function changeCurrentBlockLanguage(state, dispatch, language, auto) {
|
export function changeCurrentBlockLanguage(state, dispatch, language, auto) {
|
||||||
const block = getActiveNoteBlock(state)
|
const block = getActiveNoteBlock(state)
|
||||||
|
// if language is null, we only want to change the auto-detect flag
|
||||||
|
if (language === null) {
|
||||||
|
language = block.language.name
|
||||||
|
}
|
||||||
changeLanguageTo(state, dispatch, block, language, auto)
|
changeLanguageTo(state, dispatch, block, language, auto)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,7 +318,7 @@ export function triggerCurrenciesLoaded(state, dispatch) {
|
|||||||
// This will make Math blocks re-render so that currency conversions are applied
|
// This will make Math blocks re-render so that currency conversions are applied
|
||||||
dispatch(state.update({
|
dispatch(state.update({
|
||||||
changes:{from: 0, to: 0, insert:""},
|
changes:{from: 0, to: 0, insert:""},
|
||||||
annotations: [heynoteEvent.of(CURRENCIES_LOADED)],
|
annotations: [heynoteEvent.of(CURRENCIES_LOADED), Transaction.addToHistory.of(false)],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -369,3 +377,65 @@ function moveCurrentBlock(state, dispatch, up) {
|
|||||||
}))
|
}))
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const deleteBlock = (editor) => ({state, dispatch}) => {
|
||||||
|
const range = state.selection.asSingle().ranges[0]
|
||||||
|
const blocks = state.facet(blockState)
|
||||||
|
let block
|
||||||
|
let nextBlock
|
||||||
|
for (let i = 0; i < blocks.length; i++) {
|
||||||
|
block = blocks[i]
|
||||||
|
if (block.range.from <= range.head && block.range.to >= range.head) {
|
||||||
|
if (i < blocks.length - 1) {
|
||||||
|
nextBlock = blocks[i + 1]
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let replace = ""
|
||||||
|
let newSelection
|
||||||
|
|
||||||
|
if (blocks.length == 1) {
|
||||||
|
replace = getBlockDelimiter(editor.defaultBlockToken, editor.defaultBlockAutoDetect)
|
||||||
|
newSelection = replace.length
|
||||||
|
} else if (!nextBlock) {
|
||||||
|
// if it's the last block, the cursor should go at the en of the previous block
|
||||||
|
newSelection = block.delimiter.from
|
||||||
|
} else {
|
||||||
|
// if there is a next block, we want the cursor to be at the beginning of that block
|
||||||
|
newSelection = block.delimiter.from + (nextBlock.delimiter.to - nextBlock.delimiter.from)
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(state.update({
|
||||||
|
changes: {
|
||||||
|
from: block.range.from,
|
||||||
|
to: block.range.to,
|
||||||
|
insert: replace,
|
||||||
|
},
|
||||||
|
selection: EditorSelection.cursor(newSelection),
|
||||||
|
annotations: [heynoteEvent.of(DELETE_BLOCK)],
|
||||||
|
}))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteBlockSetCursorPreviousBlock = (editor) => ({state, dispatch}) => {
|
||||||
|
const block = getActiveNoteBlock(state)
|
||||||
|
const blocks = state.facet(blockState)
|
||||||
|
let replace = ""
|
||||||
|
let newSelection = block.delimiter.from
|
||||||
|
if (blocks.length == 1) {
|
||||||
|
replace = getBlockDelimiter(editor.defaultBlockToken, editor.defaultBlockAutoDetect)
|
||||||
|
newSelection = replace.length
|
||||||
|
}
|
||||||
|
dispatch(state.update({
|
||||||
|
changes: {
|
||||||
|
from: block.range.from,
|
||||||
|
to: block.range.to,
|
||||||
|
insert: replace,
|
||||||
|
},
|
||||||
|
selection: EditorSelection.cursor(newSelection),
|
||||||
|
annotations: [heynoteEvent.of(DELETE_BLOCK)],
|
||||||
|
}))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { EditorSelection } from "@codemirror/state"
|
import { EditorSelection } from "@codemirror/state"
|
||||||
import { getActiveNoteBlock } from "./block"
|
import { getNoteBlockFromPos } from "./block"
|
||||||
|
|
||||||
function updateSel(sel, by) {
|
function updateSel(sel, by) {
|
||||||
return EditorSelection.create(sel.ranges.map(by), sel.mainIndex);
|
return EditorSelection.create(sel.ranges.map(by), sel.mainIndex);
|
||||||
@ -28,10 +28,10 @@ export const deleteLine = (view) => {
|
|||||||
|
|
||||||
const { state } = view
|
const { state } = view
|
||||||
|
|
||||||
const block = getActiveNoteBlock(view.state)
|
|
||||||
const selectedLines = selectedLineBlocks(state)
|
const selectedLines = selectedLineBlocks(state)
|
||||||
|
|
||||||
const changes = state.changes(selectedLines.map(({ from, to }) => {
|
const changes = state.changes(selectedLines.map(({ from, to }) => {
|
||||||
|
const block = getNoteBlockFromPos(state, from)
|
||||||
if(from !== block.content.from || to !== block.content.to) {
|
if(from !== block.content.from || to !== block.content.to) {
|
||||||
if (from > 0) from--
|
if (from > 0) from--
|
||||||
else if (to < state.doc.length) to++
|
else if (to < state.doc.length) to++
|
||||||
|
@ -4,7 +4,7 @@ import { RangeSetBuilder } from "@codemirror/state"
|
|||||||
import { WidgetType } from "@codemirror/view"
|
import { WidgetType } from "@codemirror/view"
|
||||||
|
|
||||||
import { getNoteBlockFromPos } from "./block"
|
import { getNoteBlockFromPos } from "./block"
|
||||||
import { CURRENCIES_LOADED } from "../annotation"
|
import { transactionsHasAnnotation, CURRENCIES_LOADED } from "../annotation"
|
||||||
|
|
||||||
|
|
||||||
class MathResult extends WidgetType {
|
class MathResult extends WidgetType {
|
||||||
@ -107,12 +107,6 @@ function mathDeco(view) {
|
|||||||
return builder.finish()
|
return builder.finish()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// This function checks if any of the transactions has the given annotation
|
|
||||||
const transactionsHasAnnotation = (transactions, annotation) => {
|
|
||||||
return transactions.some(tr => tr.annotations.some(a => a.value === annotation))
|
|
||||||
}
|
|
||||||
|
|
||||||
export const mathBlock = ViewPlugin.fromClass(class {
|
export const mathBlock = ViewPlugin.fromClass(class {
|
||||||
decorations
|
decorations
|
||||||
|
|
||||||
|
30
src/editor/block/transpose-chars.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { EditorSelection, findClusterBreak} from "@codemirror/state";
|
||||||
|
|
||||||
|
import { getNoteBlockFromPos } from "./block"
|
||||||
|
|
||||||
|
/**
|
||||||
|
Flip the characters before and after the cursor(s).
|
||||||
|
*/
|
||||||
|
export const transposeChars = ({ state, dispatch }) => {
|
||||||
|
if (state.readOnly)
|
||||||
|
return false;
|
||||||
|
let changes = state.changeByRange(range => {
|
||||||
|
// prevent transposing characters if we're at the start or end of a block, since it'll break the block syntax
|
||||||
|
const block = getNoteBlockFromPos(state, range.from)
|
||||||
|
if (range.from === block.content.from || range.from === block.content.to) {
|
||||||
|
return { range }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!range.empty || range.from == 0 || range.from == state.doc.length)
|
||||||
|
return { range };
|
||||||
|
let pos = range.from, line = state.doc.lineAt(pos);
|
||||||
|
let from = pos == line.from ? pos - 1 : findClusterBreak(line.text, pos - line.from, false) + line.from;
|
||||||
|
let to = pos == line.to ? pos + 1 : findClusterBreak(line.text, pos - line.from, true) + line.from;
|
||||||
|
return { changes: { from, to, insert: state.doc.slice(pos, to).append(state.doc.slice(from, pos)) },
|
||||||
|
range: EditorSelection.cursor(to) };
|
||||||
|
});
|
||||||
|
if (changes.changes.empty)
|
||||||
|
return false;
|
||||||
|
dispatch(state.update(changes, { scrollIntoView: true, userEvent: "move.character" }));
|
||||||
|
return true;
|
||||||
|
};
|
10
src/editor/close-brackets.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete'
|
||||||
|
import { Prec } from "@codemirror/state"
|
||||||
|
import { keymap } from "@codemirror/view"
|
||||||
|
|
||||||
|
export function getCloseBracketsExtensions() {
|
||||||
|
return [
|
||||||
|
closeBrackets(),
|
||||||
|
Prec.highest(keymap.of(closeBracketsKeymap)),
|
||||||
|
]
|
||||||
|
}
|
167
src/editor/commands.js
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
import * as codeMirrorCommands from "@codemirror/commands"
|
||||||
|
import {
|
||||||
|
undo, redo,
|
||||||
|
indentMore, indentLess,
|
||||||
|
deleteCharBackward, deleteCharForward,
|
||||||
|
deleteGroupBackward, deleteGroupForward,
|
||||||
|
deleteLineBoundaryBackward, deleteLineBoundaryForward,
|
||||||
|
deleteToLineEnd, deleteToLineStart,
|
||||||
|
simplifySelection,
|
||||||
|
splitLine,
|
||||||
|
insertNewlineAndIndent,
|
||||||
|
} from "@codemirror/commands"
|
||||||
|
import { foldCode, unfoldCode } from "@codemirror/language"
|
||||||
|
import { selectNextOccurrence } from "@codemirror/search"
|
||||||
|
import { insertNewlineContinueMarkup } from "@codemirror/lang-markdown"
|
||||||
|
|
||||||
|
import {
|
||||||
|
addNewBlockAfterCurrent, addNewBlockBeforeCurrent, addNewBlockAfterLast, addNewBlockBeforeFirst, insertNewBlockAtCursor,
|
||||||
|
gotoPreviousBlock, gotoNextBlock, selectNextBlock, selectPreviousBlock,
|
||||||
|
gotoPreviousParagraph, gotoNextParagraph, selectNextParagraph, selectPreviousParagraph,
|
||||||
|
moveLineUp, moveLineDown,
|
||||||
|
selectAll,
|
||||||
|
deleteBlock, deleteBlockSetCursorPreviousBlock,
|
||||||
|
newCursorBelow, newCursorAbove,
|
||||||
|
} from "./block/commands.js"
|
||||||
|
import { deleteLine } from "./block/delete-line.js"
|
||||||
|
import { formatBlockContent } from "./block/format-code.js"
|
||||||
|
import { transposeChars } from "./block/transpose-chars.js"
|
||||||
|
|
||||||
|
import { cutCommand, copyCommand, pasteCommand } from "./copy-paste.js"
|
||||||
|
|
||||||
|
import { markModeMoveCommand, toggleSelectionMarkMode, selectionMarkModeCancel } from "./mark-mode.js"
|
||||||
|
|
||||||
|
|
||||||
|
const cursorPreviousBlock = markModeMoveCommand(gotoPreviousBlock, selectPreviousBlock)
|
||||||
|
const cursorNextBlock = markModeMoveCommand(gotoNextBlock, selectNextBlock)
|
||||||
|
const cursorPreviousParagraph = markModeMoveCommand(gotoPreviousParagraph, selectPreviousParagraph)
|
||||||
|
const cursorNextParagraph = markModeMoveCommand(gotoNextParagraph, selectNextParagraph)
|
||||||
|
|
||||||
|
|
||||||
|
const openLanguageSelector = (editor) => () => {
|
||||||
|
editor.openLanguageSelector()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const openBufferSelector = (editor) => () => {
|
||||||
|
editor.openBufferSelector()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const openCommandPalette = (editor) => () => {
|
||||||
|
editor.openCommandPalette()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const openMoveToBuffer = (editor) => () => {
|
||||||
|
editor.openMoveToBufferSelector()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const openCreateNewBuffer = (editor) => () => {
|
||||||
|
editor.openCreateBuffer("new")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const nothing = (view) => {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const cmd = (f, category, description) => ({
|
||||||
|
run: f,
|
||||||
|
name: f.name,
|
||||||
|
description: description,
|
||||||
|
category: category,
|
||||||
|
})
|
||||||
|
|
||||||
|
const cmdLessContext = (f, category, description) => ({
|
||||||
|
run: (editor) => f,
|
||||||
|
name: f.name,
|
||||||
|
description: description,
|
||||||
|
category: category,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const HEYNOTE_COMMANDS = {
|
||||||
|
addNewBlockAfterCurrent: cmd(addNewBlockAfterCurrent, "Block", "Add new block after current block"),
|
||||||
|
addNewBlockBeforeCurrent: cmd(addNewBlockBeforeCurrent, "Block", "Add new block before current block"),
|
||||||
|
addNewBlockAfterLast: cmd(addNewBlockAfterLast, "Block", "Add new block after last block"),
|
||||||
|
addNewBlockBeforeFirst: cmd(addNewBlockBeforeFirst, "Block", "Add new block before first block"),
|
||||||
|
insertNewBlockAtCursor: cmd(insertNewBlockAtCursor, "Block", "Insert new block at cursor"),
|
||||||
|
deleteBlock: cmd(deleteBlock, "Block", "Delete block"),
|
||||||
|
deleteBlockSetCursorPreviousBlock: cmd(deleteBlockSetCursorPreviousBlock, "Block", "Delete block and set cursor to previous block"),
|
||||||
|
cursorPreviousBlock: cmd(cursorPreviousBlock, "Cursor", "Move cursor to previous block"),
|
||||||
|
cursorNextBlock: cmd(cursorNextBlock, "Cursor", "Move cursor to next block"),
|
||||||
|
cursorPreviousParagraph: cmd(cursorPreviousParagraph, "Cursor", "Move cursor to previous paragraph"),
|
||||||
|
cursorNextParagraph: cmd(cursorNextParagraph, "Cursor", "Move cursor to next paragraph"),
|
||||||
|
toggleSelectionMarkMode: cmd(toggleSelectionMarkMode, "Cursor", "Toggle selection mark mode"),
|
||||||
|
selectionMarkModeCancel: cmd(selectionMarkModeCancel, "Cursor", "Cancel selection mark mode"),
|
||||||
|
openLanguageSelector: cmd(openLanguageSelector, "Block", "Select block language"),
|
||||||
|
openBufferSelector: cmd(openBufferSelector, "Buffer", "Buffer selector"),
|
||||||
|
openCommandPalette: cmd(openCommandPalette, "Editor", "Open command palette"),
|
||||||
|
openMoveToBuffer: cmd(openMoveToBuffer, "Block", "Move block to another buffer"),
|
||||||
|
openCreateNewBuffer: cmd(openCreateNewBuffer, "Buffer", "Create new buffer"),
|
||||||
|
cut: cmd(cutCommand, "Clipboard", "Cut selection"),
|
||||||
|
copy: cmd(copyCommand, "Clipboard", "Copy selection"),
|
||||||
|
|
||||||
|
// commands without editor context
|
||||||
|
paste: cmdLessContext(pasteCommand, "Clipboard", "Paste from clipboard"),
|
||||||
|
selectAll: cmdLessContext(selectAll, "Selection", "Select all"),
|
||||||
|
moveLineUp: cmdLessContext(moveLineUp, "Edit", "Move line up"),
|
||||||
|
moveLineDown: cmdLessContext(moveLineDown, "Edit", "Move line down"),
|
||||||
|
deleteLine: cmdLessContext(deleteLine, "Edit", "Delete line"),
|
||||||
|
formatBlockContent: cmdLessContext(formatBlockContent, "Block", "Format block content"),
|
||||||
|
newCursorAbove: cmdLessContext(newCursorAbove, "Cursor", "Add cursor above"),
|
||||||
|
newCursorBelow: cmdLessContext(newCursorBelow, "Cursor", "Add cursor below"),
|
||||||
|
selectPreviousParagraph: cmdLessContext(selectPreviousParagraph, "Selection", "Select to previous paragraph"),
|
||||||
|
selectNextParagraph: cmdLessContext(selectNextParagraph, "Selection", "Select to next paragraph"),
|
||||||
|
selectPreviousBlock: cmdLessContext(selectPreviousBlock, "Selection", "Select to previous block"),
|
||||||
|
selectNextBlock: cmdLessContext(selectNextBlock, "Selection", "Select to next block"),
|
||||||
|
nothing: cmdLessContext(nothing, "Misc", "Do nothing"),
|
||||||
|
|
||||||
|
// directly from CodeMirror
|
||||||
|
undo: cmdLessContext(undo, "Edit", "Undo"),
|
||||||
|
redo: cmdLessContext(redo, "Edit", "Redo"),
|
||||||
|
indentMore: cmdLessContext(indentMore, "Edit", "Indent more"),
|
||||||
|
indentLess: cmdLessContext(indentLess, "Edit", "Indent less"),
|
||||||
|
foldCode: cmdLessContext(foldCode, "Edit", "Fold code"),
|
||||||
|
unfoldCode: cmdLessContext(unfoldCode, "Edit", "Unfold code"),
|
||||||
|
selectNextOccurrence: cmdLessContext(selectNextOccurrence, "Cursor", "Select next occurrence"),
|
||||||
|
deleteCharBackward: cmdLessContext(deleteCharBackward, "Edit", "Delete character backward"),
|
||||||
|
deleteCharForward: cmdLessContext(deleteCharForward, "Edit", "Delete character forward"),
|
||||||
|
deleteGroupBackward: cmdLessContext(deleteGroupBackward, "Edit", "Delete group backward"),
|
||||||
|
deleteGroupForward: cmdLessContext(deleteGroupForward, "Edit", "Delete group forward"),
|
||||||
|
deleteLineBoundaryBackward: cmdLessContext(deleteLineBoundaryBackward, "Edit", "Delete from start of wrapped line"),
|
||||||
|
deleteLineBoundaryForward: cmdLessContext(deleteLineBoundaryForward, "Edit", "Delete to end of wrapped line"),
|
||||||
|
deleteToLineEnd: cmdLessContext(deleteToLineEnd, "Edit", "Delete to end of line"),
|
||||||
|
deleteToLineStart: cmdLessContext(deleteToLineStart, "Edit", "Delete from start of line"),
|
||||||
|
simplifySelection: cmdLessContext(simplifySelection, "Cursor", "Simplify selection"),
|
||||||
|
splitLine: cmdLessContext(splitLine, "Edit", "Split line"),
|
||||||
|
transposeChars: cmdLessContext(transposeChars, "Edit", "Transpose characters"),
|
||||||
|
insertNewlineAndIndent: cmdLessContext(insertNewlineAndIndent, "Edit", "Insert newline and indent"),
|
||||||
|
insertNewlineContinueMarkup: cmdLessContext(insertNewlineContinueMarkup, "Markdown", "Insert newline and continue todo lists/block quotes"),
|
||||||
|
}
|
||||||
|
|
||||||
|
// selection mark-mode:ify all cursor/select commands from CodeMirror
|
||||||
|
for (let commandSuffix of [
|
||||||
|
"CharLeft", "CharRight",
|
||||||
|
"CharBackward", "CharForward",
|
||||||
|
"LineUp", "LineDown",
|
||||||
|
"LineStart", "LineEnd",
|
||||||
|
"GroupLeft", "GroupRight",
|
||||||
|
"GroupForward", "GroupBackward",
|
||||||
|
"PageUp", "PageDown",
|
||||||
|
"SyntaxLeft", "SyntaxRight",
|
||||||
|
"SubwordBackward", "SubwordForward",
|
||||||
|
"LineBoundaryBackward", "LineBoundaryForward",
|
||||||
|
]) {
|
||||||
|
HEYNOTE_COMMANDS[`cursor${commandSuffix}`] = {
|
||||||
|
run: markModeMoveCommand(codeMirrorCommands[`cursor${commandSuffix}`], codeMirrorCommands[`select${commandSuffix}`]),
|
||||||
|
name: `cursor${commandSuffix}`,
|
||||||
|
description: `cursor${commandSuffix}`,
|
||||||
|
category: "Cursor",
|
||||||
|
}
|
||||||
|
HEYNOTE_COMMANDS[`select${commandSuffix}`] = {
|
||||||
|
run: (editor) => codeMirrorCommands[`select${commandSuffix}`],
|
||||||
|
name: `select${commandSuffix}`,
|
||||||
|
description: `select${commandSuffix}`,
|
||||||
|
category: "Cursor",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { HEYNOTE_COMMANDS }
|
@ -2,7 +2,6 @@ import { EditorState, EditorSelection } from "@codemirror/state"
|
|||||||
import { EditorView } from "@codemirror/view"
|
import { EditorView } from "@codemirror/view"
|
||||||
|
|
||||||
import { LANGUAGES } from './languages.js';
|
import { LANGUAGES } from './languages.js';
|
||||||
import { setEmacsMarkMode } from "./emacs.js"
|
|
||||||
|
|
||||||
|
|
||||||
const languageTokensMatcher = LANGUAGES.map(l => l.token).join("|")
|
const languageTokensMatcher = LANGUAGES.map(l => l.token).join("|")
|
||||||
@ -61,7 +60,7 @@ export const heynoteCopyCut = (editor) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if we're in Emacs mode, we want to exit mark mode in case we're in it
|
// if we're in Emacs mode, we want to exit mark mode in case we're in it
|
||||||
setEmacsMarkMode(false)
|
editor.selectionMarkMode = false
|
||||||
|
|
||||||
// if Editor.deselectOnCopy is set (e.g. we're in Emacs mode), we want to remove the selection after we've copied the text
|
// if Editor.deselectOnCopy is set (e.g. we're in Emacs mode), we want to remove the selection after we've copied the text
|
||||||
if (editor.deselectOnCopy && event.type == "copy") {
|
if (editor.deselectOnCopy && event.type == "copy") {
|
||||||
@ -95,7 +94,7 @@ const copyCut = (view, cut, editor) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if we're in Emacs mode, we want to exit mark mode in case we're in it
|
// if we're in Emacs mode, we want to exit mark mode in case we're in it
|
||||||
setEmacsMarkMode(false)
|
editor.selectionMarkMode = false
|
||||||
|
|
||||||
// if Editor.deselectOnCopy is set (e.g. we're in Emacs mode), we want to remove the selection after we've copied the text
|
// if Editor.deselectOnCopy is set (e.g. we're in Emacs mode), we want to remove the selection after we've copied the text
|
||||||
if (editor.deselectOnCopy && !cut) {
|
if (editor.deselectOnCopy && !cut) {
|
||||||
@ -107,6 +106,7 @@ const copyCut = (view, cut, editor) => {
|
|||||||
selection: newSelection,
|
selection: newSelection,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { Annotation, EditorState, Compartment } from "@codemirror/state"
|
import { Annotation, EditorState, Compartment, Facet, EditorSelection, Transaction, Prec } from "@codemirror/state"
|
||||||
import { EditorView, keymap, drawSelection, ViewPlugin, lineNumbers } from "@codemirror/view"
|
import { EditorView, keymap as cmKeymap, drawSelection, ViewPlugin, lineNumbers } from "@codemirror/view"
|
||||||
import { indentUnit, forceParsing, foldGutter } from "@codemirror/language"
|
import { indentUnit, forceParsing, foldGutter, ensureSyntaxTree } from "@codemirror/language"
|
||||||
import { markdown } from "@codemirror/lang-markdown"
|
import { markdown, markdownKeymap } from "@codemirror/lang-markdown"
|
||||||
import { closeBrackets } from "@codemirror/autocomplete";
|
import { closeBrackets } from "@codemirror/autocomplete";
|
||||||
|
import { undo, redo } from "@codemirror/commands"
|
||||||
|
|
||||||
import { heynoteLight } from "./theme/light.js"
|
import { heynoteLight } from "./theme/light.js"
|
||||||
import { heynoteDark } from "./theme/dark.js"
|
import { heynoteDark } from "./theme/dark.js"
|
||||||
@ -10,36 +11,31 @@ import { heynoteBase } from "./theme/base.js"
|
|||||||
import { getFontTheme } from "./theme/font-theme.js";
|
import { getFontTheme } from "./theme/font-theme.js";
|
||||||
import { customSetup } from "./setup.js"
|
import { customSetup } from "./setup.js"
|
||||||
import { heynoteLang } from "./lang-heynote/heynote.js"
|
import { heynoteLang } from "./lang-heynote/heynote.js"
|
||||||
import { noteBlockExtension, blockLineNumbers, blockState } from "./block/block.js"
|
import { getCloseBracketsExtensions } from "./close-brackets.js"
|
||||||
import { heynoteEvent, SET_CONTENT } from "./annotation.js";
|
import { noteBlockExtension, blockLineNumbers, blockState, getActiveNoteBlock, triggerCursorChange } from "./block/block.js"
|
||||||
import { changeCurrentBlockLanguage, triggerCurrenciesLoaded } from "./block/commands.js"
|
import { heynoteEvent, SET_CONTENT, DELETE_BLOCK, APPEND_BLOCK, SET_FONT } from "./annotation.js";
|
||||||
|
import { changeCurrentBlockLanguage, triggerCurrenciesLoaded, getBlockDelimiter, deleteBlock, selectAll } from "./block/commands.js"
|
||||||
import { formatBlockContent } from "./block/format-code.js"
|
import { formatBlockContent } from "./block/format-code.js"
|
||||||
import { heynoteKeymap } from "./keymap.js"
|
import { getKeymapExtensions } from "./keymap.js"
|
||||||
import { emacsKeymap } from "./emacs.js"
|
|
||||||
import { heynoteCopyCut } from "./copy-paste"
|
import { heynoteCopyCut } from "./copy-paste"
|
||||||
import { languageDetection } from "./language-detection/autodetect.js"
|
import { languageDetection } from "./language-detection/autodetect.js"
|
||||||
import { autoSaveContent } from "./save.js"
|
import { autoSaveContent } from "./save.js"
|
||||||
import { todoCheckboxPlugin} from "./todo-checkbox.ts"
|
import { todoCheckboxPlugin} from "./todo-checkbox.ts"
|
||||||
import { links } from "./links.js"
|
import { links } from "./links.js"
|
||||||
|
import { HEYNOTE_COMMANDS } from "./commands.js";
|
||||||
export const LANGUAGE_SELECTOR_EVENT = "openLanguageSelector"
|
import { NoteFormat } from "../common/note-format.js"
|
||||||
|
import { AUTO_SAVE_INTERVAL } from "../common/constants.js"
|
||||||
function getKeymapExtensions(editor, keymap) {
|
import { useHeynoteStore } from "../stores/heynote-store.js";
|
||||||
if (keymap === "emacs") {
|
import { useErrorStore } from "../stores/error-store.js";
|
||||||
return emacsKeymap(editor)
|
|
||||||
} else {
|
|
||||||
return heynoteKeymap(editor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export class HeynoteEditor {
|
export class HeynoteEditor {
|
||||||
constructor({
|
constructor({
|
||||||
element,
|
element,
|
||||||
|
path,
|
||||||
content,
|
content,
|
||||||
focus=true,
|
focus=true,
|
||||||
theme="light",
|
theme="light",
|
||||||
saveFunction=null,
|
|
||||||
keymap="default",
|
keymap="default",
|
||||||
emacsMetaKey,
|
emacsMetaKey,
|
||||||
showLineNumberGutter=true,
|
showLineNumberGutter=true,
|
||||||
@ -47,8 +43,13 @@ export class HeynoteEditor {
|
|||||||
bracketClosing=false,
|
bracketClosing=false,
|
||||||
fontFamily,
|
fontFamily,
|
||||||
fontSize,
|
fontSize,
|
||||||
|
tabSize=4,
|
||||||
|
defaultBlockToken,
|
||||||
|
defaultBlockAutoDetect,
|
||||||
|
keyBindings,
|
||||||
}) {
|
}) {
|
||||||
this.element = element
|
this.element = element
|
||||||
|
this.path = path
|
||||||
this.themeCompartment = new Compartment
|
this.themeCompartment = new Compartment
|
||||||
this.keymapCompartment = new Compartment
|
this.keymapCompartment = new Compartment
|
||||||
this.lineNumberCompartmentPre = new Compartment
|
this.lineNumberCompartmentPre = new Compartment
|
||||||
@ -56,35 +57,42 @@ export class HeynoteEditor {
|
|||||||
this.foldGutterCompartment = new Compartment
|
this.foldGutterCompartment = new Compartment
|
||||||
this.readOnlyCompartment = new Compartment
|
this.readOnlyCompartment = new Compartment
|
||||||
this.closeBracketsCompartment = new Compartment
|
this.closeBracketsCompartment = new Compartment
|
||||||
|
this.indentUnitCompartment = new Compartment
|
||||||
this.deselectOnCopy = keymap === "emacs"
|
this.deselectOnCopy = keymap === "emacs"
|
||||||
this.emacsMetaKey = emacsMetaKey
|
this.emacsMetaKey = emacsMetaKey
|
||||||
this.fontTheme = new Compartment
|
this.fontTheme = new Compartment
|
||||||
|
this.setDefaultBlockLanguage(defaultBlockToken, defaultBlockAutoDetect)
|
||||||
|
this.contentLoaded = false
|
||||||
|
this.notesStore = useHeynoteStore()
|
||||||
|
this.errorStore = useErrorStore()
|
||||||
|
this.name = ""
|
||||||
|
this.selectionMarkMode = false
|
||||||
|
|
||||||
|
|
||||||
const state = EditorState.create({
|
const state = EditorState.create({
|
||||||
doc: content || "",
|
doc: "",
|
||||||
extensions: [
|
extensions: [
|
||||||
this.keymapCompartment.of(getKeymapExtensions(this, keymap)),
|
this.keymapCompartment.of(getKeymapExtensions(this, keymap, keyBindings)),
|
||||||
heynoteCopyCut(this),
|
heynoteCopyCut(this),
|
||||||
|
|
||||||
//minimalSetup,
|
//minimalSetup,
|
||||||
this.lineNumberCompartment.of(showLineNumberGutter ? [lineNumbers(), blockLineNumbers] : []),
|
this.lineNumberCompartment.of(showLineNumberGutter ? [lineNumbers(), blockLineNumbers] : []),
|
||||||
customSetup,
|
customSetup,
|
||||||
this.foldGutterCompartment.of(showFoldGutter ? [foldGutter()] : []),
|
this.foldGutterCompartment.of(showFoldGutter ? [foldGutter()] : []),
|
||||||
|
this.closeBracketsCompartment.of(bracketClosing ? [getCloseBracketsExtensions()] : []),
|
||||||
this.closeBracketsCompartment.of(bracketClosing ? [closeBrackets()] : []),
|
|
||||||
|
|
||||||
this.readOnlyCompartment.of([]),
|
this.readOnlyCompartment.of([]),
|
||||||
|
|
||||||
this.themeCompartment.of(theme === "dark" ? heynoteDark : heynoteLight),
|
this.themeCompartment.of(theme === "dark" ? heynoteDark : heynoteLight),
|
||||||
heynoteBase,
|
heynoteBase,
|
||||||
this.fontTheme.of(getFontTheme(fontFamily, fontSize)),
|
this.fontTheme.of(getFontTheme(fontFamily, fontSize)),
|
||||||
indentUnit.of(" "),
|
this.indentUnitCompartment.of(indentUnit.of(" ".repeat(tabSize))),
|
||||||
EditorView.scrollMargins.of(f => {
|
EditorView.scrollMargins.of(f => {
|
||||||
return {top: 80, bottom: 80}
|
return {top: 80, bottom: 80}
|
||||||
}),
|
}),
|
||||||
heynoteLang(),
|
heynoteLang(),
|
||||||
noteBlockExtension(this),
|
noteBlockExtension(this),
|
||||||
languageDetection(() => this.view),
|
languageDetection(path, () => this),
|
||||||
|
|
||||||
// set cursor blink rate to 1 second
|
// set cursor blink rate to 1 second
|
||||||
drawSelection({cursorBlinkRate:1000}),
|
drawSelection({cursorBlinkRate:1000}),
|
||||||
@ -94,53 +102,137 @@ export class HeynoteEditor {
|
|||||||
return {class: view.state.facet(EditorView.darkTheme) ? "dark-theme" : "light-theme"}
|
return {class: view.state.facet(EditorView.darkTheme) ? "dark-theme" : "light-theme"}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
saveFunction ? autoSaveContent(saveFunction, 2000) : [],
|
autoSaveContent(this, AUTO_SAVE_INTERVAL),
|
||||||
|
|
||||||
|
// Markdown extensions, we need to add markdownKeymap manually with the highest precedence
|
||||||
|
// so that it takes precedence over the default keymap
|
||||||
todoCheckboxPlugin,
|
todoCheckboxPlugin,
|
||||||
markdown(),
|
markdown({addKeymap: false}),
|
||||||
|
Prec.highest(cmKeymap.of(markdownKeymap)),
|
||||||
|
|
||||||
links,
|
links,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
// make sure saveFunction is called when page is unloaded
|
// make sure saveFunction is called when page is unloaded
|
||||||
if (saveFunction) {
|
|
||||||
window.addEventListener("beforeunload", () => {
|
window.addEventListener("beforeunload", () => {
|
||||||
saveFunction(this.getContent())
|
this.save()
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
this.view = new EditorView({
|
this.view = new EditorView({
|
||||||
state: state,
|
state: state,
|
||||||
parent: element,
|
parent: element,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (focus) {
|
//this.setContent(content)
|
||||||
this.view.dispatch({
|
this.setReadOnly(true)
|
||||||
selection: {anchor: this.view.state.doc.length, head: this.view.state.doc.length},
|
this.contentLoadedPromise = this.loadContent();
|
||||||
scrollIntoView: true,
|
this.contentLoadedPromise.then(() => {
|
||||||
|
this.setReadOnly(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (focus) {
|
||||||
this.view.focus()
|
this.view.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// trigger setFont once the fonts has loaded
|
||||||
|
document.fonts.ready.then(() => {
|
||||||
|
this.setFont(fontFamily, fontSize)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async save() {
|
||||||
|
if (!this.contentLoaded) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const content = this.getContent()
|
||||||
|
if (content === this.diskContent) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//console.log("saving:", this.path)
|
||||||
|
this.diskContent = content
|
||||||
|
await window.heynote.buffer.save(this.path, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
getContent() {
|
getContent() {
|
||||||
return this.view.state.sliceDoc()
|
this.note.content = this.view.state.sliceDoc()
|
||||||
|
this.note.cursors = this.view.state.selection.toJSON()
|
||||||
|
|
||||||
|
const ranges = this.note.cursors.ranges
|
||||||
|
if (ranges.length == 1 && ranges[0].anchor == 0 && ranges[0].head == 0) {
|
||||||
|
console.log("DEBUG!! Cursor is at 0,0")
|
||||||
|
console.trace()
|
||||||
|
}
|
||||||
|
return this.note.serialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadContent() {
|
||||||
|
//console.log("loading content", this.path)
|
||||||
|
const content = await window.heynote.buffer.load(this.path)
|
||||||
|
this.diskContent = content
|
||||||
|
this.contentLoaded = true
|
||||||
|
|
||||||
|
// set up content change listener
|
||||||
|
this.onChange = (content) => {
|
||||||
|
this.diskContent = content
|
||||||
|
this.setContent(content)
|
||||||
|
}
|
||||||
|
window.heynote.buffer.addOnChangeCallback(this.path, this.onChange)
|
||||||
|
|
||||||
|
await this.setContent(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
setContent(content) {
|
setContent(content) {
|
||||||
|
try {
|
||||||
|
this.note = NoteFormat.load(content)
|
||||||
|
this.setReadOnly(false)
|
||||||
|
} catch (e) {
|
||||||
|
this.setReadOnly(true)
|
||||||
|
this.errorStore.addError(`Failed to load note: ${e.message}`)
|
||||||
|
throw new Error(`Failed to load note: ${e.message}`)
|
||||||
|
}
|
||||||
|
this.name = this.note.metadata?.name || this.path
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
// set buffer content
|
||||||
this.view.dispatch({
|
this.view.dispatch({
|
||||||
changes: {
|
changes: {
|
||||||
from: 0,
|
from: 0,
|
||||||
to: this.view.state.doc.length,
|
to: this.view.state.doc.length,
|
||||||
insert: content,
|
insert: this.note.content,
|
||||||
},
|
},
|
||||||
annotations: [heynoteEvent.of(SET_CONTENT)],
|
annotations: [heynoteEvent.of(SET_CONTENT), Transaction.addToHistory.of(false)],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Ensure we have a parsed syntax tree when buffer is loaded. This prevents errors for large buffers
|
||||||
|
// when moving the cursor to the end of the buffer when the program starts
|
||||||
|
ensureSyntaxTree(this.view.state, this.view.state.doc.length, 5000)
|
||||||
|
|
||||||
|
// Set cursor positions
|
||||||
|
// We use requestAnimationFrame to avoid a race condition causing the scrollIntoView to sometimes not work
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (this.note.cursors) {
|
||||||
|
this.view.dispatch({
|
||||||
|
selection: EditorSelection.fromJSON(this.note.cursors),
|
||||||
|
scrollIntoView: true,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// if metadata doesn't contain cursor position, we set the cursor to the end of the buffer
|
||||||
this.view.dispatch({
|
this.view.dispatch({
|
||||||
selection: {anchor: this.view.state.doc.length, head: this.view.state.doc.length},
|
selection: {anchor: this.view.state.doc.length, head: this.view.state.doc.length},
|
||||||
scrollIntoView: true,
|
scrollIntoView: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setName(name) {
|
||||||
|
this.note.metadata.name = name
|
||||||
|
this.name = name
|
||||||
|
triggerCursorChange(this.view)
|
||||||
|
}
|
||||||
|
|
||||||
getBlocks() {
|
getBlocks() {
|
||||||
return this.view.state.facet(blockState)
|
return this.view.state.facet(blockState)
|
||||||
@ -150,6 +242,13 @@ export class HeynoteEditor {
|
|||||||
return this.view.state.selection.main.head
|
return this.view.state.selection.main.head
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setCursorPosition(position) {
|
||||||
|
this.view.dispatch({
|
||||||
|
selection: {anchor: position, head: position},
|
||||||
|
scrollIntoView: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
focus() {
|
focus() {
|
||||||
this.view.focus()
|
this.view.focus()
|
||||||
}
|
}
|
||||||
@ -163,6 +262,7 @@ export class HeynoteEditor {
|
|||||||
setFont(fontFamily, fontSize) {
|
setFont(fontFamily, fontSize) {
|
||||||
this.view.dispatch({
|
this.view.dispatch({
|
||||||
effects: this.fontTheme.reconfigure(getFontTheme(fontFamily, fontSize)),
|
effects: this.fontTheme.reconfigure(getFontTheme(fontFamily, fontSize)),
|
||||||
|
annotations: [heynoteEvent.of(SET_FONT), Transaction.addToHistory.of(false)],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,16 +272,88 @@ export class HeynoteEditor {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
setKeymap(keymap, emacsMetaKey) {
|
setKeymap(keymap, emacsMetaKey, keyBindings) {
|
||||||
this.deselectOnCopy = keymap === "emacs"
|
this.deselectOnCopy = keymap === "emacs"
|
||||||
this.emacsMetaKey = emacsMetaKey
|
this.emacsMetaKey = emacsMetaKey
|
||||||
this.view.dispatch({
|
this.view.dispatch({
|
||||||
effects: this.keymapCompartment.reconfigure(getKeymapExtensions(this, keymap)),
|
effects: this.keymapCompartment.reconfigure(getKeymapExtensions(this, keymap, keyBindings)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
openLanguageSelector() {
|
openLanguageSelector() {
|
||||||
this.element.dispatchEvent(new Event(LANGUAGE_SELECTOR_EVENT))
|
this.notesStore.openLanguageSelector()
|
||||||
|
}
|
||||||
|
|
||||||
|
openBufferSelector() {
|
||||||
|
this.notesStore.openBufferSelector()
|
||||||
|
}
|
||||||
|
|
||||||
|
openCommandPalette() {
|
||||||
|
this.notesStore.openCommandPalette()
|
||||||
|
}
|
||||||
|
|
||||||
|
openCreateBuffer(createMode) {
|
||||||
|
this.notesStore.openCreateBuffer(createMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
openMoveToBufferSelector() {
|
||||||
|
this.notesStore.openMoveToBufferSelector()
|
||||||
|
}
|
||||||
|
|
||||||
|
async createNewBuffer(path, name) {
|
||||||
|
const data = getBlockDelimiter(this.defaultBlockToken, this.defaultBlockAutoDetect)
|
||||||
|
await this.notesStore.saveNewBuffer(path, name, data)
|
||||||
|
|
||||||
|
// by using requestAnimationFrame we avoid a race condition where rendering the block backgrounds
|
||||||
|
// would fail if we immediately opened the new note (since the block UI wouldn't have time to update
|
||||||
|
// after the block was deleted)
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.notesStore.openBuffer(path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async createNewBufferFromActiveBlock(path, name) {
|
||||||
|
const block = getActiveNoteBlock(this.view.state)
|
||||||
|
if (!block) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const data = this.view.state.sliceDoc(block.range.from, block.range.to)
|
||||||
|
await this.notesStore.saveNewBuffer(path, name, data)
|
||||||
|
deleteBlock(this)(this.view)
|
||||||
|
|
||||||
|
// by using requestAnimationFrame we avoid a race condition where rendering the block backgrounds
|
||||||
|
// would fail if we immediately opened the new note (since the block UI wouldn't have time to update
|
||||||
|
// after the block was deleted)
|
||||||
|
//requestAnimationFrame(() => {
|
||||||
|
// this.notesStore.openBuffer(path)
|
||||||
|
//})
|
||||||
|
|
||||||
|
// add new buffer to recent list so that it shows up at the top of the buffer selector
|
||||||
|
this.notesStore.addRecentBuffer(path)
|
||||||
|
this.notesStore.addRecentBuffer(this.notesStore.currentBufferPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
getActiveBlockContent() {
|
||||||
|
const block = getActiveNoteBlock(this.view.state)
|
||||||
|
if (!block) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return this.view.state.sliceDoc(block.range.from, block.range.to)
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteActiveBlock() {
|
||||||
|
deleteBlock(this)(this.view)
|
||||||
|
}
|
||||||
|
|
||||||
|
appendBlockContent(content) {
|
||||||
|
this.view.dispatch({
|
||||||
|
changes: {
|
||||||
|
from: this.view.state.doc.length,
|
||||||
|
to: this.view.state.doc.length,
|
||||||
|
insert: content,
|
||||||
|
},
|
||||||
|
annotations: [heynoteEvent.of(APPEND_BLOCK)],
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentLanguage(lang, auto=false) {
|
setCurrentLanguage(lang, auto=false) {
|
||||||
@ -202,10 +374,15 @@ export class HeynoteEditor {
|
|||||||
|
|
||||||
setBracketClosing(value) {
|
setBracketClosing(value) {
|
||||||
this.view.dispatch({
|
this.view.dispatch({
|
||||||
effects: this.closeBracketsCompartment.reconfigure(value ? [closeBrackets()] : []),
|
effects: this.closeBracketsCompartment.reconfigure(value ? [getCloseBracketsExtensions()] : []),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setDefaultBlockLanguage(token, autoDetect) {
|
||||||
|
this.defaultBlockToken = token || "text"
|
||||||
|
this.defaultBlockAutoDetect = autoDetect === undefined ? true : autoDetect
|
||||||
|
}
|
||||||
|
|
||||||
formatCurrentBlock() {
|
formatCurrentBlock() {
|
||||||
formatBlockContent({
|
formatBlockContent({
|
||||||
state: this.view.state,
|
state: this.view.state,
|
||||||
@ -216,6 +393,54 @@ export class HeynoteEditor {
|
|||||||
currenciesLoaded() {
|
currenciesLoaded() {
|
||||||
triggerCurrenciesLoaded(this.view.state, this.view.dispatch)
|
triggerCurrenciesLoaded(this.view.state, this.view.dispatch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
destroy(save=true) {
|
||||||
|
if (this.onChange) {
|
||||||
|
window.heynote.buffer.removeOnChangeCallback(this.path, this.onChange)
|
||||||
|
}
|
||||||
|
if (save) {
|
||||||
|
this.save()
|
||||||
|
}
|
||||||
|
this.view.destroy()
|
||||||
|
window.heynote.buffer.close(this.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
//console.log("hiding element", this.view.dom)
|
||||||
|
this.view.dom.style.setProperty("display", "none", "important")
|
||||||
|
}
|
||||||
|
show() {
|
||||||
|
//console.log("showing element", this.view.dom)
|
||||||
|
this.view.dom.style.setProperty("display", "")
|
||||||
|
triggerCursorChange(this.view)
|
||||||
|
}
|
||||||
|
|
||||||
|
undo() {
|
||||||
|
undo(this.view)
|
||||||
|
}
|
||||||
|
|
||||||
|
redo() {
|
||||||
|
redo(this.view)
|
||||||
|
}
|
||||||
|
|
||||||
|
selectAll() {
|
||||||
|
selectAll(this.view)
|
||||||
|
}
|
||||||
|
|
||||||
|
setTabSize(tabSize) {
|
||||||
|
this.view.dispatch({
|
||||||
|
effects: this.indentUnitCompartment.reconfigure(indentUnit.of(" ".repeat(tabSize)))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
executeCommand(command) {
|
||||||
|
const cmd = HEYNOTE_COMMANDS[command]
|
||||||
|
if (!cmd) {
|
||||||
|
console.error(`Command not found: ${command}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cmd.run(this)(this.view)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,121 +0,0 @@
|
|||||||
import { Direction} from "@codemirror/view"
|
|
||||||
import { EditorSelection, EditorState, Prec } from "@codemirror/state"
|
|
||||||
import {
|
|
||||||
undo, redo,
|
|
||||||
cursorGroupLeft, cursorGroupRight, selectGroupLeft, selectGroupRight,
|
|
||||||
simplifySelection,
|
|
||||||
deleteCharForward, deleteCharBackward, deleteToLineEnd,
|
|
||||||
splitLine,
|
|
||||||
transposeChars,
|
|
||||||
cursorPageDown,
|
|
||||||
cursorCharLeft, selectCharLeft,
|
|
||||||
cursorCharRight, selectCharRight,
|
|
||||||
cursorLineUp, selectLineUp,
|
|
||||||
cursorLineDown, selectLineDown,
|
|
||||||
cursorLineStart, selectLineStart,
|
|
||||||
cursorLineEnd, selectLineEnd,
|
|
||||||
} from "@codemirror/commands"
|
|
||||||
import { heynoteKeymap, keymapFromSpec } from "./keymap.js"
|
|
||||||
|
|
||||||
import {
|
|
||||||
gotoPreviousBlock, gotoNextBlock,
|
|
||||||
selectNextBlock, selectPreviousBlock,
|
|
||||||
gotoPreviousParagraph, gotoNextParagraph,
|
|
||||||
selectNextParagraph, selectPreviousParagraph,
|
|
||||||
selectAll,
|
|
||||||
} from "./block/commands.js"
|
|
||||||
import { pasteCommand, copyCommand, cutCommand } from "./copy-paste.js"
|
|
||||||
|
|
||||||
|
|
||||||
// if set to true, all keybindings for moving around is changed to their corresponding select commands
|
|
||||||
let emacsMarkMode = false
|
|
||||||
|
|
||||||
export function setEmacsMarkMode(value) {
|
|
||||||
emacsMarkMode = value
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a command that will conditionally execute either the default command or the mark mode command
|
|
||||||
*
|
|
||||||
* @param defaultCmd Default command to execute
|
|
||||||
* @param {*} markModeCmd Command to execute if mark mode is active
|
|
||||||
*/
|
|
||||||
function emacsMoveCommand(defaultCmd, markModeCmd) {
|
|
||||||
return (view) => emacsMarkMode ? markModeCmd(view) : defaultCmd(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* C-g command that exits mark mode and simplifies selection
|
|
||||||
*/
|
|
||||||
function emacsCancel(view) {
|
|
||||||
simplifySelection(view)
|
|
||||||
setEmacsMarkMode(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exit mark mode before executing selectAll command
|
|
||||||
*/
|
|
||||||
function emacsSelectAll(view) {
|
|
||||||
setEmacsMarkMode(false)
|
|
||||||
return selectAll(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function emacsMetaKeyCommand(key, editor, command) {
|
|
||||||
const handler = (view, event) => {
|
|
||||||
if (editor.emacsMetaKey === "meta" && event.metaKey || editor.emacsMetaKey === "alt" && event.altKey) {
|
|
||||||
event.preventDefault()
|
|
||||||
return command(view)
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
{key, run:handler, preventDefault:false},
|
|
||||||
{key:key.replace("Meta", "Alt"), run:handler, preventDefault:false},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function emacsKeymap(editor) {
|
|
||||||
return [
|
|
||||||
heynoteKeymap(editor),
|
|
||||||
Prec.highest(keymapFromSpec([
|
|
||||||
["Ctrl-Shift--", undo],
|
|
||||||
["Ctrl-.", redo],
|
|
||||||
["Ctrl-g", emacsCancel],
|
|
||||||
["ArrowLeft", emacsMoveCommand(cursorCharLeft, selectCharLeft)],
|
|
||||||
["ArrowRight", emacsMoveCommand(cursorCharRight, selectCharRight)],
|
|
||||||
["ArrowUp", emacsMoveCommand(cursorLineUp, selectLineUp)],
|
|
||||||
["ArrowDown", emacsMoveCommand(cursorLineDown, selectLineDown)],
|
|
||||||
{key: "Ctrl-ArrowLeft", run: emacsMoveCommand(cursorGroupLeft, selectGroupLeft), shift: selectGroupLeft},
|
|
||||||
{key: "Ctrl-ArrowRight", run: emacsMoveCommand(cursorGroupRight, selectGroupRight), shift: selectGroupRight},
|
|
||||||
|
|
||||||
["Ctrl-d", deleteCharForward],
|
|
||||||
["Ctrl-h", deleteCharBackward],
|
|
||||||
["Ctrl-k", deleteToLineEnd],
|
|
||||||
["Ctrl-o", splitLine],
|
|
||||||
["Ctrl-t", transposeChars],
|
|
||||||
["Ctrl-v", cursorPageDown],
|
|
||||||
|
|
||||||
["Ctrl-y", pasteCommand],
|
|
||||||
["Ctrl-w", cutCommand(editor)],
|
|
||||||
...emacsMetaKeyCommand("Meta-w", editor, copyCommand(editor)),
|
|
||||||
|
|
||||||
{ key: "Ctrl-b", run: emacsMoveCommand(cursorCharLeft, selectCharLeft), shift: selectCharLeft },
|
|
||||||
{ key: "Ctrl-f", run: emacsMoveCommand(cursorCharRight, selectCharRight), shift: selectCharRight },
|
|
||||||
{ key: "Ctrl-p", run: emacsMoveCommand(cursorLineUp, selectLineUp), shift: selectLineUp },
|
|
||||||
{ key: "Ctrl-n", run: emacsMoveCommand(cursorLineDown, selectLineDown), shift: selectLineDown },
|
|
||||||
{ key: "Ctrl-a", run: emacsMoveCommand(cursorLineStart, selectLineStart), shift: selectLineStart },
|
|
||||||
{ key: "Ctrl-e", run: emacsMoveCommand(cursorLineEnd, selectLineEnd), shift: selectLineEnd },
|
|
||||||
])),
|
|
||||||
|
|
||||||
Prec.highest(keymapFromSpec([
|
|
||||||
["Ctrl-Space", (view) => { emacsMarkMode = !emacsMarkMode }],
|
|
||||||
["Mod-a", emacsSelectAll],
|
|
||||||
{key:"Mod-ArrowUp", run:emacsMoveCommand(gotoPreviousBlock, selectPreviousBlock), shift:selectPreviousBlock},
|
|
||||||
{key:"Mod-ArrowDown", run:emacsMoveCommand(gotoNextBlock, selectNextBlock), shift:selectNextBlock},
|
|
||||||
{key:"Ctrl-ArrowUp", run:emacsMoveCommand(gotoPreviousParagraph, selectPreviousParagraph), shift:selectPreviousParagraph},
|
|
||||||
{key:"Ctrl-ArrowDown", run:emacsMoveCommand(gotoNextParagraph, selectNextParagraph), shift:selectNextParagraph},
|
|
||||||
])),
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
export class SelectionChangeEvent extends Event {
|
|
||||||
constructor({cursorLine, language, languageAuto, selectionSize}) {
|
|
||||||
super("selectionChange")
|
|
||||||
this.cursorLine = cursorLine
|
|
||||||
this.selectionSize = selectionSize
|
|
||||||
this.language = language
|
|
||||||
this.languageAuto = languageAuto
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,72 +1,165 @@
|
|||||||
import { keymap } from "@codemirror/view"
|
import { keymap } from "@codemirror/view"
|
||||||
//import { EditorSelection, EditorState } from "@codemirror/state"
|
import { Prec } from "@codemirror/state"
|
||||||
import {
|
|
||||||
indentLess, indentMore,
|
|
||||||
} from "@codemirror/commands"
|
|
||||||
|
|
||||||
import {
|
import { HEYNOTE_COMMANDS } from "./commands.js"
|
||||||
insertNewBlockAtCursor,
|
|
||||||
addNewBlockBeforeCurrent, addNewBlockAfterCurrent,
|
|
||||||
addNewBlockBeforeFirst, addNewBlockAfterLast,
|
|
||||||
moveLineUp, moveLineDown,
|
|
||||||
selectAll,
|
|
||||||
gotoPreviousBlock, gotoNextBlock,
|
|
||||||
selectNextBlock, selectPreviousBlock,
|
|
||||||
gotoPreviousParagraph, gotoNextParagraph,
|
|
||||||
selectNextParagraph, selectPreviousParagraph,
|
|
||||||
newCursorBelow, newCursorAbove,
|
|
||||||
moveCurrentBlockUp, moveCurrentBlockDown,
|
|
||||||
} from "./block/commands.js"
|
|
||||||
import { pasteCommand, copyCommand, cutCommand } from "./copy-paste.js"
|
|
||||||
|
|
||||||
import { formatBlockContent } from "./block/format-code.js"
|
const cmd = (key, command) => ({key, command})
|
||||||
import { deleteLine } from "./block/delete-line.js"
|
const cmdShift = (key, command, shiftCommand) => {
|
||||||
|
return [
|
||||||
|
cmd(key, command),
|
||||||
|
cmd(`Shift-${key}`, shiftCommand),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMac = window.heynote.platform.isMac
|
||||||
|
const isLinux = window.heynote.platform.isLinux
|
||||||
|
const isWindows = window.heynote.platform.isWindows
|
||||||
|
|
||||||
|
export const DEFAULT_KEYMAP = [
|
||||||
|
cmd("Enter", "insertNewlineAndIndent"),
|
||||||
|
|
||||||
|
cmd("Mod-a", "selectAll"),
|
||||||
|
cmd("Mod-Enter", "addNewBlockAfterCurrent"),
|
||||||
|
cmd("Mod-Shift-Enter", "addNewBlockAfterLast"),
|
||||||
|
cmd("Alt-Enter", "addNewBlockBeforeCurrent"),
|
||||||
|
cmd("Alt-Shift-Enter", "addNewBlockBeforeFirst"),
|
||||||
|
cmd("Mod-Alt-Enter", "insertNewBlockAtCursor"),
|
||||||
|
...cmdShift("ArrowLeft", "cursorCharLeft", "selectCharLeft"),
|
||||||
|
...cmdShift("ArrowRight", "cursorCharRight", "selectCharRight"),
|
||||||
|
...cmdShift("ArrowUp", "cursorLineUp", "selectLineUp"),
|
||||||
|
...cmdShift("ArrowDown", "cursorLineDown", "selectLineDown"),
|
||||||
|
...cmdShift("Ctrl-ArrowLeft", "cursorGroupLeft", "selectGroupLeft"),
|
||||||
|
...cmdShift("Ctrl-ArrowRight", "cursorGroupRight", "selectGroupRight"),
|
||||||
|
...cmdShift("Alt-ArrowLeft", "cursorGroupLeft", "selectGroupLeft"),
|
||||||
|
...cmdShift("Alt-ArrowRight", "cursorGroupRight", "selectGroupRight"),
|
||||||
|
...cmdShift("Mod-ArrowUp", "cursorPreviousBlock", "selectPreviousBlock"),
|
||||||
|
...cmdShift("Mod-ArrowDown", "cursorNextBlock", "selectNextBlock"),
|
||||||
|
...cmdShift("Ctrl-ArrowUp", "cursorPreviousParagraph", "selectPreviousParagraph"),
|
||||||
|
...cmdShift("Ctrl-ArrowDown", "cursorNextParagraph", "selectNextParagraph"),
|
||||||
|
...cmdShift("PageUp", "cursorPageUp", "selectPageUp"),
|
||||||
|
...cmdShift("PageDown", "cursorPageDown", "selectPageDown"),
|
||||||
|
...cmdShift("Home", "cursorLineBoundaryBackward", "selectLineBoundaryBackward"),
|
||||||
|
...cmdShift("End", "cursorLineBoundaryForward", "selectLineBoundaryForward"),
|
||||||
|
cmd("Backspace", "deleteCharBackward"),
|
||||||
|
cmd("Delete", "deleteCharForward"),
|
||||||
|
cmd("Escape", "simplifySelection"),
|
||||||
|
cmd("Ctrl-Backspace", "deleteGroupBackward"),
|
||||||
|
cmd("Ctrl-Delete", "deleteGroupForward"),
|
||||||
|
...(isMac ? [
|
||||||
|
cmd("Alt-Backspace", "deleteGroupBackward"),
|
||||||
|
cmd("Alt-Delete", "deleteGroupForward"),
|
||||||
|
cmd("Mod-Backspace", "deleteLineBoundaryBackward"),
|
||||||
|
cmd("Mod-Delete", "deleteLineBoundaryForward"),
|
||||||
|
] : []),
|
||||||
|
|
||||||
|
cmd("Alt-ArrowUp", "moveLineUp"),
|
||||||
|
cmd("Alt-ArrowDown", "moveLineDown"),
|
||||||
|
cmd("Mod-Shift-k", "deleteLine"),
|
||||||
|
cmd("Mod-Alt-ArrowDown", "newCursorBelow"),
|
||||||
|
cmd("Mod-Alt-ArrowUp", "newCursorAbove"),
|
||||||
|
cmd("Mod-Shift-d", "deleteBlock"),
|
||||||
|
cmd("Mod-d", "selectNextOccurrence"),
|
||||||
|
cmd(isMac ? "Cmd-Alt-[" : "Ctrl-Shift-[", "foldCode"),
|
||||||
|
cmd(isMac ? "Cmd-Alt-]" : "Ctrl-Shift-]", "unfoldCode"),
|
||||||
|
|
||||||
|
cmd("Mod-c", "copy"),
|
||||||
|
cmd("Mod-v", "paste"),
|
||||||
|
cmd("Mod-x", "cut"),
|
||||||
|
cmd("Mod-z", "undo"),
|
||||||
|
cmd("Mod-Shift-z", "redo"),
|
||||||
|
...(isWindows || isLinux ? [
|
||||||
|
cmd("Mod-y", "redo"),
|
||||||
|
] : []),
|
||||||
|
|
||||||
|
cmd("Tab", "indentMore"),
|
||||||
|
cmd("Shift-Tab", "indentLess"),
|
||||||
|
//cmd("Alt-ArrowLeft", "cursorSubwordBackward"),
|
||||||
|
//cmd("Alt-ArrowRight", "cursorSubwordForward"),
|
||||||
|
|
||||||
|
cmd("Mod-l", "openLanguageSelector"),
|
||||||
|
cmd("Mod-p", "openBufferSelector"),
|
||||||
|
cmd("Mod-Shift-p", "openCommandPalette"),
|
||||||
|
cmd("Mod-s", "openMoveToBuffer"),
|
||||||
|
cmd("Mod-n", "openCreateNewBuffer"),
|
||||||
|
|
||||||
|
cmd("Alt-Shift-f", "formatBlockContent"),
|
||||||
|
|
||||||
|
// search
|
||||||
|
//cmd("Mod-f", "openSearchPanel"),
|
||||||
|
//cmd("F3", "findNext"),
|
||||||
|
//cmd("Mod-g", "findNext"),
|
||||||
|
//cmd("Shift-F3", "findPrevious"),
|
||||||
|
//cmd("Shift-Mod-g", "findPrevious"),
|
||||||
|
//cmd("Mod-Alt-g", "gotoLine"),
|
||||||
|
//cmd("Mod-d", "selectNextOccurrence"),
|
||||||
|
/*
|
||||||
|
- Mod-f: [`openSearchPanel`](https://codemirror.net/6/docs/ref/#search.openSearchPanel)
|
||||||
|
- F3, Mod-g: [`findNext`](https://codemirror.net/6/docs/ref/#search.findNext)
|
||||||
|
- Shift-F3, Shift-Mod-g: [`findPrevious`](https://codemirror.net/6/docs/ref/#search.findPrevious)
|
||||||
|
- Mod-Alt-g: [`gotoLine`](https://codemirror.net/6/docs/ref/#search.gotoLine)
|
||||||
|
- Mod-d: [`selectNextOccurrence`](https://codemirror.net/6/docs/ref/#search.selectNextOccurrence)
|
||||||
|
*/
|
||||||
|
]
|
||||||
|
|
||||||
|
export const EMACS_KEYMAP = [
|
||||||
|
cmd("Ctrl-w", "cut"),
|
||||||
|
cmd("Ctrl-y", "paste"),
|
||||||
|
cmd("EmacsMeta-w", "copy"),
|
||||||
|
cmd("Ctrl-Space", "toggleSelectionMarkMode"),
|
||||||
|
cmd("Ctrl-g", "selectionMarkModeCancel"),
|
||||||
|
cmd("Escape", "selectionMarkModeCancel"),
|
||||||
|
cmd("Ctrl-o", "splitLine"),
|
||||||
|
cmd("Ctrl-d", "deleteCharForward"),
|
||||||
|
cmd("Ctrl-h", "deleteCharBackward"),
|
||||||
|
cmd("Ctrl-k", "deleteToLineEnd"),
|
||||||
|
cmd("Ctrl-t", "transposeChars"),
|
||||||
|
cmd("Ctrl-Shift--", "undo"),
|
||||||
|
cmd("Ctrl-.", "redo"),
|
||||||
|
...cmdShift("Ctrl-v", "cursorPageDown", "selectPageDown"),
|
||||||
|
...cmdShift("Ctrl-b", "cursorCharLeft", "selectCharLeft"),
|
||||||
|
...cmdShift("Ctrl-f", "cursorCharRight", "selectCharRight"),
|
||||||
|
...cmdShift("Ctrl-a", "cursorLineStart", "selectLineStart"),
|
||||||
|
...cmdShift("Ctrl-e", "cursorLineEnd", "selectLineEnd"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
export function keymapFromSpec(specs) {
|
function keymapFromSpec(specs, editor) {
|
||||||
return keymap.of(specs.map((spec) => {
|
return keymap.of(specs.map((spec) => {
|
||||||
if (spec.run) {
|
let key = spec.key
|
||||||
if ("preventDefault" in spec) {
|
if (key.indexOf("EmacsMeta") != -1) {
|
||||||
return spec
|
key = key.replace("EmacsMeta", editor.emacsMetaKey === "alt" ? "Alt" : "Meta")
|
||||||
} else {
|
|
||||||
return {...spec, preventDefault: true}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
const [key, run] = spec
|
|
||||||
return {
|
return {
|
||||||
key,
|
key: key,
|
||||||
run,
|
//preventDefault: true,
|
||||||
preventDefault: true,
|
preventDefault: false,
|
||||||
|
run: (view) => {
|
||||||
|
//console.log("run()", spec.key, spec.command)
|
||||||
|
const command = HEYNOTE_COMMANDS[spec.command]
|
||||||
|
if (!command) {
|
||||||
|
console.error(`Command not found: ${spec.command} (${spec.key})`)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
return command.run(editor)(view)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function heynoteKeymap(editor) {
|
|
||||||
return keymapFromSpec([
|
export function heynoteKeymap(editor, keymap, userKeymap) {
|
||||||
["Mod-c", copyCommand(editor)],
|
return [
|
||||||
["Mod-v", pasteCommand],
|
keymapFromSpec([
|
||||||
["Mod-x", cutCommand(editor)],
|
...userKeymap,
|
||||||
["Tab", indentMore],
|
...keymap,
|
||||||
["Shift-Tab", indentLess],
|
], editor),
|
||||||
["Alt-Shift-Enter", addNewBlockBeforeFirst],
|
]
|
||||||
["Mod-Shift-Enter", addNewBlockAfterLast],
|
}
|
||||||
["Alt-Enter", addNewBlockBeforeCurrent],
|
|
||||||
["Mod-Enter", addNewBlockAfterCurrent],
|
export function getKeymapExtensions(editor, keymap, keyBindings) {
|
||||||
["Mod-Alt-Enter", insertNewBlockAtCursor],
|
return heynoteKeymap(
|
||||||
["Mod-a", selectAll],
|
editor,
|
||||||
["Alt-ArrowUp", moveLineUp],
|
keymap === "emacs" ? EMACS_KEYMAP.concat(DEFAULT_KEYMAP) : DEFAULT_KEYMAP,
|
||||||
["Alt-ArrowDown", moveLineDown],
|
keyBindings || [],
|
||||||
["Mod-l", () => editor.openLanguageSelector()],
|
)
|
||||||
["Alt-Shift-f", formatBlockContent],
|
|
||||||
["Mod-Alt-ArrowDown", newCursorBelow],
|
|
||||||
["Mod-Alt-ArrowUp", newCursorAbove],
|
|
||||||
["Mod-Shift-k", deleteLine],
|
|
||||||
{key:"Mod-ArrowUp", run:gotoPreviousBlock, shift:selectPreviousBlock},
|
|
||||||
{key:"Mod-ArrowDown", run:gotoNextBlock, shift:selectNextBlock},
|
|
||||||
{key:"Ctrl-ArrowUp", run:gotoPreviousParagraph, shift:selectPreviousParagraph},
|
|
||||||
{key:"Ctrl-ArrowDown", run:gotoNextParagraph, shift:selectNextParagraph},
|
|
||||||
["Mod-Shift-Alt-ArrowUp", moveCurrentBlockUp],
|
|
||||||
["Mod-Shift-Alt-ArrowDown", moveCurrentBlockDown],
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ NoteDelimiter {
|
|||||||
|
|
||||||
@tokens {
|
@tokens {
|
||||||
noteDelimiterMark { "∞∞∞" }
|
noteDelimiterMark { "∞∞∞" }
|
||||||
NoteLanguage { "text" | "math" | "javascript" | "typescript" | "jsx" | "tsx" | "json" | "python" | "html" | "sql" | "markdown" | "java" | "php" | "css" | "xml" | "cpp" | "rust" | "csharp" | "ruby" | "shell" | "yaml" | "golang" | "clojure" | "erlang" | "lezer" | "toml" | "swift" | "kotlin" | "groovy" | "diff" | "powershell" }
|
NoteLanguage { "text" | "math" | "javascript" | "typescript" | "jsx" | "tsx" | "json" | "python" | "html" | "sql" | "markdown" | "java" | "php" | "css" | "xml" | "cpp" | "rust" | "csharp" | "ruby" | "shell" | "yaml" | "golang" | "clojure" | "elixir" | "erlang" | "lezer" | "toml" | "swift" | "kotlin" | "groovy" | "diff" | "powershell" | "vue" | "dart" | "scala" }
|
||||||
Auto { "-a" }
|
Auto { "-a" }
|
||||||
noteDelimiterEnter { "\n" }
|
noteDelimiterEnter { "\n" }
|
||||||
//NoteContent { String }
|
//NoteContent { String }
|
||||||
|
@ -9,6 +9,7 @@ import { markdownLanguage } from "@codemirror/lang-markdown"
|
|||||||
import { javaLanguage } from "@codemirror/lang-java"
|
import { javaLanguage } from "@codemirror/lang-java"
|
||||||
import { lezerLanguage } from "@codemirror/lang-lezer"
|
import { lezerLanguage } from "@codemirror/lang-lezer"
|
||||||
import { phpLanguage } from "@codemirror/lang-php"
|
import { phpLanguage } from "@codemirror/lang-php"
|
||||||
|
import { elixirLanguage } from "codemirror-lang-elixir"
|
||||||
|
|
||||||
import { NoteContent, NoteLanguage } from "./parser.terms.js"
|
import { NoteContent, NoteLanguage } from "./parser.terms.js"
|
||||||
import { LANGUAGES } from "../languages.js"
|
import { LANGUAGES } from "../languages.js"
|
||||||
@ -22,7 +23,13 @@ export function configureNesting() {
|
|||||||
if (id == NoteContent) {
|
if (id == NoteContent) {
|
||||||
let noteLang = node.node.parent.firstChild.getChildren(NoteLanguage)[0]
|
let noteLang = node.node.parent.firstChild.getChildren(NoteLanguage)[0]
|
||||||
let langName = input.read(noteLang?.from, noteLang?.to)
|
let langName = input.read(noteLang?.from, noteLang?.to)
|
||||||
//console.log("langName:", langName)
|
|
||||||
|
// if the NoteContent is empty, we don't want to return a parser, since that seems to cause an
|
||||||
|
// error for StreamLanguage parsers when the buffer size is large (e.g >300 kb)
|
||||||
|
if (node.node.from == node.node.to) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
if (langName in languageMapping && languageMapping[langName] !== null) {
|
if (langName in languageMapping && languageMapping[langName] !== null) {
|
||||||
//console.log("found parser for language:", langName)
|
//console.log("found parser for language:", langName)
|
||||||
return {
|
return {
|
||||||
|
@ -10,7 +10,7 @@ export const parser = LRParser.deserialize({
|
|||||||
maxTerm: 10,
|
maxTerm: 10,
|
||||||
skippedNodes: [0],
|
skippedNodes: [0],
|
||||||
repeatNodeCount: 1,
|
repeatNodeCount: 1,
|
||||||
tokenData: "-x~RbYZ!Z}!O!`#V#W!k#W#X$X#X#Y$k#Z#[%Z#[#]%|#^#_&`#_#`'|#`#a(f#a#b)O#d#e)}#f#g+i#g#h+x#h#i,b#l#m&S#m#n-a%&x%&y-g~!`OX~~!cP#T#U!f~!kOU~~!nR#`#a!w#d#e#l#g#h#r~!zP#c#d!}~#QP#^#_#T~#WP#i#j#Z~#^P#f#g#a~#dP#X#Y#g~#lOT~~#oP#d#e#g~#uQ#[#]#{#g#h#g~$OP#T#U$R~$UP#f#g#l~$[P#]#^$_~$bP#Y#Z$e~$hP#Y#Z#g~$nP#f#g$q~$tP#`#a$w~$zP#T#U$}~%QP#b#c%T~%WP#Z#[#g~%^Q#c#d$q#f#g%d~%gP#c#d%j~%mP#c#d%p~%sP#j#k%v~%yP#m#n#g~&PP#h#i&S~&VP#a#b&Y~&]P#`#a#g~&cQ#T#U&i#g#h'm~&lP#j#k&o~&rP#T#U&u~&zPT~#g#h&}~'QP#V#W'T~'WP#f#g'Z~'^P#]#^'a~'dP#d#e'g~'jP#h#i#g~'pQ#c#d'v#l#m#g~'yP#b#c#g~(PP#c#d(S~(VP#h#i(Y~(]P#`#a(`~(cP#]#^'v~(iP#X#Y(l~(oP#n#o(r~(uP#X#Y(x~({P#f#g#g~)RP#T#U)U~)XQ#f#g)_#h#i)w~)bP#_#`)e~)hP#W#X)k~)nP#c#d)q~)tP#k#l'v~)zP#[#]#g~*QR#[#]#l#c#d*Z#m#n+V~*^P#k#l*a~*dP#X#Y*g~*jP#f#g*m~*pP#g#h*s~*vP#[#]*y~*|P#X#Y+P~+SP#`#a&Y~+YP#h#i+]~+`P#[#]+c~+fP#c#d'v~+lP#i#j+o~+rQ#U#V%v#g#h'g~+{R#[#]*y#e#f&Y#k#l,U~,XP#]#^,[~,_P#Y#Z'g~,eS#X#Y,q#c#d&S#g#h,w#m#n,}~,tP#l#m'g~,zP#l#m#g~-QP#d#e-T~-WP#X#Y-Z~-^P#g#h&}~-dP#T#U&S~-jP%&x%&y-m~-pP%&x%&y-s~-xOY~",
|
tokenData: "/W~RcYZ!^}!O!c#V#W!n#W#X$[#X#Y$}#Z#[&Y#[#]&{#^#_'_#_#`(u#`#a)_#a#b)q#d#e*p#f#g,[#g#h,k#h#i-j#j#k.i#l#m'R#m#n.o%&x%&y.u~!cOX~~!fP#T#U!i~!nOU~~!qR#`#a!z#d#e#o#g#h#u~!}P#c#d#Q~#TP#^#_#W~#ZP#i#j#^~#aP#f#g#d~#gP#X#Y#j~#oOT~~#rP#d#e#j~#xQ#[#]$O#g#h#j~$RP#T#U$U~$XP#f#g#o~$_Q#T#U$e#]#^$q~$hP#f#g$k~$nP#h#i#j~$tP#Y#Z$w~$zP#Y#Z#j~%QQ#`#a%W#f#g%p~%ZP#]#^%^~%aP#l#m%d~%gP#]#^%j~%mP#f#g#j~%sP#`#a%v~%yP#T#U%|~&PP#b#c&S~&VP#Z#[#j~&]Q#c#d%p#f#g&c~&fP#c#d&i~&lP#c#d&o~&rP#j#k&u~&xP#m#n#j~'OP#h#i'R~'UP#a#b'X~'[P#`#a#j~'bQ#T#U'h#g#h(f~'kP#j#k'n~'qP#T#U't~'yPT~#g#h'|~(PP#V#W(S~(VP#f#g(Y~(]P#]#^(`~(cP#d#e$k~(iQ#c#d(o#l#m#j~(rP#b#c#j~(xP#c#d({~)OP#h#i)R~)UP#`#a)X~)[P#]#^(o~)bP#X#Y)e~)hP#n#o)k~)nP#X#Y%j~)tP#T#U)w~)zQ#f#g*Q#h#i*j~*TP#_#`*W~*ZP#W#X*^~*aP#c#d*d~*gP#k#l(o~*mP#[#]#j~*sR#[#]#o#c#d*|#m#n+x~+PP#k#l+S~+VP#X#Y+Y~+]P#f#g+`~+cP#g#h+f~+iP#[#]+l~+oP#X#Y+r~+uP#`#a'X~+{P#h#i,O~,RP#[#],U~,XP#c#d(o~,_P#i#j,b~,eQ#U#V&u#g#h$k~,nS#V#W,z#[#]+l#e#f'X#k#l-^~,}P#T#U-Q~-TP#`#a-W~-ZP#T#U#j~-aP#]#^-d~-gP#Y#Z$k~-mS#X#Y-y#c#d'R#g#h.P#m#n.V~-|P#l#m$k~.SP#l#m#j~.YP#d#e.]~.`P#X#Y.c~.fP#g#h'|~.lP#i#j#d~.rP#T#U'R~.xP%&x%&y.{~/OP%&x%&y/R~/WOY~",
|
||||||
tokenizers: [0, noteContent],
|
tokenizers: [0, noteContent],
|
||||||
topRules: {"Document":[0,2]},
|
topRules: {"Document":[0,2]},
|
||||||
tokenPrec: 0
|
tokenPrec: 0
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { EditorState } from "@codemirror/state";
|
import { EditorState } from "@codemirror/state";
|
||||||
import { EditorView } from "@codemirror/view";
|
import { EditorView, ViewPlugin } from "@codemirror/view";
|
||||||
import { redoDepth } from "@codemirror/commands";
|
import { redoDepth } from "@codemirror/commands";
|
||||||
import { getActiveNoteBlock, blockState } from "../block/block";
|
import { getActiveNoteBlock, blockState } from "../block/block";
|
||||||
import { levenshtein_distance } from "./levenshtein";
|
import { levenshtein_distance } from "./levenshtein";
|
||||||
@ -25,17 +25,18 @@ function cancelIdleCallbackCompat(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function languageDetection(getView) {
|
// we'll use a shared global web worker for the language detection, for multiple Editor instances
|
||||||
const previousBlockContent = {}
|
const editorInstances = {}
|
||||||
let idleCallbackId = null
|
|
||||||
|
|
||||||
const detectionWorker = new Worker('langdetect-worker.js?worker');
|
const detectionWorker = new Worker('langdetect-worker.js?worker');
|
||||||
detectionWorker.onmessage = (event) => {
|
detectionWorker.onmessage = (event) => {
|
||||||
//console.log("event:", event.data)
|
//console.log("event:", event.data)
|
||||||
if (!event.data.guesslang.language) {
|
if (!event.data.guesslang.language) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const view = getView()
|
|
||||||
|
const editor = editorInstances[event.data.path]
|
||||||
|
//const editor = getEditor()
|
||||||
|
const view = editor.view
|
||||||
const state = view.state
|
const state = view.state
|
||||||
const block = getActiveNoteBlock(state)
|
const block = getActiveNoteBlock(state)
|
||||||
const newLang = GUESSLANG_TO_TOKEN[event.data.guesslang.language]
|
const newLang = GUESSLANG_TO_TOKEN[event.data.guesslang.language]
|
||||||
@ -57,7 +58,16 @@ export function languageDetection(getView) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const plugin = EditorView.updateListener.of(update => {
|
export function languageDetection(path, getEditor) {
|
||||||
|
const previousBlockContent = {}
|
||||||
|
let idleCallbackId = null
|
||||||
|
const editor = getEditor()
|
||||||
|
editorInstances[path] = editor
|
||||||
|
|
||||||
|
//const plugin = EditorView.updateListener.of(update => {
|
||||||
|
const plugin = ViewPlugin.fromClass(
|
||||||
|
class {
|
||||||
|
update(update) {
|
||||||
if (update.docChanged) {
|
if (update.docChanged) {
|
||||||
if (idleCallbackId !== null) {
|
if (idleCallbackId !== null) {
|
||||||
cancelIdleCallbackCompat(idleCallbackId)
|
cancelIdleCallbackCompat(idleCallbackId)
|
||||||
@ -88,11 +98,12 @@ export function languageDetection(getView) {
|
|||||||
|
|
||||||
const content = update.state.doc.sliceString(block.content.from, block.content.to)
|
const content = update.state.doc.sliceString(block.content.from, block.content.to)
|
||||||
if (content === "" && redoDepth(update.state) === 0) {
|
if (content === "" && redoDepth(update.state) === 0) {
|
||||||
// if content is cleared, set language to plaintext
|
// if content is cleared, set language to default
|
||||||
const view = getView()
|
//const editor = getEditor()
|
||||||
|
const view = editor.view
|
||||||
const block = getActiveNoteBlock(view.state)
|
const block = getActiveNoteBlock(view.state)
|
||||||
if (block.language.name !== "text") {
|
if (block.language.name !== editor.defaultBlockToken) {
|
||||||
changeLanguageTo(view.state, view.dispatch, block, "text", true)
|
changeLanguageTo(view.state, view.dispatch, block, editor.defaultBlockToken, true)
|
||||||
}
|
}
|
||||||
delete previousBlockContent[idx]
|
delete previousBlockContent[idx]
|
||||||
}
|
}
|
||||||
@ -106,12 +117,20 @@ export function languageDetection(getView) {
|
|||||||
detectionWorker.postMessage({
|
detectionWorker.postMessage({
|
||||||
content: content,
|
content: content,
|
||||||
idx: idx,
|
idx: idx,
|
||||||
|
path: path,
|
||||||
})
|
})
|
||||||
previousBlockContent[idx] = content
|
previousBlockContent[idx] = content
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
console.log("Removing editorInstance for:", path)
|
||||||
|
delete editorInstances[path]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
return plugin
|
return plugin
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import { cppLanguage } from "@codemirror/lang-cpp"
|
|||||||
import { xmlLanguage } from "@codemirror/lang-xml"
|
import { xmlLanguage } from "@codemirror/lang-xml"
|
||||||
import { rustLanguage } from "@codemirror/lang-rust"
|
import { rustLanguage } from "@codemirror/lang-rust"
|
||||||
import { csharpLanguage } from "@replit/codemirror-lang-csharp"
|
import { csharpLanguage } from "@replit/codemirror-lang-csharp"
|
||||||
|
import { vueLanguage } from "@codemirror/lang-vue";
|
||||||
|
|
||||||
import { StreamLanguage } from "@codemirror/language"
|
import { StreamLanguage } from "@codemirror/language"
|
||||||
import { ruby } from "@codemirror/legacy-modes/mode/ruby"
|
import { ruby } from "@codemirror/legacy-modes/mode/ruby"
|
||||||
@ -19,21 +20,22 @@ import { shell } from "@codemirror/legacy-modes/mode/shell"
|
|||||||
import { yaml } from "@codemirror/legacy-modes/mode/yaml"
|
import { yaml } from "@codemirror/legacy-modes/mode/yaml"
|
||||||
import { go } from "@codemirror/legacy-modes/mode/go"
|
import { go } from "@codemirror/legacy-modes/mode/go"
|
||||||
import { clojure } from "@codemirror/legacy-modes/mode/clojure"
|
import { clojure } from "@codemirror/legacy-modes/mode/clojure"
|
||||||
|
import { elixirLanguage } from "codemirror-lang-elixir"
|
||||||
import { erlang } from "@codemirror/legacy-modes/mode/erlang"
|
import { erlang } from "@codemirror/legacy-modes/mode/erlang"
|
||||||
import { toml } from "@codemirror/legacy-modes/mode/toml"
|
import { toml } from "@codemirror/legacy-modes/mode/toml"
|
||||||
import { swift } from "@codemirror/legacy-modes/mode/swift"
|
import { swift } from "@codemirror/legacy-modes/mode/swift"
|
||||||
import { kotlin } from "@codemirror/legacy-modes/mode/clike"
|
import { kotlin, dart, scala } from "@codemirror/legacy-modes/mode/clike"
|
||||||
import { groovy } from "@codemirror/legacy-modes/mode/groovy"
|
import { groovy } from "@codemirror/legacy-modes/mode/groovy"
|
||||||
import { diff } from "@codemirror/legacy-modes/mode/diff";
|
import { diff } from "@codemirror/legacy-modes/mode/diff";
|
||||||
import { powerShell } from "@codemirror/legacy-modes/mode/powershell";
|
import { powerShell } from "@codemirror/legacy-modes/mode/powershell";
|
||||||
|
|
||||||
import typescriptPlugin from "prettier/plugins/typescript.mjs"
|
import typescriptPlugin from "prettier/plugins/typescript"
|
||||||
import babelPrettierPlugin from "prettier/plugins/babel.mjs"
|
import babelPrettierPlugin from "prettier/plugins/babel"
|
||||||
import htmlPrettierPlugin from "prettier/esm/parser-html.mjs"
|
import htmlPrettierPlugin from "prettier/plugins/html"
|
||||||
import cssPrettierPlugin from "prettier/esm/parser-postcss.mjs"
|
import cssPrettierPlugin from "prettier/plugins/postcss"
|
||||||
import markdownPrettierPlugin from "prettier/esm/parser-markdown.mjs"
|
import markdownPrettierPlugin from "prettier/plugins/markdown"
|
||||||
import yamlPrettierPlugin from "prettier/plugins/yaml.mjs"
|
import yamlPrettierPlugin from "prettier/plugins/yaml"
|
||||||
import * as prettierPluginEstree from "prettier/plugins/estree.mjs";
|
import * as prettierPluginEstree from "prettier/plugins/estree";
|
||||||
|
|
||||||
|
|
||||||
class Language {
|
class Language {
|
||||||
@ -118,7 +120,7 @@ export const LANGUAGES = [
|
|||||||
new Language({
|
new Language({
|
||||||
token: "php",
|
token: "php",
|
||||||
name: "PHP",
|
name: "PHP",
|
||||||
parser: phpLanguage.parser,
|
parser: phpLanguage.configure({top:"Program"}).parser,
|
||||||
guesslang: "php",
|
guesslang: "php",
|
||||||
}),
|
}),
|
||||||
new Language({
|
new Language({
|
||||||
@ -189,6 +191,12 @@ export const LANGUAGES = [
|
|||||||
parser: StreamLanguage.define(clojure).parser,
|
parser: StreamLanguage.define(clojure).parser,
|
||||||
guesslang: "clj",
|
guesslang: "clj",
|
||||||
}),
|
}),
|
||||||
|
new Language({
|
||||||
|
token: "elixir",
|
||||||
|
name: "Elixir",
|
||||||
|
parser: elixirLanguage.parser,
|
||||||
|
guesslang: "ex",
|
||||||
|
}),
|
||||||
new Language({
|
new Language({
|
||||||
token: "erlang",
|
token: "erlang",
|
||||||
name: "Erlang",
|
name: "Erlang",
|
||||||
@ -253,6 +261,24 @@ export const LANGUAGES = [
|
|||||||
parser: StreamLanguage.define(powerShell).parser,
|
parser: StreamLanguage.define(powerShell).parser,
|
||||||
guesslang: "ps1",
|
guesslang: "ps1",
|
||||||
}),
|
}),
|
||||||
|
new Language({
|
||||||
|
token: "vue",
|
||||||
|
name: "Vue",
|
||||||
|
parser: vueLanguage.parser,
|
||||||
|
guesslang: null,
|
||||||
|
}),
|
||||||
|
new Language({
|
||||||
|
token: "dart",
|
||||||
|
name: "Dart",
|
||||||
|
parser: StreamLanguage.define(dart).parser,
|
||||||
|
guesslang: "dart",
|
||||||
|
}),
|
||||||
|
new Language({
|
||||||
|
token: "scala",
|
||||||
|
name: "Scala",
|
||||||
|
parser: StreamLanguage.define(scala).parser,
|
||||||
|
guesslang: "scala",
|
||||||
|
}),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
39
src/editor/mark-mode.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import {
|
||||||
|
simplifySelection,
|
||||||
|
} from "@codemirror/commands"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes a command that moves the cursor and a command that marks the selection, and returns a new command that
|
||||||
|
* will run the mark command if we're in Emacs mark mode, or the move command otherwise.
|
||||||
|
*/
|
||||||
|
export function markModeMoveCommand(defaultCmd, markModeCmd) {
|
||||||
|
return (editor) => {
|
||||||
|
if (editor.selectionMarkMode) {
|
||||||
|
return (view) => {
|
||||||
|
markModeCmd(view)
|
||||||
|
// we need to return true here instead of returning what the default command returns, since the default
|
||||||
|
// codemirror select commands will return false if the selection doesn't change, which in turn will
|
||||||
|
// make the default *move* command run which will kill the selection if we're in mark mode
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return (view) => defaultCmd(view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function toggleSelectionMarkMode(editor) {
|
||||||
|
return (view) => {
|
||||||
|
editor.selectionMarkMode = !editor.selectionMarkMode
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function selectionMarkModeCancel(editor) {
|
||||||
|
return (view) => {
|
||||||
|
simplifySelection(view)
|
||||||
|
editor.selectionMarkMode = false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
@ -1,18 +1,22 @@
|
|||||||
import { ViewPlugin } from "@codemirror/view"
|
import { ViewPlugin } from "@codemirror/view"
|
||||||
import { debounce } from "debounce"
|
import { debounce } from "debounce"
|
||||||
|
import { SET_CONTENT } from "./annotation"
|
||||||
|
|
||||||
|
|
||||||
export const autoSaveContent = (saveFunction, interval) => {
|
export const autoSaveContent = (editor, interval) => {
|
||||||
const save = debounce((view) => {
|
const save = debounce(() => {
|
||||||
//console.log("saving buffer")
|
//console.log("saving buffer")
|
||||||
saveFunction(view.state.sliceDoc())
|
editor.save()
|
||||||
}, interval);
|
}, interval);
|
||||||
|
|
||||||
return ViewPlugin.fromClass(
|
return ViewPlugin.fromClass(
|
||||||
class {
|
class {
|
||||||
update(update) {
|
update(update) {
|
||||||
if (update.docChanged) {
|
if (update.docChanged) {
|
||||||
save(update.view)
|
const initialSetContent = update.transactions.flatMap(t => t.annotations).some(a => a.value === SET_CONTENT)
|
||||||
|
if (!initialSetContent) {
|
||||||
|
save()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,11 +68,12 @@ const customSetup = /*@__PURE__*/(() => [
|
|||||||
EditorView.lineWrapping,
|
EditorView.lineWrapping,
|
||||||
scrollPastEnd(),
|
scrollPastEnd(),
|
||||||
keymap.of([
|
keymap.of([
|
||||||
...closeBracketsKeymap,
|
//...closeBracketsKeymap,
|
||||||
...defaultKeymap,
|
//...defaultKeymap,
|
||||||
...searchKeymap,
|
...searchKeymap,
|
||||||
...historyKeymap,
|
//...historyKeymap,
|
||||||
...foldKeymap,
|
//...foldKeymap,
|
||||||
|
|
||||||
//...completionKeymap,
|
//...completionKeymap,
|
||||||
//...lintKeymap
|
//...lintKeymap
|
||||||
])
|
])
|
||||||
|
@ -1,11 +1,39 @@
|
|||||||
import { EditorView } from "@codemirror/view"
|
import { EditorView } from "@codemirror/view"
|
||||||
|
import { Facet } from "@codemirror/state"
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the given font family is monospace by drawing test characters on a canvas
|
||||||
|
*/
|
||||||
|
function isMonospace(fontFamily) {
|
||||||
|
const testCharacters = ['i', 'W', 'm', ' ']
|
||||||
|
const testSize = '72px'
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
const context = canvas.getContext('2d')
|
||||||
|
context.font = `${testSize} ${fontFamily}`
|
||||||
|
|
||||||
|
const widths = testCharacters.map(char => context.measureText(char).width)
|
||||||
|
return widths.every(width => width === widths[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const isMonospaceFont = Facet.define({
|
||||||
|
combine: values => values.length ? values[0] : true,
|
||||||
|
})
|
||||||
|
|
||||||
export function getFontTheme(fontFamily, fontSize) {
|
export function getFontTheme(fontFamily, fontSize) {
|
||||||
fontSize = fontSize || window.heynote.defaultFontSize
|
fontSize = fontSize || window.heynote.defaultFontSize
|
||||||
return EditorView.theme({
|
const computedFontFamily = fontFamily || window.heynote.defaultFontFamily
|
||||||
|
return [
|
||||||
|
EditorView.theme({
|
||||||
'.cm-scroller': {
|
'.cm-scroller': {
|
||||||
fontFamily: fontFamily || window.heynote.defaultFontFamily,
|
fontFamily: computedFontFamily,
|
||||||
fontSize: (fontSize) + "px",
|
fontSize: (fontSize) + "px",
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
|
// in order to avoid a short flicker when the program is loaded with the default font (Hack),
|
||||||
|
// we hardcode Hack to be monospace
|
||||||
|
isMonospaceFont.of(computedFontFamily === "Hack" ? true : isMonospace(computedFontFamily)),
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
@ -3,25 +3,45 @@ import { syntaxTree, ensureSyntaxTree } from "@codemirror/language"
|
|||||||
import { WidgetType } from "@codemirror/view"
|
import { WidgetType } from "@codemirror/view"
|
||||||
import { ViewUpdate, ViewPlugin, DecorationSet } from "@codemirror/view"
|
import { ViewUpdate, ViewPlugin, DecorationSet } from "@codemirror/view"
|
||||||
|
|
||||||
|
import { isMonospaceFont } from "./theme/font-theme"
|
||||||
|
import { transactionsHasAnnotation, SET_FONT } from "./annotation"
|
||||||
|
|
||||||
|
|
||||||
class CheckboxWidget extends WidgetType {
|
class CheckboxWidget extends WidgetType {
|
||||||
constructor(readonly checked: boolean) { super() }
|
constructor(readonly checked: boolean, readonly monospace: boolean) { super() }
|
||||||
|
|
||||||
eq(other: CheckboxWidget) { return other.checked == this.checked }
|
eq(other: CheckboxWidget) { return other.checked == this.checked && other.monospace == this.monospace }
|
||||||
|
|
||||||
toDOM() {
|
toDOM() {
|
||||||
let wrap = document.createElement("span")
|
let wrap = document.createElement("span")
|
||||||
wrap.setAttribute("aria-hidden", "true")
|
wrap.setAttribute("aria-hidden", "true")
|
||||||
wrap.className = "cm-taskmarker-toggle"
|
wrap.className = "cm-taskmarker-toggle"
|
||||||
wrap.style.position = "relative"
|
|
||||||
// Three spaces since it's the same width as [ ] and [x]
|
let box = document.createElement("input")
|
||||||
wrap.appendChild(document.createTextNode(" "))
|
|
||||||
let box = wrap.appendChild(document.createElement("input"))
|
|
||||||
box.type = "checkbox"
|
box.type = "checkbox"
|
||||||
box.checked = this.checked
|
box.checked = this.checked
|
||||||
|
box.tabIndex = -1
|
||||||
|
box.style.margin = "0"
|
||||||
|
box.style.padding = "0"
|
||||||
|
|
||||||
|
if (this.monospace) {
|
||||||
|
// if the font is monospaced, we'll set the content of the wrapper to " " and the
|
||||||
|
// position of the checkbox to absolute, since three spaces will be the same width
|
||||||
|
// as "[ ]" and "[x]" so that characters on different lines will line up
|
||||||
|
wrap.appendChild(document.createTextNode(" "))
|
||||||
|
wrap.style.position = "relative"
|
||||||
box.style.position = "absolute"
|
box.style.position = "absolute"
|
||||||
box.style.top = "-3px"
|
box.style.top = "0"
|
||||||
box.style.left = "0"
|
box.style.left = "0.25em"
|
||||||
|
box.style.width = "1.1em"
|
||||||
|
box.style.height = "1.1em"
|
||||||
|
} else {
|
||||||
|
// if the font isn't monospaced, we'll let the checkbox take up as much space as needed
|
||||||
|
box.style.position = "relative"
|
||||||
|
box.style.top = "0.1em"
|
||||||
|
box.style.marginRight = "0.5em"
|
||||||
|
}
|
||||||
|
wrap.appendChild(box)
|
||||||
return wrap
|
return wrap
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,7 +72,7 @@ function checkboxes(view: EditorView) {
|
|||||||
if (view.state.doc.sliceString(nodeRef.to, nodeRef.to+1) === " ") {
|
if (view.state.doc.sliceString(nodeRef.to, nodeRef.to+1) === " ") {
|
||||||
let isChecked = view.state.doc.sliceString(nodeRef.from, nodeRef.to).toLowerCase() === "[x]"
|
let isChecked = view.state.doc.sliceString(nodeRef.from, nodeRef.to).toLowerCase() === "[x]"
|
||||||
let deco = Decoration.replace({
|
let deco = Decoration.replace({
|
||||||
widget: new CheckboxWidget(isChecked),
|
widget: new CheckboxWidget(isChecked, view.state.facet(isMonospaceFont)),
|
||||||
inclusive: false,
|
inclusive: false,
|
||||||
})
|
})
|
||||||
widgets.push(deco.range(nodeRef.from, nodeRef.to))
|
widgets.push(deco.range(nodeRef.from, nodeRef.to))
|
||||||
@ -92,9 +112,10 @@ export const todoCheckboxPlugin = [
|
|||||||
}
|
}
|
||||||
|
|
||||||
update(update: ViewUpdate) {
|
update(update: ViewUpdate) {
|
||||||
if (update.docChanged || update.viewportChanged)
|
if (update.docChanged || update.viewportChanged || transactionsHasAnnotation(update.transactions, SET_FONT)) {
|
||||||
this.decorations = checkboxes(update.view)
|
this.decorations = checkboxes(update.view)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, {
|
}, {
|
||||||
decorations: v => v.decorations,
|
decorations: v => v.decorations,
|
||||||
|
|
||||||
|
17
src/main.js
@ -1,17 +1,33 @@
|
|||||||
import './css/application.sass'
|
import './css/application.sass'
|
||||||
|
import '../assets/font/open-sans/open-sans.css'
|
||||||
|
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import PrimeVue from 'primevue/config';
|
||||||
|
|
||||||
import App from './components/App.vue'
|
import App from './components/App.vue'
|
||||||
import { loadCurrencies } from './currency'
|
import { loadCurrencies } from './currency'
|
||||||
|
import { useErrorStore } from './stores/error-store'
|
||||||
|
import { useHeynoteStore, initHeynoteStore } from './stores/heynote-store'
|
||||||
|
|
||||||
|
|
||||||
|
const pinia = createPinia()
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(pinia)
|
||||||
|
app.use(PrimeVue)
|
||||||
app.mount('#app').$nextTick(() => {
|
app.mount('#app').$nextTick(() => {
|
||||||
// hide loading screen
|
// hide loading screen
|
||||||
postMessage({ payload: 'removeLoading' }, '*')
|
postMessage({ payload: 'removeLoading' }, '*')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const errorStore = useErrorStore()
|
||||||
|
//errorStore.addError("test error")
|
||||||
|
window.heynote.getInitErrors().then((errors) => {
|
||||||
|
errors.forEach((e) => errorStore.addError(e))
|
||||||
|
})
|
||||||
|
|
||||||
|
initHeynoteStore()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -19,3 +35,4 @@ app.mount('#app').$nextTick(() => {
|
|||||||
loadCurrencies()
|
loadCurrencies()
|
||||||
setInterval(loadCurrencies, 1000 * 3600 * 4)
|
setInterval(loadCurrencies, 1000 * 3600 * 4)
|
||||||
|
|
||||||
|
window.heynote.init()
|
||||||
|
198
src/stores/editor-cache.js
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
import { toRaw, watch } from 'vue';
|
||||||
|
import { defineStore } from "pinia"
|
||||||
|
import { NoteFormat } from "../common/note-format"
|
||||||
|
|
||||||
|
import { useSettingsStore } from './settings-store'
|
||||||
|
import { useErrorStore } from './error-store'
|
||||||
|
import { useHeynoteStore } from './heynote-store'
|
||||||
|
import { HeynoteEditor } from '../editor/editor'
|
||||||
|
|
||||||
|
const NUM_EDITOR_INSTANCES = 5
|
||||||
|
|
||||||
|
export const useEditorCacheStore = defineStore("editorCache", {
|
||||||
|
state: () => ({
|
||||||
|
editorCache: {
|
||||||
|
lru: [],
|
||||||
|
cache: {},
|
||||||
|
watchHandler: null,
|
||||||
|
themeWatchHandler: null,
|
||||||
|
containerElement: null,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
createEditor(path) {
|
||||||
|
const settingsStore = useSettingsStore()
|
||||||
|
const errorStore = useErrorStore()
|
||||||
|
try {
|
||||||
|
return new HeynoteEditor({
|
||||||
|
element: this.containerElement,
|
||||||
|
path: path,
|
||||||
|
theme: settingsStore.theme,
|
||||||
|
keymap: settingsStore.settings.keymap,
|
||||||
|
emacsMetaKey: settingsStore.settings.emacsMetaKey,
|
||||||
|
showLineNumberGutter: settingsStore.settings.showLineNumberGutter,
|
||||||
|
showFoldGutter: settingsStore.settings.showFoldGutter,
|
||||||
|
bracketClosing: settingsStore.settings.bracketClosing,
|
||||||
|
fontFamily: settingsStore.settings.fontFamily,
|
||||||
|
fontSize: settingsStore.settings.fontSize,
|
||||||
|
tabSize: settingsStore.settings.tabSize,
|
||||||
|
defaultBlockToken: settingsStore.settings.defaultBlockLanguage,
|
||||||
|
defaultBlockAutoDetect: settingsStore.settings.defaultBlockLanguageAutoDetect,
|
||||||
|
keyBindings: settingsStore.settings.keyBindings,
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
errorStore.addError("Error! " + e.message)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getOrCreateEditor(path, updateLru) {
|
||||||
|
if (updateLru) {
|
||||||
|
// move to end of LRU
|
||||||
|
this.editorCache.lru = this.editorCache.lru.filter(p => p !== path)
|
||||||
|
this.editorCache.lru.push(path)
|
||||||
|
}
|
||||||
|
if (this.editorCache.cache[path]) {
|
||||||
|
return this.editorCache.cache[path]
|
||||||
|
} else {
|
||||||
|
const editor = this.createEditor(path)
|
||||||
|
this.addEditor(path, editor)
|
||||||
|
if (!updateLru) {
|
||||||
|
// if need to add the editor to the LRU, but at the top so that it is the first to be removed
|
||||||
|
this.editorCache.lru.unshift(path)
|
||||||
|
}
|
||||||
|
return editor
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getEditor(path) {
|
||||||
|
// move to end of LRU
|
||||||
|
this.editorCache.lru = this.editorCache.lru.filter(p => p !== path)
|
||||||
|
this.editorCache.lru.push(path)
|
||||||
|
if (this.editorCache.cache[path]) {
|
||||||
|
return this.editorCache.cache[path]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
addEditor(path, editor) {
|
||||||
|
if (this.editorCache.lru.length >= NUM_EDITOR_INSTANCES) {
|
||||||
|
const pathToFree = this.editorCache.lru.shift()
|
||||||
|
this.freeEditor(pathToFree)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.editorCache.cache[path] = editor
|
||||||
|
},
|
||||||
|
|
||||||
|
freeEditor(pathToFree) {
|
||||||
|
if (!this.editorCache.cache[pathToFree]) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.editorCache.cache[pathToFree].destroy()
|
||||||
|
delete this.editorCache.cache[pathToFree]
|
||||||
|
this.editorCache.lru = this.editorCache.lru.filter(p => p !== pathToFree)
|
||||||
|
},
|
||||||
|
|
||||||
|
eachEditor(fn) {
|
||||||
|
Object.values(toRaw(this.editorCache.cache)).forEach(fn)
|
||||||
|
},
|
||||||
|
|
||||||
|
clearCache(save=true) {
|
||||||
|
console.log("Clearing editor cache")
|
||||||
|
this.eachEditor((editor) => {
|
||||||
|
editor.destroy(save=save)
|
||||||
|
})
|
||||||
|
this.editorCache.cache = {}
|
||||||
|
this.editorCache.lru = []
|
||||||
|
},
|
||||||
|
|
||||||
|
onCurrenciesLoaded() {
|
||||||
|
this.eachEditor((editor) => {
|
||||||
|
editor.currenciesLoaded()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
setUp(containerElement) {
|
||||||
|
this.containerElement = containerElement
|
||||||
|
const settingsStore = useSettingsStore()
|
||||||
|
this.watchHandler = watch(() => settingsStore.settings, (newSettings, oldSettings) => {
|
||||||
|
//console.log("Settings changed (watch)", newSettings, oldSettings)
|
||||||
|
const changedKeys = Object.keys(newSettings).filter(key => newSettings[key] !== oldSettings[key])
|
||||||
|
|
||||||
|
for (const key of changedKeys) {
|
||||||
|
this.eachEditor((editor) => {
|
||||||
|
switch (key) {
|
||||||
|
case "keymap":
|
||||||
|
case "emacsMetaKey":
|
||||||
|
case "keyBindings":
|
||||||
|
editor.setKeymap(newSettings.keymap, newSettings.emacsMetaKey, newSettings.keyBindings)
|
||||||
|
break
|
||||||
|
case "showLineNumberGutter":
|
||||||
|
editor.setLineNumberGutter(newSettings.showLineNumberGutter)
|
||||||
|
break
|
||||||
|
case "showFoldGutter":
|
||||||
|
editor.setFoldGutter(newSettings.showFoldGutter)
|
||||||
|
break
|
||||||
|
case "bracketClosing":
|
||||||
|
editor.setBracketClosing(newSettings.bracketClosing)
|
||||||
|
break
|
||||||
|
case "fontFamily":
|
||||||
|
case "fontSize":
|
||||||
|
editor.setFont(newSettings.fontFamily, newSettings.fontSize)
|
||||||
|
break
|
||||||
|
case "tabSize":
|
||||||
|
editor.setTabSize(newSettings.tabSize)
|
||||||
|
break
|
||||||
|
case "defaultBlockLanguage":
|
||||||
|
case "defaultBlockLanguageAutoDetect":
|
||||||
|
editor.setDefaultBlockLanguage(newSettings.defaultBlockLanguage, newSettings.defaultBlockLanguageAutoDetect)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.themeWatchHandler = watch(() => settingsStore.theme, (theme) => {
|
||||||
|
this.eachEditor((editor) => {
|
||||||
|
editor.setTheme(theme)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
window.document.addEventListener("currenciesLoaded", this.onCurrenciesLoaded)
|
||||||
|
},
|
||||||
|
|
||||||
|
tearDown() {
|
||||||
|
if (this.watchHandler) {
|
||||||
|
this.watchHandler()
|
||||||
|
}
|
||||||
|
if (this.themeWatchHandler) {
|
||||||
|
this.themeWatchHandler()
|
||||||
|
}
|
||||||
|
|
||||||
|
window.document.removeEventListener("currenciesLoaded", this.onCurrenciesLoaded)
|
||||||
|
|
||||||
|
this.editorCache.lru = []
|
||||||
|
this.editorCache.cache = {}
|
||||||
|
},
|
||||||
|
|
||||||
|
moveCurrentBlockToOtherEditor(targetPath) {
|
||||||
|
const heynoteStore = useHeynoteStore()
|
||||||
|
|
||||||
|
const editor = toRaw(this.getEditor(heynoteStore.currentBufferPath))
|
||||||
|
let otherEditor = toRaw(this.getOrCreateEditor(targetPath, false))
|
||||||
|
otherEditor.hide()
|
||||||
|
|
||||||
|
const content = editor.getActiveBlockContent()
|
||||||
|
otherEditor.contentLoadedPromise.then(() => {
|
||||||
|
otherEditor.appendBlockContent(content)
|
||||||
|
editor.deleteActiveBlock()
|
||||||
|
|
||||||
|
// add the target buffer to recent buffers so that it shows up at the top of the buffer selector
|
||||||
|
heynoteStore.addRecentBuffer(targetPath)
|
||||||
|
heynoteStore.addRecentBuffer(heynoteStore.currentBufferPath)
|
||||||
|
})
|
||||||
|
|
||||||
|
//console.log("LRU", this.editorCache.lru)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
21
src/stores/error-store.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useErrorStore = defineStore("errors", {
|
||||||
|
state: () => ({
|
||||||
|
errors: [],
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
setErrors(errors) {
|
||||||
|
this.errors = errors
|
||||||
|
},
|
||||||
|
|
||||||
|
addError(error) {
|
||||||
|
this.errors.push(error)
|
||||||
|
},
|
||||||
|
|
||||||
|
popError() {
|
||||||
|
this.errors.splice(0, 1)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
197
src/stores/heynote-store.js
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
import { toRaw } from 'vue';
|
||||||
|
import { defineStore } from "pinia"
|
||||||
|
import { NoteFormat } from "../common/note-format"
|
||||||
|
import { useEditorCacheStore } from "./editor-cache"
|
||||||
|
import { SCRATCH_FILE_NAME } from "../common/constants"
|
||||||
|
|
||||||
|
|
||||||
|
export const useHeynoteStore = defineStore("heynote", {
|
||||||
|
state: () => ({
|
||||||
|
buffers: {},
|
||||||
|
recentBufferPaths: [SCRATCH_FILE_NAME],
|
||||||
|
|
||||||
|
currentEditor: null,
|
||||||
|
currentBufferPath: SCRATCH_FILE_NAME,
|
||||||
|
currentBufferName: null,
|
||||||
|
currentLanguage: null,
|
||||||
|
currentLanguageAuto: null,
|
||||||
|
currentCursorLine: null,
|
||||||
|
currentSelectionSize: null,
|
||||||
|
libraryId: 0,
|
||||||
|
createBufferParams: {
|
||||||
|
mode: "new",
|
||||||
|
nameSuggestion: ""
|
||||||
|
},
|
||||||
|
|
||||||
|
showBufferSelector: false,
|
||||||
|
showLanguageSelector: false,
|
||||||
|
showCreateBuffer: false,
|
||||||
|
showEditBuffer: false,
|
||||||
|
showMoveToBufferSelector: false,
|
||||||
|
showCommandPalette: false,
|
||||||
|
}),
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
async updateBuffers() {
|
||||||
|
this.setBuffers(await window.heynote.buffer.getList())
|
||||||
|
},
|
||||||
|
|
||||||
|
setBuffers(buffers) {
|
||||||
|
this.buffers = buffers
|
||||||
|
},
|
||||||
|
|
||||||
|
openBuffer(path) {
|
||||||
|
this.closeDialog()
|
||||||
|
this.currentBufferPath = path
|
||||||
|
this.addRecentBuffer(path)
|
||||||
|
},
|
||||||
|
|
||||||
|
addRecentBuffer(path) {
|
||||||
|
const recent = this.recentBufferPaths.filter((p) => p !== path)
|
||||||
|
recent.unshift(path)
|
||||||
|
this.recentBufferPaths = recent.slice(0, 100)
|
||||||
|
},
|
||||||
|
|
||||||
|
openLanguageSelector() {
|
||||||
|
this.closeDialog()
|
||||||
|
this.showLanguageSelector = true
|
||||||
|
},
|
||||||
|
openBufferSelector() {
|
||||||
|
this.closeDialog()
|
||||||
|
this.showBufferSelector = true
|
||||||
|
|
||||||
|
},
|
||||||
|
openCommandPalette() {
|
||||||
|
this.closeDialog()
|
||||||
|
this.showCommandPalette = true
|
||||||
|
},
|
||||||
|
openMoveToBufferSelector() {
|
||||||
|
this.closeDialog()
|
||||||
|
this.showMoveToBufferSelector = true
|
||||||
|
},
|
||||||
|
openCreateBuffer(createMode, nameSuggestion) {
|
||||||
|
createMode = createMode || "new"
|
||||||
|
this.closeDialog()
|
||||||
|
this.createBufferParams = {
|
||||||
|
mode: createMode || "new",
|
||||||
|
name: nameSuggestion || ""
|
||||||
|
}
|
||||||
|
this.showCreateBuffer = true
|
||||||
|
},
|
||||||
|
closeDialog() {
|
||||||
|
this.showCreateBuffer = false
|
||||||
|
this.showBufferSelector = false
|
||||||
|
this.showLanguageSelector = false
|
||||||
|
this.showEditBuffer = false
|
||||||
|
this.showMoveToBufferSelector = false
|
||||||
|
this.showCommandPalette = false
|
||||||
|
},
|
||||||
|
|
||||||
|
closeBufferSelector() {
|
||||||
|
this.showBufferSelector = false
|
||||||
|
this.showCommandPalette = false
|
||||||
|
},
|
||||||
|
|
||||||
|
closeMoveToBufferSelector() {
|
||||||
|
this.showMoveToBufferSelector = false
|
||||||
|
},
|
||||||
|
|
||||||
|
editBufferMetadata(path) {
|
||||||
|
if (this.currentBufferPath !== path) {
|
||||||
|
this.openBuffer(path)
|
||||||
|
}
|
||||||
|
this.closeDialog()
|
||||||
|
this.showEditBuffer = true
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
executeCommand(command) {
|
||||||
|
if (this.currentEditor) {
|
||||||
|
toRaw(this.currentEditor).executeCommand(command)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new note file at `path` with name `name` from the current block of the current open editor,
|
||||||
|
* and switch to it
|
||||||
|
*/
|
||||||
|
async createNewBufferFromActiveBlock(path, name) {
|
||||||
|
await toRaw(this.currentEditor).createNewBufferFromActiveBlock(path, name)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new empty note file at `path` with name `name`, and switch to it
|
||||||
|
*/
|
||||||
|
async createNewBuffer(path, name) {
|
||||||
|
await toRaw(this.currentEditor).createNewBuffer(path, name)
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new note file at path, with name `name`, and content content
|
||||||
|
* @param {*} path: File path relative to Heynote root
|
||||||
|
* @param {*} name Name of the note
|
||||||
|
* @param {*} content Contents (without metadata)
|
||||||
|
*/
|
||||||
|
async saveNewBuffer(path, name, content) {
|
||||||
|
if (this.buffers[path]) {
|
||||||
|
throw new Error(`Note already exists: ${path}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const note = new NoteFormat()
|
||||||
|
note.content = content
|
||||||
|
note.metadata.name = name
|
||||||
|
//console.log("saving", path, note.serialize())
|
||||||
|
await window.heynote.buffer.create(path, note.serialize())
|
||||||
|
this.updateBuffers()
|
||||||
|
},
|
||||||
|
|
||||||
|
async updateBufferMetadata(path, name, newPath) {
|
||||||
|
const editorCacheStore = useEditorCacheStore()
|
||||||
|
|
||||||
|
if (this.currentEditor.path !== path) {
|
||||||
|
throw new Error(`Can't update note (${path}) since it's not the active one (${this.currentEditor.path})`)
|
||||||
|
}
|
||||||
|
//console.log("currentEditor", this.currentEditor)
|
||||||
|
toRaw(this.currentEditor).setName(name)
|
||||||
|
await (toRaw(this.currentEditor)).save()
|
||||||
|
if (newPath && path !== newPath) {
|
||||||
|
//console.log("moving note", path, newPath)
|
||||||
|
editorCacheStore.freeEditor(path)
|
||||||
|
await window.heynote.buffer.move(path, newPath)
|
||||||
|
this.openBuffer(newPath)
|
||||||
|
this.updateBuffers()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteBuffer(path) {
|
||||||
|
if (path === SCRATCH_FILE_NAME) {
|
||||||
|
throw new Error("Can't delete scratch file")
|
||||||
|
}
|
||||||
|
const editorCacheStore = useEditorCacheStore()
|
||||||
|
if (this.currentEditor.path === path) {
|
||||||
|
this.currentEditor = null
|
||||||
|
this.currentBufferPath = SCRATCH_FILE_NAME
|
||||||
|
}
|
||||||
|
editorCacheStore.freeEditor(path)
|
||||||
|
await window.heynote.buffer.delete(path)
|
||||||
|
await this.updateBuffers()
|
||||||
|
},
|
||||||
|
|
||||||
|
async reloadLibrary() {
|
||||||
|
const editorCacheStore = useEditorCacheStore()
|
||||||
|
await this.updateBuffers()
|
||||||
|
editorCacheStore.clearCache(false)
|
||||||
|
this.currentEditor = null
|
||||||
|
this.currentBufferPath = SCRATCH_FILE_NAME
|
||||||
|
this.libraryId++
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export async function initHeynoteStore() {
|
||||||
|
const heynoteStore = useHeynoteStore()
|
||||||
|
window.heynote.buffer.setLibraryPathChangeCallback(() => {
|
||||||
|
heynoteStore.reloadLibrary()
|
||||||
|
})
|
||||||
|
await heynoteStore.updateBuffers()
|
||||||
|
}
|
49
src/stores/settings-store.js
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { defineStore } from "pinia"
|
||||||
|
|
||||||
|
import { SETTINGS_CHANGE_EVENT } from '@/src/common/constants'
|
||||||
|
|
||||||
|
export const useSettingsStore = defineStore("settings", {
|
||||||
|
state: () => {
|
||||||
|
return {
|
||||||
|
settings: window.heynote.settings,
|
||||||
|
themeSetting: "system",
|
||||||
|
theme: window.heynote.themeMode.initial,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
onSettingsChange(settings) {
|
||||||
|
this.settings = settings
|
||||||
|
},
|
||||||
|
|
||||||
|
setTheme(theme) {
|
||||||
|
window.heynote.themeMode.set(theme)
|
||||||
|
this.themeSetting = theme
|
||||||
|
},
|
||||||
|
|
||||||
|
setUp() {
|
||||||
|
window.heynote.mainProcess.on(SETTINGS_CHANGE_EVENT, (event, settings) => {
|
||||||
|
this.onSettingsChange(settings)
|
||||||
|
})
|
||||||
|
|
||||||
|
window.heynote.themeMode.get().then((mode) => {
|
||||||
|
this.theme = mode.computed
|
||||||
|
this.themeSetting = mode.theme
|
||||||
|
})
|
||||||
|
const onThemeChange = (theme) => {
|
||||||
|
this.theme = theme
|
||||||
|
if (theme === "system") {
|
||||||
|
document.documentElement.setAttribute("theme", window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")
|
||||||
|
} else {
|
||||||
|
document.documentElement.setAttribute("theme", theme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onThemeChange(window.heynote.themeMode.initial)
|
||||||
|
window.heynote.themeMode.onChange(onThemeChange)
|
||||||
|
},
|
||||||
|
|
||||||
|
tearDown() {
|
||||||
|
window.heynote.themeMode.removeListener()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
@ -8,7 +8,7 @@ test.beforeEach(async ({page}) => {
|
|||||||
await heynotePage.goto()
|
await heynotePage.goto()
|
||||||
|
|
||||||
expect((await heynotePage.getBlocks()).length).toBe(1)
|
expect((await heynotePage.getBlocks()).length).toBe(1)
|
||||||
heynotePage.setContent(`
|
await heynotePage.setContent(`
|
||||||
∞∞∞text
|
∞∞∞text
|
||||||
Block A
|
Block A
|
||||||
∞∞∞text
|
∞∞∞text
|
||||||
@ -114,3 +114,32 @@ const runTest = async (page, key, expectedBlocks) => {
|
|||||||
await expect(await page.locator("css=.heynote-block-start.first")).toHaveCount(1)
|
await expect(await page.locator("css=.heynote-block-start.first")).toHaveCount(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
test("test custom default block language", async ({ page, browserName }) => {
|
||||||
|
heynotePage.setContent(`
|
||||||
|
∞∞∞text
|
||||||
|
Text block`)
|
||||||
|
await page.locator("css=.status-block.settings").click()
|
||||||
|
await page.locator("css=li.tab-editing").click()
|
||||||
|
await page.locator("css=select.block-language").selectOption("Rust")
|
||||||
|
await page.locator("body").press("Escape")
|
||||||
|
await page.locator("body").press((heynotePage.isMac ? "Meta" : "Control") + "+Enter")
|
||||||
|
expect(await heynotePage.getContent()).toBe(`
|
||||||
|
∞∞∞text
|
||||||
|
Text block
|
||||||
|
∞∞∞rust-a
|
||||||
|
`)
|
||||||
|
|
||||||
|
await page.locator("css=.status-block.settings").click()
|
||||||
|
await page.locator("css=li.tab-editing").click()
|
||||||
|
await page.locator("css=input.language-auto-detect").click()
|
||||||
|
await page.locator("body").press("Escape")
|
||||||
|
await page.locator("body").press((heynotePage.isMac ? "Meta" : "Control") + "+Enter")
|
||||||
|
expect(await heynotePage.getContent()).toBe(`
|
||||||
|
∞∞∞text
|
||||||
|
Text block
|
||||||
|
∞∞∞rust-a
|
||||||
|
|
||||||
|
∞∞∞rust
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
28
tests/block-parsing.spec.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { expect, test } from "@playwright/test"
|
||||||
|
import { EditorState } from "@codemirror/state"
|
||||||
|
|
||||||
|
import { heynoteLang } from "../src/editor/lang-heynote/heynote.js"
|
||||||
|
import { getBlocksFromSyntaxTree, getBlocksFromString } from "../src/editor/block/block-parsing.js"
|
||||||
|
|
||||||
|
test("parse blocks from both syntax tree and string contents", async ({page}) => {
|
||||||
|
const contents = `
|
||||||
|
∞∞∞text
|
||||||
|
Text Block A
|
||||||
|
∞∞∞text-a
|
||||||
|
Text Block B
|
||||||
|
∞∞∞json-a
|
||||||
|
{
|
||||||
|
"key": "value"
|
||||||
|
}
|
||||||
|
∞∞∞python
|
||||||
|
print("Hello, World!")
|
||||||
|
`
|
||||||
|
const state = EditorState.create({
|
||||||
|
doc: contents,
|
||||||
|
extensions: heynoteLang(),
|
||||||
|
})
|
||||||
|
const treeBlocks = getBlocksFromSyntaxTree(state)
|
||||||
|
const stringBlocks = getBlocksFromString(state)
|
||||||
|
|
||||||
|
expect(treeBlocks).toEqual(stringBlocks)
|
||||||
|
})
|
97
tests/buffer-creation.spec.js
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import {expect, test} from "@playwright/test";
|
||||||
|
import {HeynotePage} from "./test-utils.js";
|
||||||
|
|
||||||
|
import { AUTO_SAVE_INTERVAL } from "../src/common/constants.js"
|
||||||
|
import { NoteFormat } from "../src/common/note-format.js"
|
||||||
|
|
||||||
|
|
||||||
|
let heynotePage
|
||||||
|
|
||||||
|
test.beforeEach(async ({page}) => {
|
||||||
|
heynotePage = new HeynotePage(page)
|
||||||
|
await heynotePage.goto()
|
||||||
|
|
||||||
|
expect((await heynotePage.getBlocks()).length).toBe(1)
|
||||||
|
await heynotePage.setContent(`
|
||||||
|
∞∞∞text
|
||||||
|
Block A
|
||||||
|
∞∞∞text
|
||||||
|
Block B
|
||||||
|
∞∞∞text
|
||||||
|
Block C`)
|
||||||
|
await page.waitForTimeout(100);
|
||||||
|
// check that blocks are created
|
||||||
|
expect((await heynotePage.getBlocks()).length).toBe(3)
|
||||||
|
|
||||||
|
// check that visual block layers are created
|
||||||
|
await expect(page.locator("css=.heynote-blocks-layer > div")).toHaveCount(3)
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test("default buffer saved", async ({page}) => {
|
||||||
|
// make some change and make sure content is auto saved in default scratch buffer
|
||||||
|
await page.locator("body").pressSequentially("YAY")
|
||||||
|
await page.waitForTimeout(AUTO_SAVE_INTERVAL + 50);
|
||||||
|
const bufferList = await heynotePage.getStoredBufferList()
|
||||||
|
expect(Object.keys(bufferList).length).toBe(1)
|
||||||
|
expect(bufferList["scratch.txt"]).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("create new buffer from block", async ({page}) => {
|
||||||
|
await page.locator("body").press(heynotePage.agnosticKey("Mod+S"))
|
||||||
|
await page.waitForTimeout(50)
|
||||||
|
await page.locator("body").press("ArrowUp")
|
||||||
|
await page.locator("body").press("Enter")
|
||||||
|
await page.waitForTimeout(50)
|
||||||
|
await page.locator("body").pressSequentially("My New Buffer")
|
||||||
|
await page.locator("body").press("Enter")
|
||||||
|
await page.waitForTimeout(150)
|
||||||
|
await page.waitForTimeout(AUTO_SAVE_INTERVAL + 50);
|
||||||
|
|
||||||
|
const buffers = Object.keys(await heynotePage.getStoredBufferList())
|
||||||
|
expect(buffers).toContain("scratch.txt")
|
||||||
|
expect(buffers).toContain("my-new-buffer.txt")
|
||||||
|
|
||||||
|
const defaultBuffer = NoteFormat.load(await heynotePage.getStoredBuffer("scratch.txt"))
|
||||||
|
const newBuffer = NoteFormat.load(await heynotePage.getStoredBuffer("my-new-buffer.txt"))
|
||||||
|
|
||||||
|
expect(defaultBuffer.content).toBe(`
|
||||||
|
∞∞∞text
|
||||||
|
Block A
|
||||||
|
∞∞∞text
|
||||||
|
Block B`)
|
||||||
|
|
||||||
|
expect(newBuffer.content).toBe(`
|
||||||
|
∞∞∞text
|
||||||
|
Block C`)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
test("create new empty note", async ({page}) => {
|
||||||
|
await page.locator("body").press("Enter")
|
||||||
|
await page.locator("body").press("Backspace")
|
||||||
|
await page.locator("body").press(heynotePage.agnosticKey("Mod+N"))
|
||||||
|
await page.locator("body").pressSequentially("New Empty Buffer")
|
||||||
|
await page.locator("body").press("Enter")
|
||||||
|
await page.waitForTimeout(AUTO_SAVE_INTERVAL + 50);
|
||||||
|
|
||||||
|
const buffers = Object.keys(await heynotePage.getStoredBufferList())
|
||||||
|
expect(buffers).toContain("scratch.txt")
|
||||||
|
expect(buffers).toContain("new-empty-buffer.txt")
|
||||||
|
|
||||||
|
const defaultBuffer = NoteFormat.load(await heynotePage.getStoredBuffer("scratch.txt"))
|
||||||
|
const newBuffer = NoteFormat.load(await heynotePage.getStoredBuffer("new-empty-buffer.txt"))
|
||||||
|
|
||||||
|
expect(defaultBuffer.content).toBe(`
|
||||||
|
∞∞∞text
|
||||||
|
Block A
|
||||||
|
∞∞∞text
|
||||||
|
Block B
|
||||||
|
∞∞∞text
|
||||||
|
Block C`)
|
||||||
|
|
||||||
|
expect(newBuffer.content).toBe(`
|
||||||
|
∞∞∞text-a
|
||||||
|
`)
|
||||||
|
})
|
@ -22,7 +22,7 @@ test("test default font is Hack", async ({ page }) => {
|
|||||||
})).toBeLessThan(20)
|
})).toBeLessThan(20)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("test custom font", async ({ page, browserName }) => {
|
test("test custom font", async ({ page }) => {
|
||||||
// monkey patch window.queryLocalFonts because it's not available in Playwright
|
// monkey patch window.queryLocalFonts because it's not available in Playwright
|
||||||
await page.evaluate(() => {
|
await page.evaluate(() => {
|
||||||
window.queryLocalFonts = async () => {
|
window.queryLocalFonts = async () => {
|
||||||
@ -64,3 +64,55 @@ test("test custom font", async ({ page, browserName }) => {
|
|||||||
return el.clientHeight
|
return el.clientHeight
|
||||||
})).toBeGreaterThan(20)
|
})).toBeGreaterThan(20)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("markdown todo checkbox position with monospaced font", async ({ page }) => {
|
||||||
|
await heynotePage.setContent(`
|
||||||
|
∞∞∞markdown
|
||||||
|
- [ ] Test
|
||||||
|
- [x] Test 2
|
||||||
|
`)
|
||||||
|
|
||||||
|
await page.locator("css=.cm-taskmarker-toggle input[type=checkbox]").first().waitFor()
|
||||||
|
await expect(await page.locator("css=.cm-taskmarker-toggle input[type=checkbox]")).toHaveCount(2)
|
||||||
|
await expect(await page.locator("css=.cm-taskmarker-toggle input[type=checkbox]").first()).toHaveCSS("position", "absolute")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("markdown todo checkbox position with variable width font", async ({ page }) => {
|
||||||
|
await page.evaluate(() => {
|
||||||
|
window.queryLocalFonts = async () => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
family: "Arial",
|
||||||
|
style: "Regular",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
family: "Hack",
|
||||||
|
fullName: "Hack Regular",
|
||||||
|
style: "Regular",
|
||||||
|
postscriptName: "Hack-Regular",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
family: "Hack",
|
||||||
|
fullName: "Hack Italic",
|
||||||
|
style: "Italic",
|
||||||
|
postscriptName: "Hack-Italic",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await page.locator("css=.status-block.settings").click()
|
||||||
|
await page.locator("css=li.tab-appearance").click()
|
||||||
|
await page.locator("css=select.font-family").selectOption("Arial")
|
||||||
|
await page.locator("css=select.font-size").selectOption("20")
|
||||||
|
await page.locator("body").press("Escape")
|
||||||
|
|
||||||
|
await heynotePage.setContent(`
|
||||||
|
∞∞∞markdown
|
||||||
|
- [ ] Test
|
||||||
|
- [x] Test 2
|
||||||
|
`)
|
||||||
|
|
||||||
|
await page.locator("css=.cm-taskmarker-toggle input[type=checkbox]").first().waitFor()
|
||||||
|
await expect(await page.locator("css=.cm-taskmarker-toggle input[type=checkbox]")).toHaveCount(2)
|
||||||
|
await expect(await page.locator("css=.cm-taskmarker-toggle input[type=checkbox]").first()).toHaveCSS("position", "relative")
|
||||||
|
})
|
73
tests/custom-key-bindings.spec.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
import { HeynotePage } from "./test-utils.js";
|
||||||
|
|
||||||
|
|
||||||
|
let heynotePage
|
||||||
|
|
||||||
|
test.beforeEach(async ({page}) => {
|
||||||
|
heynotePage = new HeynotePage(page)
|
||||||
|
await heynotePage.goto()
|
||||||
|
await heynotePage.setContent(`
|
||||||
|
∞∞∞text
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("add custom key binding", async ({page}) => {
|
||||||
|
await page.locator("css=.status-block.settings").click()
|
||||||
|
await page.locator("css=.overlay .settings .dialog .sidebar li.tab-keyboard-bindings").click()
|
||||||
|
await expect(page.locator("css=.settings .tab-content.tab-keyboard-bindings")).toBeVisible()
|
||||||
|
await page.locator("css=.settings .tab-content.tab-keyboard-bindings .add-keybinding").click()
|
||||||
|
await expect(page.locator("css=.settings .tab-content.tab-keyboard-bindings .add-key-binding-dialog")).toBeVisible()
|
||||||
|
await expect(page.locator("css=.settings .tab-content.tab-keyboard-bindings .add-key-binding-dialog input.keys")).toBeFocused()
|
||||||
|
await page.locator("body").press("Control+Shift+H")
|
||||||
|
await page.locator("body").press("Enter")
|
||||||
|
await page.locator("body").pressSequentially("language")
|
||||||
|
await page.locator(".p-autocomplete-list li.p-autocomplete-option.p-focus").click()
|
||||||
|
await page.locator("css=.settings .tab-content.tab-keyboard-bindings .add-key-binding-dialog .save").click()
|
||||||
|
await expect(page.locator("css=.settings .tab-content.tab-keyboard-bindings table tr.keybind-user")).toHaveCount(1)
|
||||||
|
expect((await heynotePage.getSettings()).keyBindings).toEqual([{key:"Control-Shift-h", command:"openLanguageSelector"}])
|
||||||
|
await page.locator("css=.overlay .settings .dialog .bottom-bar .close").click()
|
||||||
|
await page.locator("body").press("Control+Shift+H")
|
||||||
|
await expect(page.locator("css=.language-selector .items > li.selected")).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("delete custom key binding", async ({page}) => {
|
||||||
|
await page.locator("css=.status-block.settings").click()
|
||||||
|
await page.locator("css=.overlay .settings .dialog .sidebar li.tab-keyboard-bindings").click()
|
||||||
|
await expect(page.locator("css=.settings .tab-content.tab-keyboard-bindings")).toBeVisible()
|
||||||
|
await page.locator("css=.settings .tab-content.tab-keyboard-bindings .add-keybinding").click()
|
||||||
|
await expect(page.locator("css=.settings .tab-content.tab-keyboard-bindings .add-key-binding-dialog")).toBeVisible()
|
||||||
|
await expect(page.locator("css=.settings .tab-content.tab-keyboard-bindings .add-key-binding-dialog input.keys")).toBeFocused()
|
||||||
|
await page.locator("body").press("Control+Shift+H")
|
||||||
|
await page.locator("body").press("Enter")
|
||||||
|
await page.locator("body").pressSequentially("language")
|
||||||
|
await page.locator(".p-autocomplete-list li.p-autocomplete-option.p-focus").click()
|
||||||
|
await page.locator("css=.settings .tab-content.tab-keyboard-bindings .add-key-binding-dialog .save").click()
|
||||||
|
await expect(page.locator("css=.settings .tab-content.tab-keyboard-bindings table tr.keybind-user")).toHaveCount(1)
|
||||||
|
expect((await heynotePage.getSettings()).keyBindings).toEqual([{key:"Control-Shift-h", command:"openLanguageSelector"}])
|
||||||
|
await page.locator("css=.overlay .settings .dialog .bottom-bar .close").click()
|
||||||
|
await page.locator("body").press("Control+Shift+H")
|
||||||
|
await expect(page.locator("css=.language-selector .items > li.selected")).toBeVisible()
|
||||||
|
|
||||||
|
await page.locator("css=.status-block.settings").click()
|
||||||
|
await page.locator("css=.overlay .settings .dialog .sidebar li.tab-keyboard-bindings").click()
|
||||||
|
await expect(page.locator("css=.settings .tab-content.tab-keyboard-bindings")).toBeVisible()
|
||||||
|
await page.locator("css=.settings .tab-content.tab-keyboard-bindings table tr.keybind-user .delete").click()
|
||||||
|
await expect(page.locator("css=.settings .tab-content.tab-keyboard-bindings table tr.keybind-user")).toHaveCount(0)
|
||||||
|
await page.locator("css=.overlay .settings .dialog .bottom-bar .close").click()
|
||||||
|
await page.locator("body").press("Control+Shift+H")
|
||||||
|
await expect(page.locator("css=.language-selector .items > li.selected")).toHaveCount(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("disable default key binding", async ({page}) => {
|
||||||
|
const langKey = heynotePage.isMac ? "Meta+L" : "Control+L"
|
||||||
|
await page.locator("body").press(langKey)
|
||||||
|
await expect(page.locator("css=.language-selector .items > li.selected")).toBeVisible()
|
||||||
|
await page.locator("body").press("Escape")
|
||||||
|
await expect(page.locator("css=.language-selector .items > li.selected")).toHaveCount(0)
|
||||||
|
const settings = await heynotePage.getSettings()
|
||||||
|
settings.keyBindings = [{key:"Mod-L", command:"nothing"}]
|
||||||
|
await heynotePage.setSettings(settings)
|
||||||
|
await page.locator("body").press(langKey)
|
||||||
|
await expect(page.locator("css=.language-selector .items > li.selected")).toHaveCount(0)
|
||||||
|
})
|
44
tests/delete-block.spec.js
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import {expect, test} from "@playwright/test";
|
||||||
|
import {HeynotePage} from "./test-utils.js";
|
||||||
|
|
||||||
|
import { AUTO_SAVE_INTERVAL } from "../src/common/constants.js"
|
||||||
|
import { NoteFormat } from "../src/common/note-format.js"
|
||||||
|
|
||||||
|
|
||||||
|
let heynotePage
|
||||||
|
|
||||||
|
test.beforeEach(async ({page}) => {
|
||||||
|
heynotePage = new HeynotePage(page)
|
||||||
|
await heynotePage.goto()
|
||||||
|
|
||||||
|
expect((await heynotePage.getBlocks()).length).toBe(1)
|
||||||
|
await heynotePage.setContent(`
|
||||||
|
∞∞∞text
|
||||||
|
Block A
|
||||||
|
∞∞∞markdown
|
||||||
|
Block B
|
||||||
|
∞∞∞text
|
||||||
|
Block C`)
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("delete first block", async ({page}) => {
|
||||||
|
await heynotePage.setCursorPosition(10)
|
||||||
|
await page.locator("body").press(heynotePage.agnosticKey("Mod+Shift+D"))
|
||||||
|
await page.waitForTimeout(50)
|
||||||
|
expect(await heynotePage.getCursorPosition()).toBe(13)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("delete middle block", async ({page}) => {
|
||||||
|
await heynotePage.setCursorPosition(32)
|
||||||
|
await page.locator("body").press(heynotePage.agnosticKey("Mod+Shift+D"))
|
||||||
|
await page.waitForTimeout(50)
|
||||||
|
expect(await heynotePage.getCursorPosition()).toBe(25)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("delete last block", async ({page}) => {
|
||||||
|
await heynotePage.setCursorPosition(52)
|
||||||
|
await page.locator("body").press(heynotePage.agnosticKey("Mod+Shift+D"))
|
||||||
|
await page.waitForTimeout(50)
|
||||||
|
expect(await heynotePage.getCursorPosition()).toBe(36)
|
||||||
|
})
|
@ -13,6 +13,7 @@ test.beforeEach(async ({ page, browserName }) => {
|
|||||||
test.skip()
|
test.skip()
|
||||||
}
|
}
|
||||||
await page.locator("css=.status-block.settings").click()
|
await page.locator("css=.status-block.settings").click()
|
||||||
|
await page.locator("css=li.tab-keyboard-bindings").click()
|
||||||
//await page.locator("css=li.tab-editing").click()
|
//await page.locator("css=li.tab-editing").click()
|
||||||
await page.locator("css=select.keymap").selectOption("emacs")
|
await page.locator("css=select.keymap").selectOption("emacs")
|
||||||
if (heynotePage.isMac) {
|
if (heynotePage.isMac) {
|
||||||
|
@ -10,7 +10,7 @@ test.beforeEach(async ({ page }) => {
|
|||||||
|
|
||||||
|
|
||||||
test("JSON formatting", async ({ page }) => {
|
test("JSON formatting", async ({ page }) => {
|
||||||
heynotePage.setContent(`
|
await heynotePage.setContent(`
|
||||||
∞∞∞json
|
∞∞∞json
|
||||||
{"test": 1, "key2": "hey!"}
|
{"test": 1, "key2": "hey!"}
|
||||||
`)
|
`)
|
||||||
@ -25,7 +25,7 @@ test("JSON formatting", async ({ page }) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test("JSON formatting (cursor at start)", async ({ page }) => {
|
test("JSON formatting (cursor at start)", async ({ page }) => {
|
||||||
heynotePage.setContent(`
|
await heynotePage.setContent(`
|
||||||
∞∞∞json
|
∞∞∞json
|
||||||
{"test": 1, "key2": "hey!"}
|
{"test": 1, "key2": "hey!"}
|
||||||
`)
|
`)
|
||||||
|
16
tests/language-selector.spec.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
import { HeynotePage } from "./test-utils.js";
|
||||||
|
|
||||||
|
let heynotePage
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
heynotePage = new HeynotePage(page)
|
||||||
|
await heynotePage.goto()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
test("test language selector search by file ending", async ({ page }) => {
|
||||||
|
await page.locator("body").press(heynotePage.agnosticKey("Mod+L"))
|
||||||
|
await page.locator("body").pressSequentially("cpp")
|
||||||
|
await expect(page.locator("css=.language-selector .items > li.selected")).toHaveText("C++")
|
||||||
|
})
|
125
tests/move-block-between-buffers.spec.js
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import {expect, test} from "@playwright/test";
|
||||||
|
import {HeynotePage} from "./test-utils.js";
|
||||||
|
|
||||||
|
import { AUTO_SAVE_INTERVAL } from "../src/common/constants.js"
|
||||||
|
import { NoteFormat } from "../src/common/note-format.js"
|
||||||
|
|
||||||
|
|
||||||
|
let heynotePage
|
||||||
|
|
||||||
|
test.beforeEach(async ({page}) => {
|
||||||
|
heynotePage = new HeynotePage(page)
|
||||||
|
await heynotePage.goto()
|
||||||
|
|
||||||
|
expect((await heynotePage.getBlocks()).length).toBe(1)
|
||||||
|
await heynotePage.setContent(`
|
||||||
|
∞∞∞text
|
||||||
|
Block A
|
||||||
|
∞∞∞text
|
||||||
|
Block B
|
||||||
|
∞∞∞text
|
||||||
|
Block C`)
|
||||||
|
await page.waitForTimeout(100);
|
||||||
|
// check that blocks are created
|
||||||
|
expect((await heynotePage.getBlocks()).length).toBe(3)
|
||||||
|
|
||||||
|
// check that visual block layers are created
|
||||||
|
await expect(page.locator("css=.heynote-blocks-layer > div")).toHaveCount(3)
|
||||||
|
|
||||||
|
// create secondary buffer
|
||||||
|
await heynotePage.saveBuffer("other.txt", `
|
||||||
|
∞∞∞text-a
|
||||||
|
First block
|
||||||
|
∞∞∞math
|
||||||
|
Second block`)
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test("move block to other buffer", async ({page}) => {
|
||||||
|
await page.locator("body").press(heynotePage.agnosticKey("Mod+S"))
|
||||||
|
await page.waitForTimeout(50)
|
||||||
|
await page.locator("body").press("Enter")
|
||||||
|
await page.waitForTimeout(AUTO_SAVE_INTERVAL + 50);
|
||||||
|
|
||||||
|
const buffers = Object.keys(await heynotePage.getStoredBufferList())
|
||||||
|
expect(buffers).toContain("other.txt")
|
||||||
|
|
||||||
|
const otherBuffer = NoteFormat.load(await heynotePage.getStoredBuffer("other.txt"))
|
||||||
|
|
||||||
|
expect(await heynotePage.getContent()).toBe(`
|
||||||
|
∞∞∞text
|
||||||
|
Block A
|
||||||
|
∞∞∞text
|
||||||
|
Block B`)
|
||||||
|
|
||||||
|
expect(otherBuffer.content).toBe(`
|
||||||
|
∞∞∞text-a
|
||||||
|
First block
|
||||||
|
∞∞∞math
|
||||||
|
Second block
|
||||||
|
∞∞∞text
|
||||||
|
Block C`)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
test("move block to other open/cached buffer", async ({page}) => {
|
||||||
|
await page.locator("body").press(heynotePage.agnosticKey("Mod+P"))
|
||||||
|
await page.locator("body").press("Enter")
|
||||||
|
await page.waitForTimeout(50)
|
||||||
|
await page.locator("body").press(heynotePage.agnosticKey("Mod+P"))
|
||||||
|
await page.locator("body").press("Enter")
|
||||||
|
await page.waitForTimeout(50)
|
||||||
|
await page.locator("body").press(heynotePage.agnosticKey("Mod+S"))
|
||||||
|
await page.waitForTimeout(50)
|
||||||
|
await page.locator("body").press("Enter")
|
||||||
|
await page.waitForTimeout(AUTO_SAVE_INTERVAL + 50);
|
||||||
|
|
||||||
|
const buffers = Object.keys(await heynotePage.getStoredBufferList())
|
||||||
|
expect(buffers).toContain("other.txt")
|
||||||
|
|
||||||
|
const otherBuffer = NoteFormat.load(await heynotePage.getStoredBuffer("other.txt"))
|
||||||
|
|
||||||
|
expect(await heynotePage.getContent()).toBe(`
|
||||||
|
∞∞∞text
|
||||||
|
Block A
|
||||||
|
∞∞∞text
|
||||||
|
Block B`)
|
||||||
|
|
||||||
|
expect(otherBuffer.content).toBe(`
|
||||||
|
∞∞∞text-a
|
||||||
|
First block
|
||||||
|
∞∞∞math
|
||||||
|
Second block
|
||||||
|
∞∞∞text
|
||||||
|
Block C`)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
test("cursor position after moving first block", async ({page}) => {
|
||||||
|
await heynotePage.setCursorPosition(10)
|
||||||
|
expect(await heynotePage.getCursorPosition()).toBe(10)
|
||||||
|
await page.locator("body").press(heynotePage.agnosticKey("Mod+S"))
|
||||||
|
await page.waitForTimeout(50)
|
||||||
|
await page.locator("body").press("Enter")
|
||||||
|
await page.waitForTimeout(50)
|
||||||
|
expect(await heynotePage.getCursorPosition()).toBe(9)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("cursor position after moving middle block", async ({page}) => {
|
||||||
|
await heynotePage.setCursorPosition(28)
|
||||||
|
await page.locator("body").press(heynotePage.agnosticKey("Mod+S"))
|
||||||
|
await page.waitForTimeout(50)
|
||||||
|
await page.locator("body").press("Enter")
|
||||||
|
await page.waitForTimeout(50)
|
||||||
|
expect(await heynotePage.getCursorPosition()).toBe(25)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("cursor position after moving last block", async ({page}) => {
|
||||||
|
await heynotePage.setCursorPosition(48)
|
||||||
|
await page.locator("body").press(heynotePage.agnosticKey("Mod+S"))
|
||||||
|
await page.waitForTimeout(50)
|
||||||
|
await page.locator("body").press("Enter")
|
||||||
|
await page.waitForTimeout(50)
|
||||||
|
expect(await heynotePage.getCursorPosition()).toBe(32)
|
||||||
|
})
|
@ -15,7 +15,6 @@ Block A
|
|||||||
Block B
|
Block B
|
||||||
∞∞∞text
|
∞∞∞text
|
||||||
Block C`)
|
Block C`)
|
||||||
|
|
||||||
// check that blocks are created
|
// check that blocks are created
|
||||||
expect((await heynotePage.getBlocks()).length).toBe(3)
|
expect((await heynotePage.getBlocks()).length).toBe(3)
|
||||||
|
|
||||||
|
62
tests/note-format.spec.js
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
import { HeynotePage } from "./test-utils.js";
|
||||||
|
import { NoteFormat } from "../src/common/note-format.js";
|
||||||
|
|
||||||
|
let heynotePage
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
heynotePage = new HeynotePage(page)
|
||||||
|
await heynotePage.goto()
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test("test restore cursor position", async ({ page, browserName }) => {
|
||||||
|
await heynotePage.setContent(`{"formatVersion":"1.0.0", "cursors":{"ranges":[{"anchor":13,"head":13}],"main":0}}
|
||||||
|
∞∞∞text
|
||||||
|
Textblock`)
|
||||||
|
await page.locator("body").press((heynotePage.isMac ? "Meta" : "Control") + "+Alt+Enter")
|
||||||
|
expect(await heynotePage.getContent()).toBe(`
|
||||||
|
∞∞∞text
|
||||||
|
Text
|
||||||
|
∞∞∞text
|
||||||
|
block`)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
test("test save cursor positions", async ({ page, browserName }) => {
|
||||||
|
await heynotePage.setContent(`{"formatVersion":"1.0.0", "cursors":{"ranges":[{"anchor":9,"head":9}],"main":0}}
|
||||||
|
∞∞∞text
|
||||||
|
this
|
||||||
|
is
|
||||||
|
a
|
||||||
|
text
|
||||||
|
block`)
|
||||||
|
await page.locator("body").press((heynotePage.isMac ? "Meta" : "Control") + "+Alt+ArrowDown")
|
||||||
|
await page.locator("body").press((heynotePage.isMac ? "Meta" : "Control") + "+Alt+ArrowDown")
|
||||||
|
await page.locator("body").press("Delete")
|
||||||
|
expect(await heynotePage.getContent()).toBe(`
|
||||||
|
∞∞∞text
|
||||||
|
his
|
||||||
|
s
|
||||||
|
|
||||||
|
text
|
||||||
|
block`)
|
||||||
|
|
||||||
|
const bufferData = await heynotePage.getBufferData()
|
||||||
|
const note = NoteFormat.load(bufferData)
|
||||||
|
expect(note.cursors.ranges.length).toBe(3)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("unknown note metadata keys is kept", async ({ page, browserName }) => {
|
||||||
|
await heynotePage.setContent(`{"yoda":[123], "formatVersion":"1.0.0", "cursors":{"ranges":[{"anchor":15,"head":15}],"main":0}}
|
||||||
|
∞∞∞text
|
||||||
|
block 1`)
|
||||||
|
await page.locator("body").pressSequentially("hello")
|
||||||
|
expect(await heynotePage.getContent()).toBe(`
|
||||||
|
∞∞∞text
|
||||||
|
block hello1`)
|
||||||
|
|
||||||
|
const bufferData = await heynotePage.getBufferData()
|
||||||
|
const note = NoteFormat.load(bufferData)
|
||||||
|
expect(note.metadata.yoda).toStrictEqual([123])
|
||||||
|
})
|
23
tests/tab-size-setting.spec.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { test, expect } from "@playwright/test";
|
||||||
|
import { HeynotePage } from "./test-utils.js";
|
||||||
|
|
||||||
|
let heynotePage
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
heynotePage = new HeynotePage(page)
|
||||||
|
await heynotePage.goto()
|
||||||
|
});
|
||||||
|
|
||||||
|
test("test default tab size", async ({ page }) => {
|
||||||
|
await page.locator("body").press("Tab")
|
||||||
|
expect(await heynotePage.getBlockContent(0)).toBe(" ")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("test custom tab size", async ({ page }) => {
|
||||||
|
await page.locator("css=.status-block.settings").click()
|
||||||
|
await page.locator("css=li.tab-editing").click()
|
||||||
|
await page.locator("css=select.tab-size").selectOption("2")
|
||||||
|
await page.locator("body").press("Escape")
|
||||||
|
await page.locator("body").press("Tab")
|
||||||
|
expect(await heynotePage.getBlockContent(0)).toBe(" ")
|
||||||
|
})
|
@ -1,4 +1,5 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
|
import { NoteFormat } from '../src/common/note-format.js';
|
||||||
|
|
||||||
export function pageErrorGetter(page) {
|
export function pageErrorGetter(page) {
|
||||||
let messages = [];
|
let messages = [];
|
||||||
@ -19,6 +20,7 @@ export class HeynotePage {
|
|||||||
async goto() {
|
async goto() {
|
||||||
await this.page.goto("/")
|
await this.page.goto("/")
|
||||||
await expect(this.page).toHaveTitle(/Heynote/)
|
await expect(this.page).toHaveTitle(/Heynote/)
|
||||||
|
await expect(this.page.locator(".cm-editor")).toBeVisible()
|
||||||
expect(this.getErrors()).toStrictEqual([])
|
expect(this.getErrors()).toStrictEqual([])
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,19 +28,31 @@ export class HeynotePage {
|
|||||||
return await this.page.evaluate(() => window._heynote_editor.getBlocks())
|
return await this.page.evaluate(() => window._heynote_editor.getBlocks())
|
||||||
}
|
}
|
||||||
|
|
||||||
async getContent() {
|
async getBufferData() {
|
||||||
return await this.page.evaluate(() => window._heynote_editor.getContent())
|
return await this.page.evaluate(() => window._heynote_editor.getContent())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getContent() {
|
||||||
|
const note = NoteFormat.load(await this.getBufferData())
|
||||||
|
return note.content
|
||||||
|
}
|
||||||
|
|
||||||
async setContent(content) {
|
async setContent(content) {
|
||||||
await expect(this.page.locator("css=.cm-editor")).toBeVisible()
|
await expect(this.page.locator("css=.cm-editor")).toBeVisible()
|
||||||
await this.page.evaluate((content) => window._heynote_editor.setContent(content), content)
|
await this.page.evaluate(async (content) => {
|
||||||
|
await window._heynote_editor.setContent(content)
|
||||||
|
await window._heynote_editor.save()
|
||||||
|
}, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCursorPosition() {
|
async getCursorPosition() {
|
||||||
return await this.page.evaluate(() => window._heynote_editor.getCursorPosition())
|
return await this.page.evaluate(() => window._heynote_editor.getCursorPosition())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setCursorPosition(position) {
|
||||||
|
await this.page.evaluate((position) => window._heynote_editor.setCursorPosition(position), position)
|
||||||
|
}
|
||||||
|
|
||||||
async getBlockContent(blockIndex) {
|
async getBlockContent(blockIndex) {
|
||||||
const blocks = await this.getBlocks()
|
const blocks = await this.getBlocks()
|
||||||
const content = await this.getContent()
|
const content = await this.getContent()
|
||||||
@ -50,4 +64,32 @@ export class HeynotePage {
|
|||||||
async getStoredSettings() {
|
async getStoredSettings() {
|
||||||
return await this.page.evaluate(() => JSON.parse(window.localStorage.getItem("settings")))
|
return await this.page.evaluate(() => JSON.parse(window.localStorage.getItem("settings")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getStoredBufferList() {
|
||||||
|
return await this.page.evaluate(() => window.heynote.buffer.getList())
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStoredBuffer(path) {
|
||||||
|
return await this.page.evaluate((path) => window.heynote.buffer.load(path), path)
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveBuffer(path, content) {
|
||||||
|
const format = new NoteFormat()
|
||||||
|
format.content = content
|
||||||
|
await this.page.evaluate(({path, content}) => window.heynote.buffer.save(path, content), {path, content:format.serialize()})
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSettings() {
|
||||||
|
return await this.page.evaluate(() => {
|
||||||
|
return JSON.parse(window.localStorage.getItem("settings") || "{}")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async setSettings(settings) {
|
||||||
|
await this.page.evaluate((settings) => window.heynote.setSettings(settings), settings)
|
||||||
|
}
|
||||||
|
|
||||||
|
agnosticKey(key) {
|
||||||
|
return key.replace("Mod", this.isMac ? "Meta" : "Control")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,10 @@
|
|||||||
"lib": ["ESNext", "DOM"],
|
"lib": ["ESNext", "DOM"],
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"allowJs": true
|
"allowJs": true,
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"],
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": ["src"," shared-utils"],
|
"include": ["src"," shared-utils"],
|
||||||
"references": [
|
"references": [
|
||||||
|
@ -17,10 +17,8 @@ rmSync('dist-electron', { recursive: true, force: true })
|
|||||||
const isDevelopment = process.env.NODE_ENV === "development" || !!process.env.VSCODE_DEBUG
|
const isDevelopment = process.env.NODE_ENV === "development" || !!process.env.VSCODE_DEBUG
|
||||||
const isProduction = process.env.NODE_ENV === "production"
|
const isProduction = process.env.NODE_ENV === "production"
|
||||||
|
|
||||||
const updateReadmeKeybinds = async () => {
|
const injectKeybindsInDocs = async () => {
|
||||||
const readmePath = path.resolve(__dirname, 'README.md')
|
const keybindsRegex = /^(<!-- keyboard_shortcuts -->\s*).*?^(```\s+#)/gms
|
||||||
let readme = fs.readFileSync(readmePath, 'utf-8')
|
|
||||||
const keybindsRegex = /^(### What are the default keyboard shortcuts\?\s*).*?^(```\s+#)/gms
|
|
||||||
const shortcuts = `$1**On Mac**
|
const shortcuts = `$1**On Mac**
|
||||||
|
|
||||||
\`\`\`
|
\`\`\`
|
||||||
@ -32,8 +30,10 @@ ${keyHelpStr('darwin')}
|
|||||||
\`\`\`
|
\`\`\`
|
||||||
${keyHelpStr('win32')}
|
${keyHelpStr('win32')}
|
||||||
$2`
|
$2`
|
||||||
readme = readme.replace(keybindsRegex, shortcuts)
|
const docsPath = path.resolve(__dirname, 'docs', 'index.md')
|
||||||
fs.writeFileSync(readmePath, readme)
|
let docs = fs.readFileSync(docsPath, 'utf-8')
|
||||||
|
docs = docs.replace(keybindsRegex, shortcuts)
|
||||||
|
fs.writeFileSync(docsPath, docs)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateGuesslangLanguagesInWebWorker = async () => {
|
const updateGuesslangLanguagesInWebWorker = async () => {
|
||||||
@ -46,17 +46,19 @@ const updateGuesslangLanguagesInWebWorker = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
const resolveConfig = {
|
||||||
export default defineConfig({
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
alias: {
|
||||||
'@': path.resolve(__dirname),
|
'@': path.resolve(__dirname),
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
resolve: resolveConfig,
|
||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
updateReadmeKeybinds(),
|
injectKeybindsInDocs(),
|
||||||
updateGuesslangLanguagesInWebWorker(),
|
updateGuesslangLanguagesInWebWorker(),
|
||||||
electron([
|
electron([
|
||||||
{
|
{
|
||||||
@ -78,10 +80,11 @@ export default defineConfig({
|
|||||||
external: Object.keys("dependencies" in pkg ? pkg.dependencies : {}),
|
external: Object.keys("dependencies" in pkg ? pkg.dependencies : {}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
resolve: resolveConfig,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
entry: 'electron/preload/index.ts',
|
entry: 'electron/preload/index.js',
|
||||||
onstart(options) {
|
onstart(options) {
|
||||||
// Notify the Renderer-Process to reload the page when the Preload-Scripts build is complete,
|
// Notify the Renderer-Process to reload the page when the Preload-Scripts build is complete,
|
||||||
// instead of restarting the entire Electron App.
|
// instead of restarting the entire Electron App.
|
||||||
@ -96,6 +99,7 @@ export default defineConfig({
|
|||||||
external: Object.keys("dependencies" in pkg ? pkg.dependencies : {}),
|
external: Object.keys("dependencies" in pkg ? pkg.dependencies : {}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
resolve: resolveConfig,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -132,8 +136,9 @@ export default defineConfig({
|
|||||||
css: {
|
css: {
|
||||||
preprocessorOptions: {
|
preprocessorOptions: {
|
||||||
sass: {
|
sass: {
|
||||||
|
api: "modern-compiler",
|
||||||
additionalData: `
|
additionalData: `
|
||||||
@import "./src/css/include.sass"
|
@use "@/src/css/include.sass" as *
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
176
webapp/bridge.js
@ -1,4 +1,8 @@
|
|||||||
import { SETTINGS_CHANGE_EVENT, OPEN_SETTINGS_EVENT } from "../electron/constants";
|
import { Exception } from "sass";
|
||||||
|
import { SETTINGS_CHANGE_EVENT, OPEN_SETTINGS_EVENT } from "@/src/common/constants";
|
||||||
|
import { NoteFormat } from "../src/common/note-format";
|
||||||
|
|
||||||
|
const NOTE_KEY_PREFIX = "heynote-library__"
|
||||||
|
|
||||||
const mediaMatch = window.matchMedia('(prefers-color-scheme: dark)')
|
const mediaMatch = window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
let themeCallback = null
|
let themeCallback = null
|
||||||
@ -14,6 +18,16 @@ let autoUpdateCallbacks = null
|
|||||||
let currencyData = null
|
let currencyData = null
|
||||||
|
|
||||||
let platform
|
let platform
|
||||||
|
|
||||||
|
// In the latest version of Playwright, the window.navigator.userAgentData.platform is not reported correctly on Mac,
|
||||||
|
// wo we'll fallback to deprecated window.navigator.platform which still works
|
||||||
|
if (__TESTS__ && window.navigator.platform.indexOf("Mac") !== -1) {
|
||||||
|
platform = {
|
||||||
|
isMac: true,
|
||||||
|
isWindows: false,
|
||||||
|
isLinux: false,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
const uaPlatform = window.navigator?.userAgentData?.platform || window.navigator.platform
|
const uaPlatform = window.navigator?.userAgentData?.platform || window.navigator.platform
|
||||||
if (uaPlatform.indexOf("Win") !== -1) {
|
if (uaPlatform.indexOf("Win") !== -1) {
|
||||||
platform = {
|
platform = {
|
||||||
@ -34,6 +48,7 @@ if (uaPlatform.indexOf("Win") !== -1) {
|
|||||||
isLinux: false,
|
isLinux: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
platform.isWebApp = true
|
platform.isWebApp = true
|
||||||
|
|
||||||
|
|
||||||
@ -49,6 +64,12 @@ class IpcRenderer {
|
|||||||
this.callbacks[event].push(callback)
|
this.callbacks[event].push(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
off(event, callback) {
|
||||||
|
if (this.callbacks[event]) {
|
||||||
|
this.callbacks[event] = this.callbacks[event].filter(cb => cb !== callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
send(event, ...args) {
|
send(event, ...args) {
|
||||||
if (this.callbacks[event]) {
|
if (this.callbacks[event]) {
|
||||||
for (const callback of this.callbacks[event]) {
|
for (const callback of this.callbacks[event]) {
|
||||||
@ -68,11 +89,55 @@ let initialSettings = {
|
|||||||
showLineNumberGutter: true,
|
showLineNumberGutter: true,
|
||||||
showFoldGutter: true,
|
showFoldGutter: true,
|
||||||
bracketClosing: false,
|
bracketClosing: false,
|
||||||
|
keyBindings: [],
|
||||||
}
|
}
|
||||||
if (settingsData !== null) {
|
if (settingsData !== null) {
|
||||||
initialSettings = Object.assign(initialSettings, JSON.parse(settingsData))
|
initialSettings = Object.assign(initialSettings, JSON.parse(settingsData))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function noteKey(path) {
|
||||||
|
return NOTE_KEY_PREFIX + path
|
||||||
|
}
|
||||||
|
|
||||||
|
function getNoteMetadata(content) {
|
||||||
|
const firstSeparator = content.indexOf("\n∞∞∞")
|
||||||
|
if (firstSeparator === -1) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const metadata = JSON.parse(content.slice(0, firstSeparator).trim())
|
||||||
|
return {"name": metadata.name}
|
||||||
|
} catch (e) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate single buffer (Heynote pre 2.0) in localStorage to notes library
|
||||||
|
// At some point we can remove this migration code
|
||||||
|
function migrateBufferFileToLibrary() {
|
||||||
|
if (!("buffer" in localStorage)) {
|
||||||
|
// nothing to migrate
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (Object.keys(localStorage).filter(key => key.startsWith(NOTE_KEY_PREFIX)).length > 0) {
|
||||||
|
// already migrated
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Migrating single buffer to notes library")
|
||||||
|
|
||||||
|
let content = localStorage.getItem("buffer")
|
||||||
|
const metadata = getNoteMetadata(content)
|
||||||
|
if (!metadata || !metadata.name) {
|
||||||
|
console.log("Adding metadata to Scratch note")
|
||||||
|
const note = NoteFormat.load(content)
|
||||||
|
note.metadata.name = "Scratch"
|
||||||
|
content = note.serialize()
|
||||||
|
}
|
||||||
|
localStorage.setItem("heynote-library__scratch.txt", content)
|
||||||
|
localStorage.removeItem("buffer")
|
||||||
|
}
|
||||||
|
migrateBufferFileToLibrary()
|
||||||
|
|
||||||
const Heynote = {
|
const Heynote = {
|
||||||
platform: platform,
|
platform: platform,
|
||||||
@ -80,38 +145,99 @@ const Heynote = {
|
|||||||
defaultFontSize: isMobileDevice ? 16 : 12,
|
defaultFontSize: isMobileDevice ? 16 : 12,
|
||||||
|
|
||||||
buffer: {
|
buffer: {
|
||||||
async load() {
|
async load(path) {
|
||||||
const content = localStorage.getItem("buffer")
|
//console.log("loading", path)
|
||||||
return content === null ? "\n∞∞∞text-a\n" : content
|
const content = localStorage.getItem(noteKey(path))
|
||||||
|
return content === null ? '{"formatVersion":"1.0.0","name":"Scratch"}\n∞∞∞text-a\n' : content
|
||||||
},
|
},
|
||||||
|
|
||||||
async save(content) {
|
async save(path, content) {
|
||||||
localStorage.setItem("buffer", content)
|
//console.log("saving", path, content)
|
||||||
|
localStorage.setItem(noteKey(path), content)
|
||||||
},
|
},
|
||||||
|
|
||||||
async saveAndQuit(content) {
|
async create(path, content) {
|
||||||
|
localStorage.setItem(noteKey(path), content)
|
||||||
|
},
|
||||||
|
|
||||||
|
async delete(path) {
|
||||||
|
localStorage.removeItem(noteKey(path))
|
||||||
|
},
|
||||||
|
|
||||||
|
async move(path, newPath) {
|
||||||
|
const content = localStorage.getItem(noteKey(path))
|
||||||
|
localStorage.setItem(noteKey(newPath), content)
|
||||||
|
localStorage.removeItem(noteKey(path))
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveAndQuit(contents) {
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onChangeCallback(callback) {
|
async exists(path) {
|
||||||
|
return localStorage.getItem(noteKey(path)) !== null
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onWindowClose(callback) {
|
async getList() {
|
||||||
//ipcRenderer.on(WINDOW_CLOSE_EVENT, callback)
|
//return {"scratch.txt": {name:"Scratch"}}
|
||||||
|
const notes = {}
|
||||||
|
for (let [key, content] of Object.entries(localStorage)) {
|
||||||
|
if (key.startsWith(NOTE_KEY_PREFIX)) {
|
||||||
|
const path = key.slice(NOTE_KEY_PREFIX.length)
|
||||||
|
notes[path] = getNoteMetadata(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return notes
|
||||||
|
},
|
||||||
|
|
||||||
|
async getDirectoryList() {
|
||||||
|
const directories = new Set()
|
||||||
|
for (let key in localStorage) {
|
||||||
|
if (key.startsWith(NOTE_KEY_PREFIX)) {
|
||||||
|
const path = key.slice(NOTE_KEY_PREFIX.length)
|
||||||
|
const parts = path.split("/")
|
||||||
|
if (parts.length > 1) {
|
||||||
|
for (let i = 1; i < parts.length; i++) {
|
||||||
|
directories.add(parts.slice(0, i).join("/"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//console.log("directories", directories)
|
||||||
|
return [...directories]
|
||||||
|
},
|
||||||
|
|
||||||
|
async close(path) {
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
_onChangeCallbacks: {},
|
||||||
|
addOnChangeCallback(path, callback) {
|
||||||
|
|
||||||
|
},
|
||||||
|
removeOnChangeCallback(path, callback) {
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
pathSeparator: "/",
|
||||||
|
},
|
||||||
|
|
||||||
|
mainProcess: {
|
||||||
|
on(event, callback) {
|
||||||
|
ipcRenderer.on(event, callback)
|
||||||
|
},
|
||||||
|
|
||||||
|
off(event, callback) {
|
||||||
|
ipcRenderer.off(event, callback)
|
||||||
|
},
|
||||||
|
|
||||||
|
invoke(event, ...args) {
|
||||||
|
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
settings: initialSettings,
|
settings: initialSettings,
|
||||||
|
|
||||||
onOpenSettings(callback) {
|
|
||||||
ipcRenderer.on(OPEN_SETTINGS_EVENT, callback)
|
|
||||||
},
|
|
||||||
|
|
||||||
onSettingsChange(callback) {
|
|
||||||
ipcRenderer.on(SETTINGS_CHANGE_EVENT, (event, settings) => callback(settings))
|
|
||||||
},
|
|
||||||
|
|
||||||
setSettings(settings) {
|
setSettings(settings) {
|
||||||
localStorage.setItem("settings", JSON.stringify(settings))
|
localStorage.setItem("settings", JSON.stringify(settings))
|
||||||
ipcRenderer.send(SETTINGS_CHANGE_EVENT, settings)
|
ipcRenderer.send(SETTINGS_CHANGE_EVENT, settings)
|
||||||
@ -121,7 +247,7 @@ const Heynote = {
|
|||||||
set: (mode) => {
|
set: (mode) => {
|
||||||
localStorage.setItem("theme", mode)
|
localStorage.setItem("theme", mode)
|
||||||
themeCallback(mode)
|
themeCallback(mode)
|
||||||
console.log("set theme to", mode)
|
//console.log("set theme to", mode)
|
||||||
},
|
},
|
||||||
get: async () => {
|
get: async () => {
|
||||||
const theme = localStorage.getItem("theme") || "system"
|
const theme = localStorage.getItem("theme") || "system"
|
||||||
@ -152,6 +278,14 @@ const Heynote = {
|
|||||||
async getVersion() {
|
async getVersion() {
|
||||||
return __APP_VERSION__ + " (" + __GIT_HASH__ + ")"
|
return __APP_VERSION__ + " (" + __GIT_HASH__ + ")"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async getInitErrors() {
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
setWindowTitle(title) {
|
||||||
|
document.title = title + " - Heynote"
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Heynote, ipcRenderer}
|
export { Heynote, ipcRenderer}
|
||||||
|