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>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,34 @@ serde = { version = "1", features = ["derive"] }
|
|||
serde_json = "1"
|
||||
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]
|
||||
cbindgen = "0.27"
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ typedef struct SwiftlyDoc SwiftlyDoc;
|
|||
|
||||
char *swiftly_list_notes(void);
|
||||
|
||||
char *swiftly_highlight(const char *source, const char *lang);
|
||||
|
||||
void swiftly_free_string(char *s);
|
||||
|
||||
#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::eval;
|
||||
use crate::highlight;
|
||||
use crate::persist;
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
#[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]
|
||||
pub extern "C" fn swiftly_free_string(s: *mut c_char) {
|
||||
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 document;
|
||||
pub mod eval;
|
||||
pub mod highlight;
|
||||
pub mod interp;
|
||||
pub mod persist;
|
||||
pub mod ffi;
|
||||
|
|
|
|||
|
|
@ -126,6 +126,13 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
findItem.tag = Int(NSTextFinder.Action.showFindInterface.rawValue)
|
||||
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
|
||||
return item
|
||||
}
|
||||
|
|
@ -285,6 +292,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
|
|||
return Array(Set(types))
|
||||
}()
|
||||
|
||||
@objc private func formatDocument() {
|
||||
NotificationCenter.default.post(name: .formatDocument, object: nil)
|
||||
}
|
||||
|
||||
@objc private func openSettings() {
|
||||
SettingsWindowController.show()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,7 +66,8 @@ enum FileFormat: String, CaseIterable {
|
|||
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:
|
||||
.shell, .java, .kotlin, .swift, .zig, .sql, .makefile, .dockerfile,
|
||||
.json, .toml, .yaml, .xml, .svg:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
|
@ -75,6 +76,37 @@ enum FileFormat: String, CaseIterable {
|
|||
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -30,4 +30,5 @@ extension Notification.Name {
|
|||
static let toggleSidebar = Notification.Name("toggleSidebar")
|
||||
static let focusEditor = Notification.Name("focusEditor")
|
||||
static let focusTitle = Notification.Name("focusTitle")
|
||||
static let formatDocument = Notification.Name("formatDocument")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1045,6 +1045,12 @@ struct EditorTextView: NSViewRepresentable {
|
|||
tv.window?.makeFirstResponder(tv)
|
||||
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(
|
||||
forName: .settingsChanged, object: nil, queue: .main
|
||||
) { [weak self] _ in
|
||||
|
|
@ -1097,7 +1103,7 @@ struct EditorTextView: NSViewRepresentable {
|
|||
|
||||
func textView(_ textView: NSTextView, doCommandBy commandSelector: Selector) -> Bool {
|
||||
if commandSelector == #selector(NSResponder.insertNewline(_:)) {
|
||||
textView.insertNewlineIgnoringFieldEditor(nil)
|
||||
insertNewlineWithAutoIndent(textView)
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.parent.onEvaluate()
|
||||
}
|
||||
|
|
@ -1112,6 +1118,149 @@ struct EditorTextView: NSViewRepresentable {
|
|||
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 {
|
||||
var urlString: String?
|
||||
if let url = link as? URL {
|
||||
|
|
@ -1412,7 +1561,7 @@ func applySyntaxHighlighting(to textStorage: NSTextStorage, format: FileFormat =
|
|||
textStorage.setAttributes(baseAttrs, range: fullRange)
|
||||
|
||||
if format.isCode {
|
||||
applyCodeFileHighlighting(to: textStorage, syn: syn, baseFont: baseFont)
|
||||
applyCodeFileHighlighting(to: textStorage, syn: syn, baseFont: baseFont, format: format)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -1464,21 +1613,88 @@ func applySyntaxHighlighting(to textStorage: NSTextStorage, format: FileFormat =
|
|||
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
|
||||
|
||||
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) {
|
||||
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 {
|
||||
if trimmed.hasPrefix("### ") {
|
||||
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?) {
|
||||
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 }),
|
||||
let data = pb.data(forType: imageType) {
|
||||
if let image = NSImage(data: data), let pngData = image.tiffRepresentation,
|
||||
|
|
@ -2357,6 +2574,48 @@ class LineNumberTextView: NSTextView {
|
|||
} 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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -101,6 +101,36 @@ class RustBridge {
|
|||
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) {
|
||||
freeDocument(id)
|
||||
let cacheDir = FileManager.default.homeDirectoryForCurrentUser
|
||||
|
|
|
|||
Loading…
Reference in New Issue