tree-sitter syntax highlighting, auto-indent, auto-close pairs, smart paste, format command
This commit is contained in:
parent
cca7d78cb3
commit
030b38a7a2
12
Info.plist
12
Info.plist
|
|
@ -366,6 +366,18 @@
|
||||||
<string>mk</string>
|
<string>mk</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</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>
|
<dict>
|
||||||
<key>CFBundleTypeName</key>
|
<key>CFBundleTypeName</key>
|
||||||
<string>Configuration</string>
|
<string>Configuration</string>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -126,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
|
||||||
}
|
}
|
||||||
|
|
@ -285,6 +292,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
return Array(Set(types))
|
return Array(Set(types))
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@objc private func formatDocument() {
|
||||||
|
NotificationCenter.default.post(name: .formatDocument, object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func openSettings() {
|
@objc private func openSettings() {
|
||||||
SettingsWindowController.show()
|
SettingsWindowController.show()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,8 @@ enum FileFormat: String, CaseIterable {
|
||||||
switch self {
|
switch self {
|
||||||
case .rust, .c, .cpp, .objc, .javascript, .typescript, .jsx, .tsx,
|
case .rust, .c, .cpp, .objc, .javascript, .typescript, .jsx, .tsx,
|
||||||
.html, .css, .scss, .less, .python, .go, .ruby, .php, .lua,
|
.html, .css, .scss, .less, .python, .go, .ruby, .php, .lua,
|
||||||
.shell, .java, .kotlin, .swift, .zig, .sql, .makefile, .dockerfile:
|
.shell, .java, .kotlin, .swift, .zig, .sql, .makefile, .dockerfile,
|
||||||
|
.json, .toml, .yaml, .xml, .svg:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|
@ -75,6 +76,37 @@ enum FileFormat: String, CaseIterable {
|
||||||
|
|
||||||
var isMarkdown: Bool { self == .markdown }
|
var isMarkdown: Bool { self == .markdown }
|
||||||
var isCSV: Bool { self == .csv }
|
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 {
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1045,6 +1045,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
|
||||||
|
|
@ -1097,7 +1103,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()
|
||||||
}
|
}
|
||||||
|
|
@ -1112,6 +1118,149 @@ 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 closers: [Character: Character] = ["}": "{", ")": "(", "]": "["]
|
||||||
|
if let opener = closers[ch] {
|
||||||
|
let str = textView.string as NSString
|
||||||
|
if range.location < str.length {
|
||||||
|
let next = str.character(at: range.location)
|
||||||
|
if next == ch.asciiValue.map({ UInt16($0) }) ?? 0 {
|
||||||
|
textView.setSelectedRange(NSRange(location: range.location + 1, length: 0))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pairs: [Character: String] = ["{": "}", "(": ")", "[": "]"]
|
||||||
|
if let close = pairs[ch] {
|
||||||
|
textView.insertText(String(ch) + close, replacementRange: range)
|
||||||
|
textView.setSelectedRange(NSRange(location: range.location + 1, length: 0))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ch == "\"" || ch == "'" {
|
||||||
|
let str = textView.string as NSString
|
||||||
|
if range.location < str.length {
|
||||||
|
let next = str.character(at: range.location)
|
||||||
|
if next == ch.asciiValue.map({ UInt16($0) }) ?? 0 {
|
||||||
|
textView.setSelectedRange(NSRange(location: range.location + 1, length: 0))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 closesFirst = trimmed.hasPrefix("}") || trimmed.hasPrefix(")") || trimmed.hasPrefix("]")
|
||||||
|
if closesFirst && depth > 0 { depth -= 1 }
|
||||||
|
|
||||||
|
let indent = String(repeating: " ", count: depth)
|
||||||
|
result.append(indent + trimmed)
|
||||||
|
|
||||||
|
let opens = trimmed.filter { "{([".contains($0) }.count
|
||||||
|
let closes = trimmed.filter { "})]".contains($0) }.count
|
||||||
|
depth += opens - closes
|
||||||
|
if closesFirst { depth += 1; depth -= 1 }
|
||||||
|
if depth < 0 { depth = 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
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 cursorBeforeLineEnd = cursor < NSMaxRange(lineRange) - 1
|
||||||
|
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 = String(indent.dropLast(4).isEmpty ? "" : 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 {
|
||||||
|
|
@ -1412,7 +1561,7 @@ func applySyntaxHighlighting(to textStorage: NSTextStorage, format: FileFormat =
|
||||||
textStorage.setAttributes(baseAttrs, range: fullRange)
|
textStorage.setAttributes(baseAttrs, range: fullRange)
|
||||||
|
|
||||||
if format.isCode {
|
if format.isCode {
|
||||||
applyCodeFileHighlighting(to: textStorage, syn: syn, baseFont: baseFont)
|
applyCodeFileHighlighting(to: textStorage, syn: syn, baseFont: baseFont, format: format)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1464,21 +1613,88 @@ func applySyntaxHighlighting(to textStorage: NSTextStorage, format: FileFormat =
|
||||||
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) {
|
private func applyCodeFileHighlighting(to textStorage: NSTextStorage, syn: Theme.SyntaxColors, baseFont: NSFont, format: FileFormat = .unknown) {
|
||||||
let text = textStorage.string
|
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
|
let nsText = text as NSString
|
||||||
var lineStart = 0
|
var lineStart = 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))
|
||||||
let line = nsText.substring(with: lineRange)
|
let line = nsText.substring(with: lineRange)
|
||||||
highlightCodeLine(line, lineRange: lineRange, textStorage: textStorage, syn: syn)
|
highlightCodeLine(line, lineRange: lineRange, textStorage: textStorage, syn: syn)
|
||||||
lineStart = NSMaxRange(lineRange)
|
lineStart = NSMaxRange(lineRange)
|
||||||
}
|
}
|
||||||
|
|
||||||
highlightBlockComments(textStorage: textStorage, syn: syn, baseFont: baseFont)
|
highlightBlockComments(textStorage: textStorage, syn: syn, baseFont: baseFont)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func applyTreeSitterHighlighting(spans: [RustBridge.HighlightSpan], textStorage: NSTextStorage, syn: Theme.SyntaxColors, baseFont: NSFont) {
|
||||||
|
let palette = Theme.current
|
||||||
|
let italicFont = NSFontManager.shared.convert(baseFont, toHaveTrait: .italicFontMask)
|
||||||
|
let nsText = textStorage.string as NSString
|
||||||
|
let textLen = nsText.length
|
||||||
|
|
||||||
|
for span in spans {
|
||||||
|
guard span.start < textLen && span.end <= textLen && span.start < span.end else { continue }
|
||||||
|
|
||||||
|
let byteStart = span.start
|
||||||
|
let byteEnd = span.end
|
||||||
|
let str = textStorage.string
|
||||||
|
let bytes = str.utf8
|
||||||
|
let startIdx = bytes.index(bytes.startIndex, offsetBy: byteStart, limitedBy: bytes.endIndex) ?? bytes.endIndex
|
||||||
|
let endIdx = bytes.index(bytes.startIndex, offsetBy: byteEnd, limitedBy: bytes.endIndex) ?? bytes.endIndex
|
||||||
|
let charStart = str.distance(from: str.startIndex, to: String.Index(startIdx, within: str) ?? str.endIndex)
|
||||||
|
let charEnd = str.distance(from: str.startIndex, to: String.Index(endIdx, within: str) ?? str.endIndex)
|
||||||
|
|
||||||
|
guard charStart < textLen && charEnd <= textLen && charStart < charEnd else { continue }
|
||||||
|
let range = NSRange(location: charStart, length: charEnd - charStart)
|
||||||
|
|
||||||
|
let color: NSColor
|
||||||
|
var font: NSFont? = nil
|
||||||
|
|
||||||
|
// kind indices match HIGHLIGHT_NAMES in highlight.rs
|
||||||
|
switch span.kind {
|
||||||
|
case 0: color = syn.keyword // keyword
|
||||||
|
case 1: color = syn.function // function
|
||||||
|
case 2: color = syn.function // function.builtin
|
||||||
|
case 3: color = syn.type // type
|
||||||
|
case 4: color = syn.type // type.builtin
|
||||||
|
case 5: color = syn.type // constructor
|
||||||
|
case 6: color = palette.peach // constant
|
||||||
|
case 7: color = palette.peach // constant.builtin
|
||||||
|
case 8: color = syn.string // string
|
||||||
|
case 9: color = syn.number // number
|
||||||
|
case 10: color = syn.comment; font = italicFont // comment
|
||||||
|
case 11: color = palette.text // variable
|
||||||
|
case 12: color = palette.red // variable.builtin
|
||||||
|
case 13: color = palette.maroon // variable.parameter
|
||||||
|
case 14: color = syn.operator // operator
|
||||||
|
case 15: color = palette.overlay2 // punctuation
|
||||||
|
case 16: color = palette.overlay2 // punctuation.bracket
|
||||||
|
case 17: color = palette.overlay2 // punctuation.delimiter
|
||||||
|
case 18: color = palette.teal // property
|
||||||
|
case 19: color = palette.red // tag (HTML)
|
||||||
|
case 20: color = palette.yellow // attribute
|
||||||
|
case 21: color = palette.sapphire // label
|
||||||
|
case 22: color = palette.pink // escape
|
||||||
|
case 23: color = palette.teal // embedded
|
||||||
|
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)
|
||||||
|
|
@ -2336,12 +2552,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,
|
||||||
|
|
@ -2357,6 +2574,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,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