use ferriki_textmate::{EncodedTokenAttributes, FontStyle, StandardTokenType};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::ops::Range;
use crate::{Error, ErrorKind, Result};
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub struct TokenizeOptions {
pub time_limit_millis: u64,
pub max_line_length: usize,
pub include_token_type: bool,
pub include_scopes: bool,
pub preserve_scope_boundaries: bool,
}
impl Default for TokenizeOptions {
fn default() -> Self {
Self {
time_limit_millis: 500,
max_line_length: 0,
include_token_type: false,
include_scopes: false,
preserve_scope_boundaries: false,
}
}
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct HighlightToken {
pub content: String,
pub offset: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub color: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub font_style: Option<FontStyle>,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub token_type: Option<StandardTokenType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope_names: Option<Vec<String>>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct HighlightTokensResult {
pub tokens: Vec<Vec<HighlightToken>>,
#[serde(rename = "fg")]
pub foreground: String,
#[serde(rename = "bg")]
pub background: String,
pub theme_name: String,
}
pub(crate) fn convert_token_offsets_to_utf8<'a>(
offsets: impl IntoIterator<Item = &'a mut usize>,
input: &str,
) -> Result<()> {
let map = utf16_to_byte_map(input);
for offset in offsets {
*offset = *map.get(*offset).ok_or_else(|| {
Error::new(
ErrorKind::Internal,
"Token offset exceeds the highlighted source.",
)
})?;
}
Ok(())
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct HighlightThemeTokenStyle {
#[serde(skip_serializing_if = "Option::is_none")]
pub color: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub font_style: Option<FontStyle>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct HighlightThemeToken {
pub content: String,
pub offset: usize,
pub variants: BTreeMap<String, HighlightThemeTokenStyle>,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub token_type: Option<StandardTokenType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope_names: Option<Vec<String>>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct HighlightThemeMetadata {
pub color: String,
pub name: String,
pub foreground: String,
pub background: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct HighlightTokensWithThemesResult {
pub tokens: Vec<Vec<HighlightThemeToken>>,
pub themes: Vec<HighlightThemeMetadata>,
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn token_from_metadata(
line: &str,
utf16_map: &[usize],
range: Range<usize>,
line_offset: usize,
metadata: u32,
color_map: &[String],
include_token_type: bool,
scope_names: Option<Vec<String>>,
) -> Option<HighlightToken> {
let start_index = range.start;
let end_index = range.end;
if start_index >= end_index {
return None;
}
let start_byte = *utf16_map.get(start_index)?;
let end_byte = *utf16_map.get(end_index)?;
let attributes = EncodedTokenAttributes::new(metadata);
Some(HighlightToken {
content: line.get(start_byte..end_byte)?.to_owned(),
offset: line_offset + start_index,
color: Some(
color_map
.get(attributes.foreground() as usize)
.cloned()
.unwrap_or_default(),
),
font_style: Some(attributes.font_style()),
token_type: include_token_type.then_some(attributes.token_type()),
scope_names,
})
}
pub(crate) fn utf16_to_byte_map(input: &str) -> Vec<usize> {
let mut map = Vec::with_capacity(input.encode_utf16().count() + 1);
for (byte_index, character) in input.char_indices() {
map.push(byte_index);
if character.len_utf16() == 2 {
map.push(byte_index);
}
}
map.push(input.len());
map
}
pub(crate) fn split_lines(input: &str) -> Vec<(&str, usize)> {
if input.is_empty() {
return vec![("", 0)];
}
let mut lines = Vec::new();
let mut start_byte = 0;
let mut start_utf16 = 0;
for (newline_byte, _) in input.match_indices('\n') {
let line_end = if newline_byte > start_byte
&& input.as_bytes().get(newline_byte - 1) == Some(&b'\r')
{
newline_byte - 1
} else {
newline_byte
};
lines.push((&input[start_byte..line_end], start_utf16));
start_utf16 += input[start_byte..=newline_byte].encode_utf16().count();
start_byte = newline_byte + 1;
}
lines.push((&input[start_byte..], start_utf16));
lines
}
impl TokenizeOptions {
#[must_use]
pub fn with_time_limit_millis(mut self, value: u64) -> Self {
self.time_limit_millis = value;
self
}
#[must_use]
pub fn with_max_line_length(mut self, value: usize) -> Self {
self.max_line_length = value;
self
}
#[must_use]
pub fn with_include_token_type(mut self, value: bool) -> Self {
self.include_token_type = value;
self
}
#[must_use]
pub fn with_preserve_scope_boundaries(mut self, value: bool) -> Self {
self.preserve_scope_boundaries = value;
self
}
#[must_use]
pub fn with_include_scopes(mut self, value: bool) -> Self {
self.include_scopes = value;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_to_shiki_tokenization_limits() {
assert_eq!(
TokenizeOptions::default(),
TokenizeOptions {
time_limit_millis: 500,
max_line_length: 0,
include_token_type: false,
include_scopes: false,
preserve_scope_boundaries: false,
}
);
}
#[test]
fn splits_crlf_lines_with_global_utf16_offsets() {
assert_eq!(
split_lines("😀a\r\nb\n"),
vec![("😀a", 0), ("b", 5), ("", 7)]
);
}
#[test]
fn maps_utf16_offsets_to_utf8_boundaries() {
assert_eq!(utf16_to_byte_map("a😀b"), vec![0, 1, 1, 5, 6]);
}
#[test]
fn typed_token_fields_keep_the_numeric_json_shape() {
let token = HighlightToken {
content: "fn".to_owned(),
offset: 0,
color: Some("#81A1C1".to_owned()),
font_style: Some(FontStyle::ITALIC | FontStyle::BOLD),
token_type: Some(StandardTokenType::Comment),
scope_names: None,
};
let json = serde_json::to_string(&token).expect("serialize");
assert_eq!(
json,
r##"{"content":"fn","offset":0,"color":"#81A1C1","fontStyle":3,"type":1}"##
);
assert_eq!(
serde_json::from_str::<HighlightToken>(&json).expect("deserialize"),
token
);
assert!(
serde_json::from_str::<HighlightToken>(r#"{"content":"","offset":0,"type":4}"#)
.is_err()
);
}
}