merge file handling, tree-sitter, and editor intelligence
This commit is contained in:
commit
df35ed406e
396
Info.plist
396
Info.plist
|
|
@ -28,5 +28,401 @@
|
||||||
<false/>
|
<false/>
|
||||||
<key>NSSupportsSuddenTermination</key>
|
<key>NSSupportsSuddenTermination</key>
|
||||||
<false/>
|
<false/>
|
||||||
|
<key>CFBundleDocumentTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Markdown</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
<key>LSItemContentTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>net.daringfireball.markdown</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>md</string>
|
||||||
|
<string>markdown</string>
|
||||||
|
<string>mdown</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>CSV</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
<key>LSItemContentTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>public.comma-separated-values-text</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>csv</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>JSON</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
<key>LSItemContentTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>public.json</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>json</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>TOML</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>toml</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>YAML</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>yaml</string>
|
||||||
|
<string>yml</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>XML</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
<key>LSItemContentTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>public.xml</string>
|
||||||
|
<string>public.svg-image</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>xml</string>
|
||||||
|
<string>svg</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Rust Source</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>rs</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>C/C++ Source</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
<key>LSItemContentTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>public.c-source</string>
|
||||||
|
<string>public.c-plus-plus-source</string>
|
||||||
|
<string>public.c-header</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>c</string>
|
||||||
|
<string>cpp</string>
|
||||||
|
<string>cc</string>
|
||||||
|
<string>cxx</string>
|
||||||
|
<string>h</string>
|
||||||
|
<string>hpp</string>
|
||||||
|
<string>hxx</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>JavaScript/TypeScript</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
<key>LSItemContentTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>com.netscape.javascript-source</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>js</string>
|
||||||
|
<string>jsx</string>
|
||||||
|
<string>ts</string>
|
||||||
|
<string>tsx</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>HTML</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
<key>LSItemContentTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>public.html</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>html</string>
|
||||||
|
<string>htm</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>CSS</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>css</string>
|
||||||
|
<string>scss</string>
|
||||||
|
<string>less</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Python Source</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
<key>LSItemContentTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>public.python-script</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>py</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Go Source</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>go</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Ruby Source</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
<key>LSItemContentTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>public.ruby-script</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>rb</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>PHP Source</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
<key>LSItemContentTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>public.php-script</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>php</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Lua Source</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>lua</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Shell Script</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
<key>LSItemContentTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>public.shell-script</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>sh</string>
|
||||||
|
<string>bash</string>
|
||||||
|
<string>zsh</string>
|
||||||
|
<string>fish</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Java/Kotlin Source</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>java</string>
|
||||||
|
<string>kt</string>
|
||||||
|
<string>kts</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Swift Source</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
<key>LSItemContentTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>public.swift-source</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>swift</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Zig Source</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>zig</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>SQL</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>sql</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Makefile</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>mk</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Dockerfile</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>dockerfile</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Configuration</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>ini</string>
|
||||||
|
<string>cfg</string>
|
||||||
|
<string>conf</string>
|
||||||
|
<string>env</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Lock File</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>lock</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>Plain Text</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Alternate</string>
|
||||||
|
<key>LSItemContentTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>public.plain-text</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>txt</string>
|
||||||
|
<string>text</string>
|
||||||
|
<string>log</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,34 @@ serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
uuid = { version = "1", features = ["v4"] }
|
uuid = { version = "1", features = ["v4"] }
|
||||||
|
|
||||||
|
tree-sitter = "0.24"
|
||||||
|
tree-sitter-highlight = "0.24"
|
||||||
|
tree-sitter-language = "0.1"
|
||||||
|
tree-sitter-rust = "0.23"
|
||||||
|
tree-sitter-c = "0.23"
|
||||||
|
tree-sitter-cpp = "0.23"
|
||||||
|
tree-sitter-javascript = "0.23"
|
||||||
|
tree-sitter-typescript = "0.23"
|
||||||
|
tree-sitter-python = "0.23"
|
||||||
|
tree-sitter-go = "0.23"
|
||||||
|
tree-sitter-ruby = "0.23"
|
||||||
|
tree-sitter-bash = "0.23"
|
||||||
|
tree-sitter-java = "0.23"
|
||||||
|
tree-sitter-html = "0.23"
|
||||||
|
tree-sitter-css = "0.23"
|
||||||
|
tree-sitter-json = "0.24"
|
||||||
|
tree-sitter-lua = "0.4"
|
||||||
|
tree-sitter-php = "0.23"
|
||||||
|
tree-sitter-toml = "0.20"
|
||||||
|
tree-sitter-yaml = "0.6"
|
||||||
|
tree-sitter-swift = "0.6"
|
||||||
|
tree-sitter-zig = "1"
|
||||||
|
tree-sitter-sql = "0.0.2"
|
||||||
|
tree-sitter-md = "0.5"
|
||||||
|
tree-sitter-make = "1"
|
||||||
|
tree-sitter-dockerfile = "0.2"
|
||||||
|
tree-sitter-kotlin = "0.3"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
cbindgen = "0.27"
|
cbindgen = "0.27"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ typedef struct SwiftlyDoc SwiftlyDoc;
|
||||||
|
|
||||||
char *swiftly_list_notes(void);
|
char *swiftly_list_notes(void);
|
||||||
|
|
||||||
|
char *swiftly_highlight(const char *source, const char *lang);
|
||||||
|
|
||||||
void swiftly_free_string(char *s);
|
void swiftly_free_string(char *s);
|
||||||
|
|
||||||
#endif /* SWIFTLY_H */
|
#endif /* SWIFTLY_H */
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
[
|
||||||
|
"FROM"
|
||||||
|
"AS"
|
||||||
|
"RUN"
|
||||||
|
"CMD"
|
||||||
|
"LABEL"
|
||||||
|
"EXPOSE"
|
||||||
|
"ENV"
|
||||||
|
"ADD"
|
||||||
|
"COPY"
|
||||||
|
"ENTRYPOINT"
|
||||||
|
"VOLUME"
|
||||||
|
"USER"
|
||||||
|
"WORKDIR"
|
||||||
|
"ARG"
|
||||||
|
"ONBUILD"
|
||||||
|
"STOPSIGNAL"
|
||||||
|
"HEALTHCHECK"
|
||||||
|
"SHELL"
|
||||||
|
"MAINTAINER"
|
||||||
|
"CROSS_BUILD"
|
||||||
|
(heredoc_marker)
|
||||||
|
(heredoc_end)
|
||||||
|
] @keyword
|
||||||
|
|
||||||
|
[
|
||||||
|
":"
|
||||||
|
"@"
|
||||||
|
] @operator
|
||||||
|
|
||||||
|
(comment) @comment
|
||||||
|
|
||||||
|
|
||||||
|
(image_spec
|
||||||
|
(image_tag
|
||||||
|
":" @punctuation.special)
|
||||||
|
(image_digest
|
||||||
|
"@" @punctuation.special))
|
||||||
|
|
||||||
|
[
|
||||||
|
(double_quoted_string)
|
||||||
|
(single_quoted_string)
|
||||||
|
(json_string)
|
||||||
|
(heredoc_line)
|
||||||
|
] @string
|
||||||
|
|
||||||
|
(expansion
|
||||||
|
[
|
||||||
|
"$"
|
||||||
|
"{"
|
||||||
|
"}"
|
||||||
|
] @punctuation.special
|
||||||
|
) @none
|
||||||
|
|
||||||
|
((variable) @constant
|
||||||
|
(#match? @constant "^[A-Z][A-Z_0-9]*$"))
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,380 @@
|
||||||
|
;; Based on the nvim-treesitter highlighting, which is under the Apache license.
|
||||||
|
;; See https://github.com/nvim-treesitter/nvim-treesitter/blob/f8ab59861eed4a1c168505e3433462ed800f2bae/queries/kotlin/highlights.scm
|
||||||
|
;;
|
||||||
|
;; The only difference in this file is that queries using #lua-match?
|
||||||
|
;; have been removed.
|
||||||
|
|
||||||
|
;;; Identifiers
|
||||||
|
|
||||||
|
(simple_identifier) @variable
|
||||||
|
|
||||||
|
; `it` keyword inside lambdas
|
||||||
|
; FIXME: This will highlight the keyword outside of lambdas since tree-sitter
|
||||||
|
; does not allow us to check for arbitrary nestation
|
||||||
|
((simple_identifier) @variable.builtin
|
||||||
|
(#eq? @variable.builtin "it"))
|
||||||
|
|
||||||
|
; `field` keyword inside property getter/setter
|
||||||
|
; FIXME: This will highlight the keyword outside of getters and setters
|
||||||
|
; since tree-sitter does not allow us to check for arbitrary nestation
|
||||||
|
((simple_identifier) @variable.builtin
|
||||||
|
(#eq? @variable.builtin "field"))
|
||||||
|
|
||||||
|
; `this` this keyword inside classes
|
||||||
|
(this_expression) @variable.builtin
|
||||||
|
|
||||||
|
; `super` keyword inside classes
|
||||||
|
(super_expression) @variable.builtin
|
||||||
|
|
||||||
|
(class_parameter
|
||||||
|
(simple_identifier) @property)
|
||||||
|
|
||||||
|
(class_body
|
||||||
|
(property_declaration
|
||||||
|
(variable_declaration
|
||||||
|
(simple_identifier) @property)))
|
||||||
|
|
||||||
|
; id_1.id_2.id_3: `id_2` and `id_3` are assumed as object properties
|
||||||
|
(_
|
||||||
|
(navigation_suffix
|
||||||
|
(simple_identifier) @property))
|
||||||
|
|
||||||
|
(enum_entry
|
||||||
|
(simple_identifier) @constant)
|
||||||
|
|
||||||
|
(type_identifier) @type
|
||||||
|
|
||||||
|
((type_identifier) @type.builtin
|
||||||
|
(#any-of? @type.builtin
|
||||||
|
"Byte"
|
||||||
|
"Short"
|
||||||
|
"Int"
|
||||||
|
"Long"
|
||||||
|
"UByte"
|
||||||
|
"UShort"
|
||||||
|
"UInt"
|
||||||
|
"ULong"
|
||||||
|
"Float"
|
||||||
|
"Double"
|
||||||
|
"Boolean"
|
||||||
|
"Char"
|
||||||
|
"String"
|
||||||
|
"Array"
|
||||||
|
"ByteArray"
|
||||||
|
"ShortArray"
|
||||||
|
"IntArray"
|
||||||
|
"LongArray"
|
||||||
|
"UByteArray"
|
||||||
|
"UShortArray"
|
||||||
|
"UIntArray"
|
||||||
|
"ULongArray"
|
||||||
|
"FloatArray"
|
||||||
|
"DoubleArray"
|
||||||
|
"BooleanArray"
|
||||||
|
"CharArray"
|
||||||
|
"Map"
|
||||||
|
"Set"
|
||||||
|
"List"
|
||||||
|
"EmptyMap"
|
||||||
|
"EmptySet"
|
||||||
|
"EmptyList"
|
||||||
|
"MutableMap"
|
||||||
|
"MutableSet"
|
||||||
|
"MutableList"
|
||||||
|
))
|
||||||
|
|
||||||
|
(package_header
|
||||||
|
. (identifier)) @namespace
|
||||||
|
|
||||||
|
(import_header
|
||||||
|
"import" @include)
|
||||||
|
|
||||||
|
|
||||||
|
; TODO: Seperate labeled returns/breaks/continue/super/this
|
||||||
|
; Must be implemented in the parser first
|
||||||
|
(label) @label
|
||||||
|
|
||||||
|
;;; Function definitions
|
||||||
|
|
||||||
|
(function_declaration
|
||||||
|
. (simple_identifier) @function)
|
||||||
|
|
||||||
|
(getter
|
||||||
|
("get") @function.builtin)
|
||||||
|
(setter
|
||||||
|
("set") @function.builtin)
|
||||||
|
|
||||||
|
(primary_constructor) @constructor
|
||||||
|
(secondary_constructor
|
||||||
|
("constructor") @constructor)
|
||||||
|
|
||||||
|
(constructor_invocation
|
||||||
|
(user_type
|
||||||
|
(type_identifier) @constructor))
|
||||||
|
|
||||||
|
(anonymous_initializer
|
||||||
|
("init") @constructor)
|
||||||
|
|
||||||
|
(parameter
|
||||||
|
(simple_identifier) @parameter)
|
||||||
|
|
||||||
|
(parameter_with_optional_type
|
||||||
|
(simple_identifier) @parameter)
|
||||||
|
|
||||||
|
; lambda parameters
|
||||||
|
(lambda_literal
|
||||||
|
(lambda_parameters
|
||||||
|
(variable_declaration
|
||||||
|
(simple_identifier) @parameter)))
|
||||||
|
|
||||||
|
;;; Function calls
|
||||||
|
|
||||||
|
; function()
|
||||||
|
(call_expression
|
||||||
|
. (simple_identifier) @function)
|
||||||
|
|
||||||
|
; object.function() or object.property.function()
|
||||||
|
(call_expression
|
||||||
|
(navigation_expression
|
||||||
|
(navigation_suffix
|
||||||
|
(simple_identifier) @function) . ))
|
||||||
|
|
||||||
|
(call_expression
|
||||||
|
. (simple_identifier) @function.builtin
|
||||||
|
(#any-of? @function.builtin
|
||||||
|
"arrayOf"
|
||||||
|
"arrayOfNulls"
|
||||||
|
"byteArrayOf"
|
||||||
|
"shortArrayOf"
|
||||||
|
"intArrayOf"
|
||||||
|
"longArrayOf"
|
||||||
|
"ubyteArrayOf"
|
||||||
|
"ushortArrayOf"
|
||||||
|
"uintArrayOf"
|
||||||
|
"ulongArrayOf"
|
||||||
|
"floatArrayOf"
|
||||||
|
"doubleArrayOf"
|
||||||
|
"booleanArrayOf"
|
||||||
|
"charArrayOf"
|
||||||
|
"emptyArray"
|
||||||
|
"mapOf"
|
||||||
|
"setOf"
|
||||||
|
"listOf"
|
||||||
|
"emptyMap"
|
||||||
|
"emptySet"
|
||||||
|
"emptyList"
|
||||||
|
"mutableMapOf"
|
||||||
|
"mutableSetOf"
|
||||||
|
"mutableListOf"
|
||||||
|
"print"
|
||||||
|
"println"
|
||||||
|
"error"
|
||||||
|
"TODO"
|
||||||
|
"run"
|
||||||
|
"runCatching"
|
||||||
|
"repeat"
|
||||||
|
"lazy"
|
||||||
|
"lazyOf"
|
||||||
|
"enumValues"
|
||||||
|
"enumValueOf"
|
||||||
|
"assert"
|
||||||
|
"check"
|
||||||
|
"checkNotNull"
|
||||||
|
"require"
|
||||||
|
"requireNotNull"
|
||||||
|
"with"
|
||||||
|
"suspend"
|
||||||
|
"synchronized"
|
||||||
|
))
|
||||||
|
|
||||||
|
;;; Literals
|
||||||
|
|
||||||
|
[
|
||||||
|
(line_comment)
|
||||||
|
(multiline_comment)
|
||||||
|
(shebang_line)
|
||||||
|
] @comment
|
||||||
|
|
||||||
|
(real_literal) @float
|
||||||
|
[
|
||||||
|
(integer_literal)
|
||||||
|
(long_literal)
|
||||||
|
(hex_literal)
|
||||||
|
(bin_literal)
|
||||||
|
(unsigned_literal)
|
||||||
|
] @number
|
||||||
|
|
||||||
|
[
|
||||||
|
"null" ; should be highlighted the same as booleans
|
||||||
|
(boolean_literal)
|
||||||
|
] @boolean
|
||||||
|
|
||||||
|
(character_literal) @character
|
||||||
|
|
||||||
|
(string_literal) @string
|
||||||
|
|
||||||
|
(character_escape_seq) @string.escape
|
||||||
|
|
||||||
|
; There are 3 ways to define a regex
|
||||||
|
; - "[abc]?".toRegex()
|
||||||
|
(call_expression
|
||||||
|
(navigation_expression
|
||||||
|
((string_literal) @string.regex)
|
||||||
|
(navigation_suffix
|
||||||
|
((simple_identifier) @_function
|
||||||
|
(#eq? @_function "toRegex")))))
|
||||||
|
|
||||||
|
; - Regex("[abc]?")
|
||||||
|
(call_expression
|
||||||
|
((simple_identifier) @_function
|
||||||
|
(#eq? @_function "Regex"))
|
||||||
|
(call_suffix
|
||||||
|
(value_arguments
|
||||||
|
(value_argument
|
||||||
|
(string_literal) @string.regex))))
|
||||||
|
|
||||||
|
; - Regex.fromLiteral("[abc]?")
|
||||||
|
(call_expression
|
||||||
|
(navigation_expression
|
||||||
|
((simple_identifier) @_class
|
||||||
|
(#eq? @_class "Regex"))
|
||||||
|
(navigation_suffix
|
||||||
|
((simple_identifier) @_function
|
||||||
|
(#eq? @_function "fromLiteral"))))
|
||||||
|
(call_suffix
|
||||||
|
(value_arguments
|
||||||
|
(value_argument
|
||||||
|
(string_literal) @string.regex))))
|
||||||
|
|
||||||
|
;;; Keywords
|
||||||
|
|
||||||
|
(type_alias "typealias" @keyword)
|
||||||
|
[
|
||||||
|
(class_modifier)
|
||||||
|
(member_modifier)
|
||||||
|
(function_modifier)
|
||||||
|
(property_modifier)
|
||||||
|
(platform_modifier)
|
||||||
|
(variance_modifier)
|
||||||
|
(parameter_modifier)
|
||||||
|
(visibility_modifier)
|
||||||
|
(reification_modifier)
|
||||||
|
(inheritance_modifier)
|
||||||
|
]@keyword
|
||||||
|
|
||||||
|
[
|
||||||
|
"val"
|
||||||
|
"var"
|
||||||
|
"enum"
|
||||||
|
"class"
|
||||||
|
"object"
|
||||||
|
"interface"
|
||||||
|
; "typeof" ; NOTE: It is reserved for future use
|
||||||
|
] @keyword
|
||||||
|
|
||||||
|
("fun") @keyword.function
|
||||||
|
|
||||||
|
(jump_expression) @keyword.return
|
||||||
|
|
||||||
|
[
|
||||||
|
"if"
|
||||||
|
"else"
|
||||||
|
"when"
|
||||||
|
] @conditional
|
||||||
|
|
||||||
|
[
|
||||||
|
"for"
|
||||||
|
"do"
|
||||||
|
"while"
|
||||||
|
] @repeat
|
||||||
|
|
||||||
|
[
|
||||||
|
"try"
|
||||||
|
"catch"
|
||||||
|
"throw"
|
||||||
|
"finally"
|
||||||
|
] @exception
|
||||||
|
|
||||||
|
|
||||||
|
(annotation
|
||||||
|
"@" @attribute (use_site_target)? @attribute)
|
||||||
|
(annotation
|
||||||
|
(user_type
|
||||||
|
(type_identifier) @attribute))
|
||||||
|
(annotation
|
||||||
|
(constructor_invocation
|
||||||
|
(user_type
|
||||||
|
(type_identifier) @attribute)))
|
||||||
|
|
||||||
|
(file_annotation
|
||||||
|
"@" @attribute "file" @attribute ":" @attribute)
|
||||||
|
(file_annotation
|
||||||
|
(user_type
|
||||||
|
(type_identifier) @attribute))
|
||||||
|
(file_annotation
|
||||||
|
(constructor_invocation
|
||||||
|
(user_type
|
||||||
|
(type_identifier) @attribute)))
|
||||||
|
|
||||||
|
;;; Operators & Punctuation
|
||||||
|
|
||||||
|
[
|
||||||
|
"!"
|
||||||
|
"!="
|
||||||
|
"!=="
|
||||||
|
"="
|
||||||
|
"=="
|
||||||
|
"==="
|
||||||
|
">"
|
||||||
|
">="
|
||||||
|
"<"
|
||||||
|
"<="
|
||||||
|
"||"
|
||||||
|
"&&"
|
||||||
|
"+"
|
||||||
|
"++"
|
||||||
|
"+="
|
||||||
|
"-"
|
||||||
|
"--"
|
||||||
|
"-="
|
||||||
|
"*"
|
||||||
|
"*="
|
||||||
|
"/"
|
||||||
|
"/="
|
||||||
|
"%"
|
||||||
|
"%="
|
||||||
|
"?."
|
||||||
|
"?:"
|
||||||
|
"!!"
|
||||||
|
"is"
|
||||||
|
"!is"
|
||||||
|
"in"
|
||||||
|
"!in"
|
||||||
|
"as"
|
||||||
|
"as?"
|
||||||
|
".."
|
||||||
|
"->"
|
||||||
|
] @operator
|
||||||
|
|
||||||
|
[
|
||||||
|
"(" ")"
|
||||||
|
"[" "]"
|
||||||
|
"{" "}"
|
||||||
|
] @punctuation.bracket
|
||||||
|
|
||||||
|
[
|
||||||
|
"."
|
||||||
|
","
|
||||||
|
";"
|
||||||
|
":"
|
||||||
|
"::"
|
||||||
|
] @punctuation.delimiter
|
||||||
|
|
||||||
|
; NOTE: `interpolated_identifier`s can be highlighted in any way
|
||||||
|
(string_literal
|
||||||
|
"$" @punctuation.special
|
||||||
|
(interpolated_identifier) @none)
|
||||||
|
(string_literal
|
||||||
|
"${" @punctuation.special
|
||||||
|
(interpolated_expression) @none
|
||||||
|
"}" @punctuation.special)
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
; (identifier) @variable FIXME this overrides function call pattern
|
||||||
|
(string) @string
|
||||||
|
(number) @number
|
||||||
|
(comment) @comment
|
||||||
|
|
||||||
|
(function_call
|
||||||
|
function: (identifier) @function)
|
||||||
|
|
||||||
|
[
|
||||||
|
(NULL)
|
||||||
|
(TRUE)
|
||||||
|
(FALSE)
|
||||||
|
] @constant.builtin
|
||||||
|
|
||||||
|
[
|
||||||
|
"<"
|
||||||
|
"<="
|
||||||
|
"<>"
|
||||||
|
"="
|
||||||
|
">"
|
||||||
|
">="
|
||||||
|
"::"
|
||||||
|
] @operator
|
||||||
|
|
||||||
|
[
|
||||||
|
"("
|
||||||
|
")"
|
||||||
|
"["
|
||||||
|
"]"
|
||||||
|
] @punctuation.bracket
|
||||||
|
|
||||||
|
[
|
||||||
|
(type)
|
||||||
|
(array_type)
|
||||||
|
] @type
|
||||||
|
|
||||||
|
|
||||||
|
[
|
||||||
|
"CREATE TABLE"
|
||||||
|
"CREATE TYPE"
|
||||||
|
"CREATE DOMAIN"
|
||||||
|
"CREATE"
|
||||||
|
"INDEX"
|
||||||
|
"UNIQUE"
|
||||||
|
"SELECT"
|
||||||
|
"WHERE"
|
||||||
|
"FROM"
|
||||||
|
"AS"
|
||||||
|
"GROUP BY"
|
||||||
|
"ORDER BY"
|
||||||
|
] @keyword
|
||||||
|
|
@ -4,6 +4,7 @@ use std::path::Path;
|
||||||
|
|
||||||
use crate::document::SwiftlyDoc;
|
use crate::document::SwiftlyDoc;
|
||||||
use crate::eval;
|
use crate::eval;
|
||||||
|
use crate::highlight;
|
||||||
use crate::persist;
|
use crate::persist;
|
||||||
|
|
||||||
fn cstr_to_str<'a>(ptr: *const c_char) -> Option<&'a str> {
|
fn cstr_to_str<'a>(ptr: *const c_char) -> Option<&'a str> {
|
||||||
|
|
@ -136,6 +137,21 @@ pub extern "C" fn swiftly_list_notes() -> *mut c_char {
|
||||||
str_to_cstr(&json)
|
str_to_cstr(&json)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn swiftly_highlight(source: *const c_char, lang: *const c_char) -> *mut c_char {
|
||||||
|
let source = match cstr_to_str(source) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => return str_to_cstr("[]"),
|
||||||
|
};
|
||||||
|
let lang = match cstr_to_str(lang) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => return str_to_cstr("[]"),
|
||||||
|
};
|
||||||
|
let spans = highlight::highlight_source(source, lang);
|
||||||
|
let json = serde_json::to_string(&spans).unwrap_or_else(|_| "[]".into());
|
||||||
|
str_to_cstr(&json)
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn swiftly_free_string(s: *mut c_char) {
|
pub extern "C" fn swiftly_free_string(s: *mut c_char) {
|
||||||
if s.is_null() { return; }
|
if s.is_null() { return; }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,252 @@
|
||||||
|
use tree_sitter_highlight::{HighlightConfiguration, HighlightEvent, Highlighter};
|
||||||
|
use tree_sitter::Language;
|
||||||
|
|
||||||
|
/// Convert an old-API tree-sitter Language (v0.19/v0.20) to current v0.24.
|
||||||
|
/// Both are newtype wrappers around `*const TSLanguage`; the C ABI is identical.
|
||||||
|
unsafe fn lang_compat<T>(old: T) -> Language {
|
||||||
|
std::mem::transmute_copy(&old)
|
||||||
|
}
|
||||||
|
|
||||||
|
const HIGHLIGHT_NAMES: &[&str] = &[
|
||||||
|
"keyword",
|
||||||
|
"function",
|
||||||
|
"function.builtin",
|
||||||
|
"type",
|
||||||
|
"type.builtin",
|
||||||
|
"constructor",
|
||||||
|
"constant",
|
||||||
|
"constant.builtin",
|
||||||
|
"string",
|
||||||
|
"number",
|
||||||
|
"comment",
|
||||||
|
"variable",
|
||||||
|
"variable.builtin",
|
||||||
|
"variable.parameter",
|
||||||
|
"operator",
|
||||||
|
"punctuation",
|
||||||
|
"punctuation.bracket",
|
||||||
|
"punctuation.delimiter",
|
||||||
|
"property",
|
||||||
|
"tag",
|
||||||
|
"attribute",
|
||||||
|
"label",
|
||||||
|
"escape",
|
||||||
|
"embedded",
|
||||||
|
];
|
||||||
|
|
||||||
|
#[derive(serde::Serialize)]
|
||||||
|
pub struct HighlightSpan {
|
||||||
|
pub start: usize,
|
||||||
|
pub end: usize,
|
||||||
|
pub kind: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LangDef {
|
||||||
|
language: Language,
|
||||||
|
highlights: &'static str,
|
||||||
|
injections: &'static str,
|
||||||
|
locals: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lang_def(lang_id: &str) -> Option<LangDef> {
|
||||||
|
let ld = match lang_id {
|
||||||
|
"rust" => LangDef {
|
||||||
|
language: tree_sitter_rust::LANGUAGE.into(),
|
||||||
|
highlights: tree_sitter_rust::HIGHLIGHTS_QUERY,
|
||||||
|
injections: tree_sitter_rust::INJECTIONS_QUERY,
|
||||||
|
locals: "",
|
||||||
|
},
|
||||||
|
"c" => LangDef {
|
||||||
|
language: tree_sitter_c::LANGUAGE.into(),
|
||||||
|
highlights: tree_sitter_c::HIGHLIGHT_QUERY,
|
||||||
|
injections: "",
|
||||||
|
locals: "",
|
||||||
|
},
|
||||||
|
"cpp" => LangDef {
|
||||||
|
language: tree_sitter_cpp::LANGUAGE.into(),
|
||||||
|
highlights: tree_sitter_cpp::HIGHLIGHT_QUERY,
|
||||||
|
injections: "",
|
||||||
|
locals: "",
|
||||||
|
},
|
||||||
|
"javascript" | "jsx" => LangDef {
|
||||||
|
language: tree_sitter_javascript::LANGUAGE.into(),
|
||||||
|
highlights: tree_sitter_javascript::HIGHLIGHT_QUERY,
|
||||||
|
injections: tree_sitter_javascript::INJECTIONS_QUERY,
|
||||||
|
locals: tree_sitter_javascript::LOCALS_QUERY,
|
||||||
|
},
|
||||||
|
"typescript" => LangDef {
|
||||||
|
language: tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
|
||||||
|
highlights: tree_sitter_typescript::HIGHLIGHTS_QUERY,
|
||||||
|
injections: "",
|
||||||
|
locals: tree_sitter_typescript::LOCALS_QUERY,
|
||||||
|
},
|
||||||
|
"tsx" => LangDef {
|
||||||
|
language: tree_sitter_typescript::LANGUAGE_TSX.into(),
|
||||||
|
highlights: tree_sitter_typescript::HIGHLIGHTS_QUERY,
|
||||||
|
injections: "",
|
||||||
|
locals: tree_sitter_typescript::LOCALS_QUERY,
|
||||||
|
},
|
||||||
|
"python" => LangDef {
|
||||||
|
language: tree_sitter_python::LANGUAGE.into(),
|
||||||
|
highlights: tree_sitter_python::HIGHLIGHTS_QUERY,
|
||||||
|
injections: "",
|
||||||
|
locals: "",
|
||||||
|
},
|
||||||
|
"go" => LangDef {
|
||||||
|
language: tree_sitter_go::LANGUAGE.into(),
|
||||||
|
highlights: tree_sitter_go::HIGHLIGHTS_QUERY,
|
||||||
|
injections: "",
|
||||||
|
locals: "",
|
||||||
|
},
|
||||||
|
"ruby" => LangDef {
|
||||||
|
language: tree_sitter_ruby::LANGUAGE.into(),
|
||||||
|
highlights: tree_sitter_ruby::HIGHLIGHTS_QUERY,
|
||||||
|
injections: "",
|
||||||
|
locals: tree_sitter_ruby::LOCALS_QUERY,
|
||||||
|
},
|
||||||
|
"bash" | "shell" => LangDef {
|
||||||
|
language: tree_sitter_bash::LANGUAGE.into(),
|
||||||
|
highlights: tree_sitter_bash::HIGHLIGHT_QUERY,
|
||||||
|
injections: "",
|
||||||
|
locals: "",
|
||||||
|
},
|
||||||
|
"java" => LangDef {
|
||||||
|
language: tree_sitter_java::LANGUAGE.into(),
|
||||||
|
highlights: tree_sitter_java::HIGHLIGHTS_QUERY,
|
||||||
|
injections: "",
|
||||||
|
locals: "",
|
||||||
|
},
|
||||||
|
"html" => LangDef {
|
||||||
|
language: tree_sitter_html::LANGUAGE.into(),
|
||||||
|
highlights: tree_sitter_html::HIGHLIGHTS_QUERY,
|
||||||
|
injections: tree_sitter_html::INJECTIONS_QUERY,
|
||||||
|
locals: "",
|
||||||
|
},
|
||||||
|
"css" | "scss" | "less" => LangDef {
|
||||||
|
language: tree_sitter_css::LANGUAGE.into(),
|
||||||
|
highlights: tree_sitter_css::HIGHLIGHTS_QUERY,
|
||||||
|
injections: "",
|
||||||
|
locals: "",
|
||||||
|
},
|
||||||
|
"json" => LangDef {
|
||||||
|
language: tree_sitter_json::LANGUAGE.into(),
|
||||||
|
highlights: tree_sitter_json::HIGHLIGHTS_QUERY,
|
||||||
|
injections: "",
|
||||||
|
locals: "",
|
||||||
|
},
|
||||||
|
"lua" => LangDef {
|
||||||
|
language: tree_sitter_lua::LANGUAGE.into(),
|
||||||
|
highlights: tree_sitter_lua::HIGHLIGHTS_QUERY,
|
||||||
|
injections: tree_sitter_lua::INJECTIONS_QUERY,
|
||||||
|
locals: tree_sitter_lua::LOCALS_QUERY,
|
||||||
|
},
|
||||||
|
"php" => LangDef {
|
||||||
|
language: tree_sitter_php::LANGUAGE_PHP.into(),
|
||||||
|
highlights: tree_sitter_php::HIGHLIGHTS_QUERY,
|
||||||
|
injections: tree_sitter_php::INJECTIONS_QUERY,
|
||||||
|
locals: "",
|
||||||
|
},
|
||||||
|
"toml" => LangDef {
|
||||||
|
language: unsafe { lang_compat(tree_sitter_toml::language()) },
|
||||||
|
highlights: tree_sitter_toml::HIGHLIGHT_QUERY,
|
||||||
|
injections: "",
|
||||||
|
locals: "",
|
||||||
|
},
|
||||||
|
"yaml" => LangDef {
|
||||||
|
language: unsafe { lang_compat(tree_sitter_yaml::language()) },
|
||||||
|
highlights: tree_sitter_yaml::HIGHLIGHTS_QUERY,
|
||||||
|
injections: "",
|
||||||
|
locals: "",
|
||||||
|
},
|
||||||
|
"swift" => LangDef {
|
||||||
|
language: tree_sitter_swift::LANGUAGE.into(),
|
||||||
|
highlights: tree_sitter_swift::HIGHLIGHTS_QUERY,
|
||||||
|
injections: "",
|
||||||
|
locals: "",
|
||||||
|
},
|
||||||
|
"zig" => LangDef {
|
||||||
|
language: tree_sitter_zig::LANGUAGE.into(),
|
||||||
|
highlights: tree_sitter_zig::HIGHLIGHTS_QUERY,
|
||||||
|
injections: tree_sitter_zig::INJECTIONS_QUERY,
|
||||||
|
locals: "",
|
||||||
|
},
|
||||||
|
"sql" => LangDef {
|
||||||
|
language: unsafe { lang_compat(tree_sitter_sql::language()) },
|
||||||
|
highlights: include_str!("../queries/sql-highlights.scm"),
|
||||||
|
injections: "",
|
||||||
|
locals: "",
|
||||||
|
},
|
||||||
|
"make" | "makefile" => LangDef {
|
||||||
|
language: tree_sitter_make::LANGUAGE.into(),
|
||||||
|
highlights: tree_sitter_make::HIGHLIGHTS_QUERY,
|
||||||
|
injections: "",
|
||||||
|
locals: "",
|
||||||
|
},
|
||||||
|
"dockerfile" => LangDef {
|
||||||
|
language: unsafe { lang_compat(tree_sitter_dockerfile::language()) },
|
||||||
|
highlights: include_str!("../queries/dockerfile-highlights.scm"),
|
||||||
|
injections: "",
|
||||||
|
locals: "",
|
||||||
|
},
|
||||||
|
"kotlin" => LangDef {
|
||||||
|
language: unsafe { lang_compat(tree_sitter_kotlin::language()) },
|
||||||
|
highlights: include_str!("../queries/kotlin-highlights.scm"),
|
||||||
|
injections: "",
|
||||||
|
locals: "",
|
||||||
|
},
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some(ld)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_config(def: LangDef, name: &str) -> Option<HighlightConfiguration> {
|
||||||
|
let mut config = HighlightConfiguration::new(
|
||||||
|
def.language,
|
||||||
|
name,
|
||||||
|
def.highlights,
|
||||||
|
def.injections,
|
||||||
|
def.locals,
|
||||||
|
).ok()?;
|
||||||
|
config.configure(HIGHLIGHT_NAMES);
|
||||||
|
Some(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn highlight_source(source: &str, lang_id: &str) -> Vec<HighlightSpan> {
|
||||||
|
let def = match lang_def(lang_id) {
|
||||||
|
Some(d) => d,
|
||||||
|
None => return Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let config = match make_config(def, lang_id) {
|
||||||
|
Some(c) => c,
|
||||||
|
None => return Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut highlighter = Highlighter::new();
|
||||||
|
let events = match highlighter.highlight(&config, source.as_bytes(), None, |_| None) {
|
||||||
|
Ok(e) => e,
|
||||||
|
Err(_) => return Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut spans = Vec::new();
|
||||||
|
let mut stack: Vec<u8> = Vec::new();
|
||||||
|
|
||||||
|
for event in events {
|
||||||
|
match event {
|
||||||
|
Ok(HighlightEvent::Source { start, end }) => {
|
||||||
|
if let Some(&kind) = stack.last() {
|
||||||
|
spans.push(HighlightSpan { start, end, kind });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(HighlightEvent::HighlightStart(h)) => {
|
||||||
|
stack.push(h.0 as u8);
|
||||||
|
}
|
||||||
|
Ok(HighlightEvent::HighlightEnd) => {
|
||||||
|
stack.pop();
|
||||||
|
}
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spans
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
pub mod doc;
|
pub mod doc;
|
||||||
pub mod document;
|
pub mod document;
|
||||||
pub mod eval;
|
pub mod eval;
|
||||||
|
pub mod highlight;
|
||||||
pub mod interp;
|
pub mod interp;
|
||||||
pub mod persist;
|
pub mod persist;
|
||||||
pub mod ffi;
|
pub mod ffi;
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,11 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func application(_ application: NSApplication, open urls: [URL]) {
|
||||||
|
guard let url = urls.first else { return }
|
||||||
|
appState.loadNoteFromFile(url)
|
||||||
|
}
|
||||||
|
|
||||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
@ -121,6 +126,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
findItem.tag = Int(NSTextFinder.Action.showFindInterface.rawValue)
|
findItem.tag = Int(NSTextFinder.Action.showFindInterface.rawValue)
|
||||||
menu.addItem(findItem)
|
menu.addItem(findItem)
|
||||||
|
|
||||||
|
menu.addItem(.separator())
|
||||||
|
|
||||||
|
let formatItem = NSMenuItem(title: "Format Document", action: #selector(formatDocument), keyEquivalent: "F")
|
||||||
|
formatItem.keyEquivalentModifierMask = [.command, .shift]
|
||||||
|
formatItem.target = self
|
||||||
|
menu.addItem(formatItem)
|
||||||
|
|
||||||
item.submenu = menu
|
item.submenu = menu
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
@ -169,7 +181,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
|
||||||
@objc private func openNote() {
|
@objc private func openNote() {
|
||||||
let panel = NSOpenPanel()
|
let panel = NSOpenPanel()
|
||||||
panel.allowedContentTypes = [UTType(filenameExtension: "md")!, .plainText]
|
panel.allowedContentTypes = Self.supportedContentTypes
|
||||||
panel.canChooseFiles = true
|
panel.canChooseFiles = true
|
||||||
panel.canChooseDirectories = false
|
panel.canChooseDirectories = false
|
||||||
panel.allowsMultipleSelection = false
|
panel.allowsMultipleSelection = false
|
||||||
|
|
@ -180,13 +192,21 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func saveNote() {
|
@objc private func saveNote() {
|
||||||
appState.saveNote()
|
if appState.currentFileURL != nil {
|
||||||
|
appState.saveNote()
|
||||||
|
} else {
|
||||||
|
saveNoteAs()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func saveNoteAs() {
|
@objc private func saveNoteAs() {
|
||||||
let panel = NSSavePanel()
|
let panel = NSSavePanel()
|
||||||
panel.allowedContentTypes = [UTType(filenameExtension: "md")!]
|
panel.allowedContentTypes = Self.supportedContentTypes
|
||||||
panel.nameFieldStringValue = defaultFilename()
|
panel.nameFieldStringValue = defaultFilename()
|
||||||
|
if let url = appState.currentFileURL {
|
||||||
|
panel.directoryURL = url.deletingLastPathComponent()
|
||||||
|
panel.nameFieldStringValue = url.lastPathComponent
|
||||||
|
}
|
||||||
panel.beginSheetModal(for: window) { [weak self] response in
|
panel.beginSheetModal(for: window) { [weak self] response in
|
||||||
guard response == .OK, let url = panel.url else { return }
|
guard response == .OK, let url = panel.url else { return }
|
||||||
self?.appState.saveNoteToFile(url)
|
self?.appState.saveNoteToFile(url)
|
||||||
|
|
@ -194,6 +214,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func defaultFilename() -> String {
|
private func defaultFilename() -> String {
|
||||||
|
if let url = appState.currentFileURL {
|
||||||
|
return url.lastPathComponent
|
||||||
|
}
|
||||||
let firstLine = appState.documentText
|
let firstLine = appState.documentText
|
||||||
.components(separatedBy: "\n").first?
|
.components(separatedBy: "\n").first?
|
||||||
.trimmingCharacters(in: .whitespaces) ?? ""
|
.trimmingCharacters(in: .whitespaces) ?? ""
|
||||||
|
|
@ -201,9 +224,76 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
of: "^#+\\s*", with: "", options: .regularExpression
|
of: "^#+\\s*", with: "", options: .regularExpression
|
||||||
)
|
)
|
||||||
let trimmed = stripped.trimmingCharacters(in: .whitespaces)
|
let trimmed = stripped.trimmingCharacters(in: .whitespaces)
|
||||||
guard !trimmed.isEmpty, trimmed != "Untitled" else { return "note.md" }
|
let ext = extensionForFormat(appState.currentFileFormat)
|
||||||
|
guard !trimmed.isEmpty, trimmed != "Untitled" else { return "note.\(ext)" }
|
||||||
let sanitized = trimmed.map { "/:\\\\".contains($0) ? "-" : String($0) }.joined()
|
let sanitized = trimmed.map { "/:\\\\".contains($0) ? "-" : String($0) }.joined()
|
||||||
return sanitized.prefix(80) + ".md"
|
return sanitized.prefix(80) + ".\(ext)"
|
||||||
|
}
|
||||||
|
|
||||||
|
private func extensionForFormat(_ format: FileFormat) -> String {
|
||||||
|
switch format {
|
||||||
|
case .markdown: return "md"
|
||||||
|
case .csv: return "csv"
|
||||||
|
case .json: return "json"
|
||||||
|
case .toml: return "toml"
|
||||||
|
case .yaml: return "yaml"
|
||||||
|
case .xml: return "xml"
|
||||||
|
case .svg: return "svg"
|
||||||
|
case .rust: return "rs"
|
||||||
|
case .c: return "c"
|
||||||
|
case .cpp: return "cpp"
|
||||||
|
case .objc: return "m"
|
||||||
|
case .javascript: return "js"
|
||||||
|
case .typescript: return "ts"
|
||||||
|
case .jsx: return "jsx"
|
||||||
|
case .tsx: return "tsx"
|
||||||
|
case .html: return "html"
|
||||||
|
case .css: return "css"
|
||||||
|
case .scss: return "scss"
|
||||||
|
case .less: return "less"
|
||||||
|
case .python: return "py"
|
||||||
|
case .go: return "go"
|
||||||
|
case .ruby: return "rb"
|
||||||
|
case .php: return "php"
|
||||||
|
case .lua: return "lua"
|
||||||
|
case .shell: return "sh"
|
||||||
|
case .java: return "java"
|
||||||
|
case .kotlin: return "kt"
|
||||||
|
case .swift: return "swift"
|
||||||
|
case .zig: return "zig"
|
||||||
|
case .sql: return "sql"
|
||||||
|
case .makefile: return "mk"
|
||||||
|
case .dockerfile: return "Dockerfile"
|
||||||
|
case .config: return "conf"
|
||||||
|
case .lock: return "lock"
|
||||||
|
case .plainText, .unknown: return "txt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static let supportedContentTypes: [UTType] = {
|
||||||
|
let extensions = [
|
||||||
|
"md", "markdown", "mdown",
|
||||||
|
"csv", "json", "toml", "yaml", "yml", "xml", "svg",
|
||||||
|
"rs", "c", "cpp", "cc", "cxx", "h", "hpp", "hxx",
|
||||||
|
"js", "jsx", "ts", "tsx",
|
||||||
|
"html", "htm", "css", "scss", "less",
|
||||||
|
"py", "go", "rb", "php", "lua",
|
||||||
|
"sh", "bash", "zsh", "fish",
|
||||||
|
"java", "kt", "kts", "swift", "zig", "sql",
|
||||||
|
"mk", "ini", "cfg", "conf", "env",
|
||||||
|
"lock", "txt", "text", "log"
|
||||||
|
]
|
||||||
|
var types: [UTType] = [.plainText]
|
||||||
|
for ext in extensions {
|
||||||
|
if let t = UTType(filenameExtension: ext) {
|
||||||
|
types.append(t)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array(Set(types))
|
||||||
|
}()
|
||||||
|
|
||||||
|
@objc private func formatDocument() {
|
||||||
|
NotificationCenter.default.post(name: .formatDocument, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func openSettings() {
|
@objc private func openSettings() {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,114 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
|
enum FileFormat: String, CaseIterable {
|
||||||
|
case markdown, csv, json, toml, yaml, xml, svg
|
||||||
|
case rust, c, cpp, objc
|
||||||
|
case javascript, typescript, jsx, tsx
|
||||||
|
case html, css, scss, less
|
||||||
|
case python, go, ruby, php, lua
|
||||||
|
case shell, java, kotlin, swift, zig, sql
|
||||||
|
case makefile, dockerfile
|
||||||
|
case config, lock, plainText
|
||||||
|
case unknown
|
||||||
|
|
||||||
|
static func from(extension ext: String) -> FileFormat {
|
||||||
|
switch ext.lowercased() {
|
||||||
|
case "md", "markdown", "mdown": return .markdown
|
||||||
|
case "csv": return .csv
|
||||||
|
case "json": return .json
|
||||||
|
case "toml": return .toml
|
||||||
|
case "yaml", "yml": return .yaml
|
||||||
|
case "xml": return .xml
|
||||||
|
case "svg": return .svg
|
||||||
|
case "rs": return .rust
|
||||||
|
case "c": return .c
|
||||||
|
case "cpp", "cc", "cxx": return .cpp
|
||||||
|
case "h", "hpp", "hxx": return .cpp
|
||||||
|
case "m": return .objc
|
||||||
|
case "js": return .javascript
|
||||||
|
case "jsx": return .jsx
|
||||||
|
case "ts": return .typescript
|
||||||
|
case "tsx": return .tsx
|
||||||
|
case "html", "htm": return .html
|
||||||
|
case "css": return .css
|
||||||
|
case "scss": return .scss
|
||||||
|
case "less": return .less
|
||||||
|
case "py": return .python
|
||||||
|
case "go": return .go
|
||||||
|
case "rb": return .ruby
|
||||||
|
case "php": return .php
|
||||||
|
case "lua": return .lua
|
||||||
|
case "sh", "bash", "zsh", "fish": return .shell
|
||||||
|
case "java": return .java
|
||||||
|
case "kt", "kts": return .kotlin
|
||||||
|
case "swift": return .swift
|
||||||
|
case "zig": return .zig
|
||||||
|
case "sql": return .sql
|
||||||
|
case "mk": return .makefile
|
||||||
|
case "ini", "cfg", "conf", "env": return .config
|
||||||
|
case "lock": return .lock
|
||||||
|
case "txt", "text", "log": return .plainText
|
||||||
|
default: return .unknown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func from(filename: String) -> FileFormat {
|
||||||
|
let lower = filename.lowercased()
|
||||||
|
if lower == "makefile" { return .makefile }
|
||||||
|
if lower == "dockerfile" { return .dockerfile }
|
||||||
|
let ext = (filename as NSString).pathExtension
|
||||||
|
if ext.isEmpty { return .unknown }
|
||||||
|
return from(extension: ext)
|
||||||
|
}
|
||||||
|
|
||||||
|
var isCode: Bool {
|
||||||
|
switch self {
|
||||||
|
case .rust, .c, .cpp, .objc, .javascript, .typescript, .jsx, .tsx,
|
||||||
|
.html, .css, .scss, .less, .python, .go, .ruby, .php, .lua,
|
||||||
|
.shell, .java, .kotlin, .swift, .zig, .sql, .makefile, .dockerfile,
|
||||||
|
.json, .toml, .yaml, .xml, .svg:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isMarkdown: Bool { self == .markdown }
|
||||||
|
var isCSV: Bool { self == .csv }
|
||||||
|
|
||||||
|
var treeSitterLang: String? {
|
||||||
|
switch self {
|
||||||
|
case .rust: return "rust"
|
||||||
|
case .c: return "c"
|
||||||
|
case .cpp: return "cpp"
|
||||||
|
case .javascript: return "javascript"
|
||||||
|
case .jsx: return "jsx"
|
||||||
|
case .typescript: return "typescript"
|
||||||
|
case .tsx: return "tsx"
|
||||||
|
case .python: return "python"
|
||||||
|
case .go: return "go"
|
||||||
|
case .ruby: return "ruby"
|
||||||
|
case .php: return "php"
|
||||||
|
case .lua: return "lua"
|
||||||
|
case .shell: return "bash"
|
||||||
|
case .java: return "java"
|
||||||
|
case .kotlin: return "kotlin"
|
||||||
|
case .swift: return "swift"
|
||||||
|
case .zig: return "zig"
|
||||||
|
case .sql: return "sql"
|
||||||
|
case .html: return "html"
|
||||||
|
case .css, .scss, .less: return "css"
|
||||||
|
case .json: return "json"
|
||||||
|
case .toml: return "toml"
|
||||||
|
case .yaml: return "yaml"
|
||||||
|
case .makefile: return "make"
|
||||||
|
case .dockerfile: return "dockerfile"
|
||||||
|
default: return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class AppState: ObservableObject {
|
class AppState: ObservableObject {
|
||||||
@Published var documentText: String = "" {
|
@Published var documentText: String = "" {
|
||||||
didSet {
|
didSet {
|
||||||
|
|
@ -16,6 +124,8 @@ class AppState: ObservableObject {
|
||||||
@Published var currentNoteID: UUID
|
@Published var currentNoteID: UUID
|
||||||
@Published var selectedNoteIDs: Set<UUID> = []
|
@Published var selectedNoteIDs: Set<UUID> = []
|
||||||
@Published var modified: Bool = false
|
@Published var modified: Bool = false
|
||||||
|
@Published var currentFileURL: URL? = nil
|
||||||
|
@Published var currentFileFormat: FileFormat = .markdown
|
||||||
|
|
||||||
private let bridge = RustBridge.shared
|
private let bridge = RustBridge.shared
|
||||||
private var autoSaveTimer: DispatchSourceTimer?
|
private var autoSaveTimer: DispatchSourceTimer?
|
||||||
|
|
@ -120,6 +230,8 @@ class AppState: ObservableObject {
|
||||||
documentText = ""
|
documentText = ""
|
||||||
evalResults = [:]
|
evalResults = [:]
|
||||||
modified = false
|
modified = false
|
||||||
|
currentFileURL = nil
|
||||||
|
currentFileFormat = .markdown
|
||||||
refreshNoteList()
|
refreshNoteList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,21 +273,46 @@ class AppState: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveNote() {
|
func saveNote() {
|
||||||
bridge.setText(currentNoteID, text: documentText)
|
let textToSave: String
|
||||||
|
if currentFileFormat.isCSV {
|
||||||
|
textToSave = markdownTableToCSV(documentText)
|
||||||
|
} else {
|
||||||
|
textToSave = documentText
|
||||||
|
}
|
||||||
|
bridge.setText(currentNoteID, text: textToSave)
|
||||||
|
if let url = currentFileURL {
|
||||||
|
try? textToSave.write(to: url, atomically: true, encoding: .utf8)
|
||||||
|
}
|
||||||
let _ = bridge.cacheSave(currentNoteID)
|
let _ = bridge.cacheSave(currentNoteID)
|
||||||
modified = false
|
modified = false
|
||||||
refreshNoteList()
|
refreshNoteList()
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveNoteToFile(_ url: URL) {
|
func saveNoteToFile(_ url: URL) {
|
||||||
let _ = bridge.saveNote(currentNoteID, path: url.path)
|
let format = FileFormat.from(filename: url.lastPathComponent)
|
||||||
|
let textToSave: String
|
||||||
|
if format.isCSV {
|
||||||
|
textToSave = markdownTableToCSV(documentText)
|
||||||
|
} else {
|
||||||
|
textToSave = documentText
|
||||||
|
}
|
||||||
|
try? textToSave.write(to: url, atomically: true, encoding: .utf8)
|
||||||
|
currentFileURL = url
|
||||||
|
currentFileFormat = format
|
||||||
modified = false
|
modified = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadNoteFromFile(_ url: URL) {
|
func loadNoteFromFile(_ url: URL) {
|
||||||
|
let format = FileFormat.from(filename: url.lastPathComponent)
|
||||||
if let (id, text) = bridge.loadNote(path: url.path) {
|
if let (id, text) = bridge.loadNote(path: url.path) {
|
||||||
currentNoteID = id
|
currentNoteID = id
|
||||||
documentText = text
|
currentFileURL = url
|
||||||
|
currentFileFormat = format
|
||||||
|
if format.isCSV {
|
||||||
|
documentText = csvToMarkdownTable(text)
|
||||||
|
} else {
|
||||||
|
documentText = text
|
||||||
|
}
|
||||||
modified = false
|
modified = false
|
||||||
let _ = bridge.cacheSave(id)
|
let _ = bridge.cacheSave(id)
|
||||||
evaluate()
|
evaluate()
|
||||||
|
|
@ -183,6 +320,109 @@ class AppState: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - CSV conversion
|
||||||
|
|
||||||
|
private func csvToMarkdownTable(_ csv: String) -> String {
|
||||||
|
let rows = parseCSVRows(csv)
|
||||||
|
guard let header = rows.first, !header.isEmpty else { return csv }
|
||||||
|
|
||||||
|
var lines: [String] = []
|
||||||
|
lines.append("| " + header.joined(separator: " | ") + " |")
|
||||||
|
lines.append("| " + header.map { _ in "---" }.joined(separator: " | ") + " |")
|
||||||
|
for row in rows.dropFirst() {
|
||||||
|
var cells = row
|
||||||
|
while cells.count < header.count { cells.append("") }
|
||||||
|
lines.append("| " + cells.prefix(header.count).joined(separator: " | ") + " |")
|
||||||
|
}
|
||||||
|
return lines.joined(separator: "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func markdownTableToCSV(_ markdown: String) -> String {
|
||||||
|
let lines = markdown.components(separatedBy: "\n").filter { !$0.isEmpty }
|
||||||
|
var csvRows: [String] = []
|
||||||
|
|
||||||
|
for line in lines {
|
||||||
|
let trimmed = line.trimmingCharacters(in: .whitespaces)
|
||||||
|
guard trimmed.hasPrefix("|") else { continue }
|
||||||
|
if isTableSeparatorLine(trimmed) { continue }
|
||||||
|
let cells = extractTableCells(trimmed)
|
||||||
|
csvRows.append(cells.map { escapeCSVField($0) }.joined(separator: ","))
|
||||||
|
}
|
||||||
|
|
||||||
|
return csvRows.joined(separator: "\n") + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
private func parseCSVRows(_ csv: String) -> [[String]] {
|
||||||
|
var rows: [[String]] = []
|
||||||
|
var current: [String] = []
|
||||||
|
var field = ""
|
||||||
|
var inQuotes = false
|
||||||
|
let chars = Array(csv)
|
||||||
|
var i = 0
|
||||||
|
|
||||||
|
while i < chars.count {
|
||||||
|
let ch = chars[i]
|
||||||
|
if inQuotes {
|
||||||
|
if ch == "\"" {
|
||||||
|
if i + 1 < chars.count && chars[i + 1] == "\"" {
|
||||||
|
field.append("\"")
|
||||||
|
i += 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
inQuotes = false
|
||||||
|
} else {
|
||||||
|
field.append(ch)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ch == "\"" {
|
||||||
|
inQuotes = true
|
||||||
|
} else if ch == "," {
|
||||||
|
current.append(field.trimmingCharacters(in: .whitespaces))
|
||||||
|
field = ""
|
||||||
|
} else if ch == "\n" || ch == "\r" {
|
||||||
|
current.append(field.trimmingCharacters(in: .whitespaces))
|
||||||
|
field = ""
|
||||||
|
if !current.isEmpty {
|
||||||
|
rows.append(current)
|
||||||
|
}
|
||||||
|
current = []
|
||||||
|
if ch == "\r" && i + 1 < chars.count && chars[i + 1] == "\n" {
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
field.append(ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if !field.isEmpty || !current.isEmpty {
|
||||||
|
current.append(field.trimmingCharacters(in: .whitespaces))
|
||||||
|
rows.append(current)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isTableSeparatorLine(_ line: String) -> Bool {
|
||||||
|
let stripped = line.replacingOccurrences(of: " ", with: "")
|
||||||
|
return stripped.allSatisfy { "|:-".contains($0) } && stripped.contains("-")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func extractTableCells(_ line: String) -> [String] {
|
||||||
|
var trimmed = line.trimmingCharacters(in: .whitespaces)
|
||||||
|
if trimmed.hasPrefix("|") { trimmed = String(trimmed.dropFirst()) }
|
||||||
|
if trimmed.hasSuffix("|") { trimmed = String(trimmed.dropLast()) }
|
||||||
|
return trimmed.components(separatedBy: "|").map { $0.trimmingCharacters(in: .whitespaces) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func escapeCSVField(_ field: String) -> String {
|
||||||
|
if field.contains(",") || field.contains("\"") || field.contains("\n") {
|
||||||
|
return "\"" + field.replacingOccurrences(of: "\"", with: "\"\"") + "\""
|
||||||
|
}
|
||||||
|
return field
|
||||||
|
}
|
||||||
|
|
||||||
func deleteNote(_ id: UUID) {
|
func deleteNote(_ id: UUID) {
|
||||||
bridge.deleteNote(id)
|
bridge.deleteNote(id)
|
||||||
if id == currentNoteID {
|
if id == currentNoteID {
|
||||||
|
|
|
||||||
|
|
@ -30,4 +30,5 @@ extension Notification.Name {
|
||||||
static let toggleSidebar = Notification.Name("toggleSidebar")
|
static let toggleSidebar = Notification.Name("toggleSidebar")
|
||||||
static let focusEditor = Notification.Name("focusEditor")
|
static let focusEditor = Notification.Name("focusEditor")
|
||||||
static let focusTitle = Notification.Name("focusTitle")
|
static let focusTitle = Notification.Name("focusTitle")
|
||||||
|
static let formatDocument = Notification.Name("formatDocument")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1138,6 +1138,7 @@ struct EditorView: View {
|
||||||
EditorTextView(
|
EditorTextView(
|
||||||
text: bodyBinding,
|
text: bodyBinding,
|
||||||
evalResults: offsetEvalResults(state.evalResults),
|
evalResults: offsetEvalResults(state.evalResults),
|
||||||
|
fileFormat: state.currentFileFormat,
|
||||||
onEvaluate: { state.evaluate() },
|
onEvaluate: { state.evaluate() },
|
||||||
onBackspaceAtStart: {
|
onBackspaceAtStart: {
|
||||||
NotificationCenter.default.post(name: .focusTitle, object: nil)
|
NotificationCenter.default.post(name: .focusTitle, object: nil)
|
||||||
|
|
@ -1158,6 +1159,7 @@ struct EditorView: View {
|
||||||
struct EditorTextView: NSViewRepresentable {
|
struct EditorTextView: NSViewRepresentable {
|
||||||
@Binding var text: String
|
@Binding var text: String
|
||||||
var evalResults: [Int: EvalEntry]
|
var evalResults: [Int: EvalEntry]
|
||||||
|
var fileFormat: FileFormat = .markdown
|
||||||
var onEvaluate: () -> Void
|
var onEvaluate: () -> Void
|
||||||
var onBackspaceAtStart: (() -> Void)? = nil
|
var onBackspaceAtStart: (() -> Void)? = nil
|
||||||
|
|
||||||
|
|
@ -1213,7 +1215,7 @@ struct EditorTextView: NSViewRepresentable {
|
||||||
|
|
||||||
if let ts = textView.textStorage {
|
if let ts = textView.textStorage {
|
||||||
ts.beginEditing()
|
ts.beginEditing()
|
||||||
applySyntaxHighlighting(to: ts)
|
applySyntaxHighlighting(to: ts, format: fileFormat)
|
||||||
ts.endEditing()
|
ts.endEditing()
|
||||||
}
|
}
|
||||||
textView.typingAttributes = [
|
textView.typingAttributes = [
|
||||||
|
|
@ -1237,7 +1239,7 @@ struct EditorTextView: NSViewRepresentable {
|
||||||
textView.string = text
|
textView.string = text
|
||||||
if let ts = textView.textStorage {
|
if let ts = textView.textStorage {
|
||||||
ts.beginEditing()
|
ts.beginEditing()
|
||||||
applySyntaxHighlighting(to: ts)
|
applySyntaxHighlighting(to: ts, format: fileFormat)
|
||||||
ts.endEditing()
|
ts.endEditing()
|
||||||
}
|
}
|
||||||
textView.selectedRanges = selectedRanges
|
textView.selectedRanges = selectedRanges
|
||||||
|
|
@ -1273,6 +1275,12 @@ struct EditorTextView: NSViewRepresentable {
|
||||||
tv.window?.makeFirstResponder(tv)
|
tv.window?.makeFirstResponder(tv)
|
||||||
tv.setSelectedRange(NSRange(location: 0, length: 0))
|
tv.setSelectedRange(NSRange(location: 0, length: 0))
|
||||||
}
|
}
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
forName: .formatDocument, object: nil, queue: .main
|
||||||
|
) { [weak self] _ in
|
||||||
|
self?.formatCurrentDocument()
|
||||||
|
}
|
||||||
|
|
||||||
settingsObserver = NotificationCenter.default.addObserver(
|
settingsObserver = NotificationCenter.default.addObserver(
|
||||||
forName: .settingsChanged, object: nil, queue: .main
|
forName: .settingsChanged, object: nil, queue: .main
|
||||||
) { [weak self] _ in
|
) { [weak self] _ in
|
||||||
|
|
@ -1286,7 +1294,7 @@ struct EditorTextView: NSViewRepresentable {
|
||||||
.foregroundColor: palette.text
|
.foregroundColor: palette.text
|
||||||
]
|
]
|
||||||
ts.beginEditing()
|
ts.beginEditing()
|
||||||
applySyntaxHighlighting(to: ts)
|
applySyntaxHighlighting(to: ts, format: parent.fileFormat)
|
||||||
ts.endEditing()
|
ts.endEditing()
|
||||||
tv.needsDisplay = true
|
tv.needsDisplay = true
|
||||||
}
|
}
|
||||||
|
|
@ -1307,7 +1315,7 @@ struct EditorTextView: NSViewRepresentable {
|
||||||
parent.text = tv.string
|
parent.text = tv.string
|
||||||
let sel = tv.selectedRanges
|
let sel = tv.selectedRanges
|
||||||
ts.beginEditing()
|
ts.beginEditing()
|
||||||
applySyntaxHighlighting(to: ts)
|
applySyntaxHighlighting(to: ts, format: parent.fileFormat)
|
||||||
ts.endEditing()
|
ts.endEditing()
|
||||||
tv.typingAttributes = [
|
tv.typingAttributes = [
|
||||||
.font: Theme.editorFont,
|
.font: Theme.editorFont,
|
||||||
|
|
@ -1325,7 +1333,7 @@ struct EditorTextView: NSViewRepresentable {
|
||||||
|
|
||||||
func textView(_ textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
|
func textView(_ textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
|
||||||
if commandSelector == #selector(NSResponder.insertNewline(_:)) {
|
if commandSelector == #selector(NSResponder.insertNewline(_:)) {
|
||||||
textView.insertNewlineIgnoringFieldEditor(nil)
|
insertNewlineWithAutoIndent(textView)
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
self?.parent.onEvaluate()
|
self?.parent.onEvaluate()
|
||||||
}
|
}
|
||||||
|
|
@ -1340,6 +1348,155 @@ struct EditorTextView: NSViewRepresentable {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func textView(_ textView: NSTextView, shouldChangeTextIn range: NSRange, replacementString text: String?) -> Bool {
|
||||||
|
guard let text = text, text.count == 1 else { return true }
|
||||||
|
let ch = text.first!
|
||||||
|
let hasSelection = range.length > 0
|
||||||
|
|
||||||
|
// Skip over matching closer when cursor is right before it
|
||||||
|
if !hasSelection {
|
||||||
|
let closerChars: Set<Character> = ["}", ")", "]", "\"", "'"]
|
||||||
|
if closerChars.contains(ch) {
|
||||||
|
let str = textView.string as NSString
|
||||||
|
if range.location < str.length {
|
||||||
|
let next = Character(UnicodeScalar(str.character(at: range.location))!)
|
||||||
|
if next == ch {
|
||||||
|
textView.setSelectedRange(NSRange(location: range.location + 1, length: 0))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pairClosers: [Character: Character] = ["{": "}", "(": ")", "[": "]"]
|
||||||
|
if let close = pairClosers[ch] {
|
||||||
|
if hasSelection {
|
||||||
|
let selected = (textView.string as NSString).substring(with: range)
|
||||||
|
textView.insertText(String(ch) + selected + String(close), replacementRange: range)
|
||||||
|
textView.setSelectedRange(NSRange(location: range.location + 1, length: selected.count))
|
||||||
|
} else {
|
||||||
|
textView.insertText(String(ch) + String(close), replacementRange: range)
|
||||||
|
textView.setSelectedRange(NSRange(location: range.location + 1, length: 0))
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ch == "\"" || ch == "'" {
|
||||||
|
if hasSelection {
|
||||||
|
let selected = (textView.string as NSString).substring(with: range)
|
||||||
|
textView.insertText(String(ch) + selected + String(ch), replacementRange: range)
|
||||||
|
textView.setSelectedRange(NSRange(location: range.location + 1, length: selected.count))
|
||||||
|
} else {
|
||||||
|
textView.insertText(String(ch) + String(ch), replacementRange: range)
|
||||||
|
textView.setSelectedRange(NSRange(location: range.location + 1, length: 0))
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func formatCurrentDocument() {
|
||||||
|
guard let tv = textView else { return }
|
||||||
|
let format = parent.fileFormat
|
||||||
|
let text = tv.string
|
||||||
|
|
||||||
|
var formatted: String?
|
||||||
|
switch format {
|
||||||
|
case .json:
|
||||||
|
formatted = formatJSON(text)
|
||||||
|
default:
|
||||||
|
if format.isCode {
|
||||||
|
formatted = normalizeIndentation(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let result = formatted, result != text {
|
||||||
|
let sel = tv.selectedRanges
|
||||||
|
tv.string = result
|
||||||
|
parent.text = result
|
||||||
|
if let ts = tv.textStorage {
|
||||||
|
ts.beginEditing()
|
||||||
|
applySyntaxHighlighting(to: ts, format: format)
|
||||||
|
ts.endEditing()
|
||||||
|
}
|
||||||
|
tv.selectedRanges = sel
|
||||||
|
tv.needsDisplay = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func formatJSON(_ text: String) -> String? {
|
||||||
|
guard let data = text.data(using: .utf8),
|
||||||
|
let obj = try? JSONSerialization.jsonObject(with: data),
|
||||||
|
let pretty = try? JSONSerialization.data(withJSONObject: obj, options: [.prettyPrinted, .sortedKeys]),
|
||||||
|
let result = String(data: pretty, encoding: .utf8) else { return nil }
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private func normalizeIndentation(_ text: String) -> String {
|
||||||
|
let lines = text.components(separatedBy: "\n")
|
||||||
|
var result: [String] = []
|
||||||
|
var depth = 0
|
||||||
|
|
||||||
|
for line in lines {
|
||||||
|
let trimmed = line.trimmingCharacters(in: .whitespaces)
|
||||||
|
if trimmed.isEmpty {
|
||||||
|
result.append("")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let opens = trimmed.filter { "{([".contains($0) }.count
|
||||||
|
let closes = trimmed.filter { "})]".contains($0) }.count
|
||||||
|
let delta = opens - closes
|
||||||
|
|
||||||
|
if delta < 0 { depth = max(0, depth + delta) }
|
||||||
|
|
||||||
|
let indent = String(repeating: " ", count: depth)
|
||||||
|
result.append(indent + trimmed)
|
||||||
|
|
||||||
|
if delta > 0 { depth += delta }
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.joined(separator: "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func insertNewlineWithAutoIndent(_ textView: NSTextView) {
|
||||||
|
let str = textView.string as NSString
|
||||||
|
let cursor = textView.selectedRange().location
|
||||||
|
let lineRange = str.lineRange(for: NSRange(location: cursor, length: 0))
|
||||||
|
let currentLine = str.substring(with: lineRange)
|
||||||
|
|
||||||
|
var indent = ""
|
||||||
|
for c in currentLine {
|
||||||
|
if c == " " || c == "\t" { indent.append(c) }
|
||||||
|
else { break }
|
||||||
|
}
|
||||||
|
|
||||||
|
let trimmed = currentLine.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
let shouldIndent = trimmed.hasSuffix("{") || trimmed.hasSuffix(":") ||
|
||||||
|
trimmed.hasSuffix("do") || trimmed.hasSuffix("then") ||
|
||||||
|
trimmed.hasSuffix("(") || trimmed.hasSuffix("[")
|
||||||
|
|
||||||
|
if shouldIndent {
|
||||||
|
indent += " "
|
||||||
|
}
|
||||||
|
|
||||||
|
let charBeforeCursor: Character? = cursor > 0 ? Character(UnicodeScalar(str.character(at: cursor - 1))!) : nil
|
||||||
|
let charAtCursor: Character? = cursor < str.length ? Character(UnicodeScalar(str.character(at: cursor))!) : nil
|
||||||
|
|
||||||
|
// Between matching pairs: insert extra newline
|
||||||
|
if let before = charBeforeCursor, let after = charAtCursor,
|
||||||
|
(before == "{" && after == "}") || (before == "(" && after == ")") || (before == "[" && after == "]") {
|
||||||
|
let baseIndent = indent.count >= 4 ? String(indent.dropLast(4)) : ""
|
||||||
|
let insertion = "\n" + indent + "\n" + baseIndent
|
||||||
|
textView.insertText(insertion, replacementRange: textView.selectedRange())
|
||||||
|
textView.setSelectedRange(NSRange(location: cursor + 1 + indent.count, length: 0))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
textView.insertText("\n" + indent, replacementRange: textView.selectedRange())
|
||||||
|
}
|
||||||
|
|
||||||
func textView(_ textView: NSTextView, clickedOnLink link: Any, at charIndex: Int) -> Bool {
|
func textView(_ textView: NSTextView, clickedOnLink link: Any, at charIndex: Int) -> Bool {
|
||||||
var urlString: String?
|
var urlString: String?
|
||||||
if let url = link as? URL {
|
if let url = link as? URL {
|
||||||
|
|
@ -1405,7 +1562,7 @@ struct EditorTextView: NSViewRepresentable {
|
||||||
let sel = tv.selectedRanges
|
let sel = tv.selectedRanges
|
||||||
ts.beginEditing()
|
ts.beginEditing()
|
||||||
ts.replaceCharacters(in: range, with: newMarkdown)
|
ts.replaceCharacters(in: range, with: newMarkdown)
|
||||||
applySyntaxHighlighting(to: ts)
|
applySyntaxHighlighting(to: ts, format: parent.fileFormat)
|
||||||
ts.endEditing()
|
ts.endEditing()
|
||||||
tv.selectedRanges = sel
|
tv.selectedRanges = sel
|
||||||
parent.text = tv.string
|
parent.text = tv.string
|
||||||
|
|
@ -1626,7 +1783,7 @@ private let syntaxBooleans: Set<String> = ["true", "false"]
|
||||||
|
|
||||||
private let syntaxOperatorChars = CharacterSet(charactersIn: "+-*/=^<>!(){}[]:,.&|%")
|
private let syntaxOperatorChars = CharacterSet(charactersIn: "+-*/=^<>!(){}[]:,.&|%")
|
||||||
|
|
||||||
func applySyntaxHighlighting(to textStorage: NSTextStorage) {
|
func applySyntaxHighlighting(to textStorage: NSTextStorage, format: FileFormat = .markdown) {
|
||||||
let text = textStorage.string
|
let text = textStorage.string
|
||||||
let fullRange = NSRange(location: 0, length: (text as NSString).length)
|
let fullRange = NSRange(location: 0, length: (text as NSString).length)
|
||||||
let palette = Theme.current
|
let palette = Theme.current
|
||||||
|
|
@ -1639,6 +1796,11 @@ func applySyntaxHighlighting(to textStorage: NSTextStorage) {
|
||||||
]
|
]
|
||||||
textStorage.setAttributes(baseAttrs, range: fullRange)
|
textStorage.setAttributes(baseAttrs, range: fullRange)
|
||||||
|
|
||||||
|
if format.isCode {
|
||||||
|
applyCodeFileHighlighting(to: textStorage, syn: syn, baseFont: baseFont, format: format)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let fencedRanges = highlightFencedCodeBlocks(textStorage: textStorage, palette: palette, baseFont: baseFont)
|
let fencedRanges = highlightFencedCodeBlocks(textStorage: textStorage, palette: palette, baseFont: baseFont)
|
||||||
let tableHeaderLines = findTableHeaderLines(textStorage: textStorage, fencedRanges: fencedRanges)
|
let tableHeaderLines = findTableHeaderLines(textStorage: textStorage, fencedRanges: fencedRanges)
|
||||||
|
|
||||||
|
|
@ -1687,6 +1849,95 @@ func applySyntaxHighlighting(to textStorage: NSTextStorage) {
|
||||||
highlightAutolinks(textStorage: textStorage, palette: palette, fencedRanges: fencedRanges)
|
highlightAutolinks(textStorage: textStorage, palette: palette, fencedRanges: fencedRanges)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func applyCodeFileHighlighting(to textStorage: NSTextStorage, syn: Theme.SyntaxColors, baseFont: NSFont, format: FileFormat = .unknown) {
|
||||||
|
let text = textStorage.string
|
||||||
|
|
||||||
|
if let lang = format.treeSitterLang {
|
||||||
|
let spans = RustBridge.shared.highlight(source: text, lang: lang)
|
||||||
|
if !spans.isEmpty {
|
||||||
|
applyTreeSitterHighlighting(spans: spans, textStorage: textStorage, syn: syn, baseFont: baseFont)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let nsText = text as NSString
|
||||||
|
var lineStart = 0
|
||||||
|
while lineStart < nsText.length {
|
||||||
|
let lineRange = nsText.lineRange(for: NSRange(location: lineStart, length: 0))
|
||||||
|
let line = nsText.substring(with: lineRange)
|
||||||
|
highlightCodeLine(line, lineRange: lineRange, textStorage: textStorage, syn: syn)
|
||||||
|
lineStart = NSMaxRange(lineRange)
|
||||||
|
}
|
||||||
|
highlightBlockComments(textStorage: textStorage, syn: syn, baseFont: baseFont)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyTreeSitterHighlighting(spans: [RustBridge.HighlightSpan], textStorage: NSTextStorage, syn: Theme.SyntaxColors, baseFont: NSFont, offset: Int = 0) {
|
||||||
|
let palette = Theme.current
|
||||||
|
let italicFont = NSFontManager.shared.convert(baseFont, toHaveTrait: .italicFontMask)
|
||||||
|
let textLen = (textStorage.string as NSString).length
|
||||||
|
|
||||||
|
// When offset > 0, spans are byte-relative to a substring starting at `offset` chars.
|
||||||
|
// Extract that substring and convert bytes to chars within it.
|
||||||
|
let sourceStr: String
|
||||||
|
if offset > 0 {
|
||||||
|
sourceStr = (textStorage.string as NSString).substring(from: offset)
|
||||||
|
} else {
|
||||||
|
sourceStr = textStorage.string
|
||||||
|
}
|
||||||
|
let sourceBytes = Array(sourceStr.utf8)
|
||||||
|
|
||||||
|
for span in spans {
|
||||||
|
guard span.start < sourceBytes.count && span.end <= sourceBytes.count && span.start < span.end else { continue }
|
||||||
|
|
||||||
|
// Convert byte offsets to character offsets within sourceStr
|
||||||
|
let prefix = sourceStr.utf8.prefix(span.start)
|
||||||
|
let charStart = sourceStr.distance(from: sourceStr.startIndex, to: sourceStr.utf8.index(sourceStr.utf8.startIndex, offsetBy: prefix.count))
|
||||||
|
let endPrefix = sourceStr.utf8.prefix(span.end)
|
||||||
|
let charEnd = sourceStr.distance(from: sourceStr.startIndex, to: sourceStr.utf8.index(sourceStr.utf8.startIndex, offsetBy: endPrefix.count))
|
||||||
|
|
||||||
|
let absStart = offset + charStart
|
||||||
|
let absEnd = offset + charEnd
|
||||||
|
guard absStart < textLen && absEnd <= textLen && absStart < absEnd else { continue }
|
||||||
|
let range = NSRange(location: absStart, length: absEnd - absStart)
|
||||||
|
|
||||||
|
let color: NSColor
|
||||||
|
var font: NSFont? = nil
|
||||||
|
|
||||||
|
switch span.kind {
|
||||||
|
case 0: color = syn.keyword
|
||||||
|
case 1: color = syn.function
|
||||||
|
case 2: color = syn.function
|
||||||
|
case 3: color = syn.type
|
||||||
|
case 4: color = syn.type
|
||||||
|
case 5: color = syn.type
|
||||||
|
case 6: color = palette.peach
|
||||||
|
case 7: color = palette.peach
|
||||||
|
case 8: color = syn.string
|
||||||
|
case 9: color = syn.number
|
||||||
|
case 10: color = syn.comment; font = italicFont
|
||||||
|
case 11: color = palette.text
|
||||||
|
case 12: color = palette.red
|
||||||
|
case 13: color = palette.maroon
|
||||||
|
case 14: color = syn.operator
|
||||||
|
case 15: color = palette.overlay2
|
||||||
|
case 16: color = palette.overlay2
|
||||||
|
case 17: color = palette.overlay2
|
||||||
|
case 18: color = palette.teal
|
||||||
|
case 19: color = palette.red
|
||||||
|
case 20: color = palette.yellow
|
||||||
|
case 21: color = palette.sapphire
|
||||||
|
case 22: color = palette.pink
|
||||||
|
case 23: color = palette.teal
|
||||||
|
default: continue
|
||||||
|
}
|
||||||
|
|
||||||
|
textStorage.addAttribute(.foregroundColor, value: color, range: range)
|
||||||
|
if let f = font {
|
||||||
|
textStorage.addAttribute(.font, value: f, range: range)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func highlightMarkdownLine(_ trimmed: String, line: String, lineRange: NSRange, textStorage: NSTextStorage, baseFont: NSFont, palette: CatppuccinPalette, isTableHeader: Bool = false) -> Bool {
|
private func highlightMarkdownLine(_ trimmed: String, line: String, lineRange: NSRange, textStorage: NSTextStorage, baseFont: NSFont, palette: CatppuccinPalette, isTableHeader: Bool = false) -> Bool {
|
||||||
if trimmed.hasPrefix("### ") {
|
if trimmed.hasPrefix("### ") {
|
||||||
let hashRange = (textStorage.string as NSString).range(of: "###", range: lineRange)
|
let hashRange = (textStorage.string as NSString).range(of: "###", range: lineRange)
|
||||||
|
|
@ -1995,9 +2246,12 @@ private func highlightFencedCodeBlocks(textStorage: NSTextStorage, palette: Catp
|
||||||
let text = textStorage.string
|
let text = textStorage.string
|
||||||
let nsText = text as NSString
|
let nsText = text as NSString
|
||||||
let monoFont = NSFont.monospacedSystemFont(ofSize: baseFont.pointSize, weight: .regular)
|
let monoFont = NSFont.monospacedSystemFont(ofSize: baseFont.pointSize, weight: .regular)
|
||||||
|
let syn = Theme.syntax
|
||||||
var fencedRanges: [NSRange] = []
|
var fencedRanges: [NSRange] = []
|
||||||
var lineStart = 0
|
var lineStart = 0
|
||||||
var openFence: Int? = nil
|
var openFence: Int? = nil
|
||||||
|
var fenceLang: String? = nil
|
||||||
|
var codeStart: Int = 0
|
||||||
|
|
||||||
while lineStart < nsText.length {
|
while lineStart < nsText.length {
|
||||||
let lineRange = nsText.lineRange(for: NSRange(location: lineStart, length: 0))
|
let lineRange = nsText.lineRange(for: NSRange(location: lineStart, length: 0))
|
||||||
|
|
@ -2007,29 +2261,35 @@ private func highlightFencedCodeBlocks(textStorage: NSTextStorage, palette: Catp
|
||||||
if openFence == nil {
|
if openFence == nil {
|
||||||
if trimmed.hasPrefix("```") {
|
if trimmed.hasPrefix("```") {
|
||||||
openFence = lineRange.location
|
openFence = lineRange.location
|
||||||
// Mute the fence line
|
codeStart = NSMaxRange(lineRange)
|
||||||
|
let langId = String(trimmed.dropFirst(3)).trimmingCharacters(in: .whitespaces)
|
||||||
|
fenceLang = langId.isEmpty ? nil : langId.lowercased()
|
||||||
textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: lineRange)
|
textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: lineRange)
|
||||||
textStorage.addAttribute(.font, value: monoFont, range: lineRange)
|
textStorage.addAttribute(.font, value: monoFont, range: lineRange)
|
||||||
// Language identifier after ```
|
|
||||||
if trimmed.count > 3 {
|
|
||||||
let langStart = (nsText as NSString).range(of: "```", range: lineRange)
|
|
||||||
if langStart.location != NSNotFound {
|
|
||||||
let after = langStart.location + langStart.length
|
|
||||||
let langRange = NSRange(location: after, length: NSMaxRange(lineRange) - after)
|
|
||||||
if langRange.length > 0 {
|
|
||||||
textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: langRange)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if trimmed == "```" {
|
if trimmed == "```" {
|
||||||
// Close fence
|
|
||||||
textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: lineRange)
|
textStorage.addAttribute(.foregroundColor, value: palette.overlay0, range: lineRange)
|
||||||
textStorage.addAttribute(.font, value: monoFont, range: lineRange)
|
textStorage.addAttribute(.font, value: monoFont, range: lineRange)
|
||||||
let blockRange = NSRange(location: openFence!, length: NSMaxRange(lineRange) - openFence!)
|
let blockRange = NSRange(location: openFence!, length: NSMaxRange(lineRange) - openFence!)
|
||||||
fencedRanges.append(blockRange)
|
fencedRanges.append(blockRange)
|
||||||
|
|
||||||
|
// Apply tree-sitter highlighting to the code content
|
||||||
|
let codeEnd = lineRange.location
|
||||||
|
if let lang = fenceLang, codeEnd > codeStart {
|
||||||
|
let codeRange = NSRange(location: codeStart, length: codeEnd - codeStart)
|
||||||
|
let code = nsText.substring(with: codeRange)
|
||||||
|
textStorage.addAttribute(.font, value: monoFont, range: codeRange)
|
||||||
|
let spans = RustBridge.shared.highlight(source: code, lang: lang)
|
||||||
|
if !spans.isEmpty {
|
||||||
|
applyTreeSitterHighlighting(spans: spans, textStorage: textStorage, syn: syn, baseFont: monoFont, offset: codeStart)
|
||||||
|
} else {
|
||||||
|
textStorage.addAttribute(.foregroundColor, value: palette.text, range: codeRange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
openFence = nil
|
openFence = nil
|
||||||
|
fenceLang = nil
|
||||||
} else {
|
} else {
|
||||||
textStorage.addAttribute(.font, value: monoFont, range: lineRange)
|
textStorage.addAttribute(.font, value: monoFont, range: lineRange)
|
||||||
textStorage.addAttribute(.foregroundColor, value: palette.text, range: lineRange)
|
textStorage.addAttribute(.foregroundColor, value: palette.text, range: lineRange)
|
||||||
|
|
@ -2715,12 +2975,13 @@ class LineNumberTextView: NSTextView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Paste (image from clipboard)
|
// MARK: - Paste
|
||||||
|
|
||||||
override func paste(_ sender: Any?) {
|
override func paste(_ sender: Any?) {
|
||||||
let pb = NSPasteboard.general
|
let pb = NSPasteboard.general
|
||||||
let imageTypes: [NSPasteboard.PasteboardType] = [.tiff, .png]
|
|
||||||
|
|
||||||
|
// Image paste
|
||||||
|
let imageTypes: [NSPasteboard.PasteboardType] = [.tiff, .png]
|
||||||
if let imageType = imageTypes.first(where: { pb.data(forType: $0) != nil }),
|
if let imageType = imageTypes.first(where: { pb.data(forType: $0) != nil }),
|
||||||
let data = pb.data(forType: imageType) {
|
let data = pb.data(forType: imageType) {
|
||||||
if let image = NSImage(data: data), let pngData = image.tiffRepresentation,
|
if let image = NSImage(data: data), let pngData = image.tiffRepresentation,
|
||||||
|
|
@ -2736,6 +2997,48 @@ class LineNumberTextView: NSTextView {
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Smart text paste with indent adjustment
|
||||||
|
if let text = pb.string(forType: .string) {
|
||||||
|
let lines = text.components(separatedBy: "\n")
|
||||||
|
if lines.count > 1 {
|
||||||
|
let str = string as NSString
|
||||||
|
let cursor = selectedRange().location
|
||||||
|
let lineRange = str.lineRange(for: NSRange(location: cursor, length: 0))
|
||||||
|
let currentLine = str.substring(with: lineRange)
|
||||||
|
var currentIndent = ""
|
||||||
|
for c in currentLine {
|
||||||
|
if c == " " || c == "\t" { currentIndent.append(c) }
|
||||||
|
else { break }
|
||||||
|
}
|
||||||
|
|
||||||
|
var minIndent = Int.max
|
||||||
|
for line in lines where !line.trimmingCharacters(in: .whitespaces).isEmpty {
|
||||||
|
let spaces = line.prefix(while: { $0 == " " || $0 == "\t" }).count
|
||||||
|
minIndent = min(minIndent, spaces)
|
||||||
|
}
|
||||||
|
if minIndent == Int.max { minIndent = 0 }
|
||||||
|
|
||||||
|
var adjusted: [String] = []
|
||||||
|
for (i, line) in lines.enumerated() {
|
||||||
|
if line.trimmingCharacters(in: .whitespaces).isEmpty {
|
||||||
|
adjusted.append("")
|
||||||
|
} else {
|
||||||
|
let stripped = String(line.dropFirst(minIndent))
|
||||||
|
if i == 0 {
|
||||||
|
adjusted.append(stripped)
|
||||||
|
} else {
|
||||||
|
adjusted.append(currentIndent + stripped)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = adjusted.joined(separator: "\n")
|
||||||
|
insertText(result, replacementRange: selectedRange())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
super.paste(sender)
|
super.paste(sender)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,36 @@ class RustBridge {
|
||||||
return parseNoteListJSON(json)
|
return parseNoteListJSON(json)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct HighlightSpan {
|
||||||
|
let start: Int
|
||||||
|
let end: Int
|
||||||
|
let kind: Int
|
||||||
|
}
|
||||||
|
|
||||||
|
func highlight(source: String, lang: String) -> [HighlightSpan] {
|
||||||
|
guard let cstr = source.withCString({ src in
|
||||||
|
lang.withCString({ lng in
|
||||||
|
swiftly_highlight(src, lng)
|
||||||
|
})
|
||||||
|
}) else { return [] }
|
||||||
|
let json = String(cString: cstr)
|
||||||
|
swiftly_free_string(cstr)
|
||||||
|
return parseHighlightJSON(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func parseHighlightJSON(_ json: String) -> [HighlightSpan] {
|
||||||
|
guard let data = json.data(using: .utf8) else { return [] }
|
||||||
|
guard let arr = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else { return [] }
|
||||||
|
var spans: [HighlightSpan] = []
|
||||||
|
for item in arr {
|
||||||
|
guard let start = item["start"] as? Int,
|
||||||
|
let end = item["end"] as? Int,
|
||||||
|
let kind = item["kind"] as? Int else { continue }
|
||||||
|
spans.append(HighlightSpan(start: start, end: end, kind: kind))
|
||||||
|
}
|
||||||
|
return spans
|
||||||
|
}
|
||||||
|
|
||||||
func deleteNote(_ id: UUID) {
|
func deleteNote(_ id: UUID) {
|
||||||
freeDocument(id)
|
freeDocument(id)
|
||||||
let cacheDir = FileManager.default.homeDirectoryForCurrentUser
|
let cacheDir = FileManager.default.homeDirectoryForCurrentUser
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue