use std::ops::Range;
use anyhow::Result;
use gpui::{App, Context, HighlightStyle, SharedString, Task, Window};
use instant::Duration;
use lsp_types::{Position, SemanticTokens, SemanticTokensLegend};
use ropey::Rope;
use crate::input::{EditorMode, HighlightStyleResolver, InputBaseState, Lsp, RopeExt};
pub trait DocumentRangeSemanticTokensProvider {
fn legend(&self) -> SemanticTokensLegend;
fn semantic_tokens(
&self,
text: &Rope,
range: Range<usize>,
window: &mut Window,
cx: &mut App,
) -> Task<Result<SemanticTokens>>;
}
impl Lsp {
pub(crate) fn semantic_tokens_for_range(
&self,
text: &Rope,
visible_range: &Range<usize>,
theme: &dyn HighlightStyleResolver,
) -> Vec<(Range<usize>, HighlightStyle)> {
if self.semantic_tokens.is_empty() {
return Vec::new();
}
let visible_start = text.offset_to_position(visible_range.start);
let visible_end = text.offset_to_position(visible_range.end);
let hi = self
.semantic_tokens
.partition_point(|(range, _)| range.start < visible_end);
let lo = self
.semantic_tokens
.partition_point(|(range, _)| range.start.line < visible_start.line);
self.semantic_tokens[lo..hi]
.iter()
.filter_map(|(range, name)| {
let start = text.position_to_offset(&range.start);
let end = text.position_to_offset(&range.end);
if start >= end || start >= visible_range.end || end <= visible_range.start {
return None;
}
let style = theme.style(name.as_ref())?;
Some((start..end, style))
})
.collect()
}
pub(crate) fn update_semantic_tokens(
&mut self,
text: &Rope,
window: &mut Window,
cx: &mut Context<InputBaseState<EditorMode>>,
) {
let Some(provider) = self.semantic_tokens_provider.as_ref() else {
return;
};
let provider = provider.clone();
let legend = provider.legend();
let text = text.clone();
let range = 0..text.len();
let input_state = cx.entity();
self._semantic_tokens_task = cx.spawn_in(window, async move |_, cx| {
cx.background_executor()
.timer(Duration::from_millis(100))
.await;
let task_result = cx
.update(|window, cx| provider.semantic_tokens(&text, range, window, cx))
.ok();
if let Some(task) = task_result {
if let Ok(tokens) = task.await {
let decoded = decode_semantic_tokens(&tokens, &legend);
let _ = input_state.update(cx, |input_state, cx| {
if decoded != input_state.extras.lsp.semantic_tokens {
input_state.extras.lsp.semantic_tokens = decoded;
cx.notify();
}
});
}
}
});
}
}
fn decode_semantic_tokens(
tokens: &SemanticTokens,
legend: &SemanticTokensLegend,
) -> Vec<(lsp_types::Range, SharedString)> {
let names: Vec<SharedString> = legend
.token_types
.iter()
.map(|t| SharedString::from(t.as_str().to_owned()))
.collect();
let mut out = Vec::with_capacity(tokens.data.len());
let mut line: u32 = 0;
let mut character: u32 = 0;
for token in &tokens.data {
if token.delta_line > 0 {
line += token.delta_line;
character = token.delta_start;
} else {
character += token.delta_start;
}
let Some(name) = names.get(token.token_type as usize) else {
continue;
};
let start = Position::new(line, character);
let end = Position::new(line, character + token.length);
out.push((lsp_types::Range { start, end }, name.clone()));
}
out.sort_by_key(|(range, _)| range.start);
out
}
#[cfg(test)]
mod tests {
use super::*;
use gpui::hsla;
use lsp_types::{SemanticToken, SemanticTokenType, SemanticTokensLegend};
fn legend() -> SemanticTokensLegend {
SemanticTokensLegend {
token_types: vec![SemanticTokenType::KEYWORD, SemanticTokenType::COMMENT],
token_modifiers: vec![],
}
}
struct TestTheme;
impl HighlightStyleResolver for TestTheme {
fn style(&self, name: &str) -> Option<HighlightStyle> {
(name == "keyword" || name == "comment").then_some(HighlightStyle {
color: Some(hsla(0.5, 0.5, 0.5, 1.)),
..Default::default()
})
}
}
#[test]
fn test_decode_semantic_tokens_delta() {
let tokens = SemanticTokens {
result_id: None,
data: vec![
SemanticToken {
delta_line: 0,
delta_start: 0,
length: 4,
token_type: 0,
token_modifiers_bitset: 0,
},
SemanticToken {
delta_line: 1,
delta_start: 2,
length: 5,
token_type: 1,
token_modifiers_bitset: 0,
},
],
};
let decoded = decode_semantic_tokens(&tokens, &legend());
assert_eq!(decoded.len(), 2);
assert_eq!(decoded[0].0.start, Position::new(0, 0));
assert_eq!(decoded[0].0.end, Position::new(0, 4));
assert_eq!(decoded[0].1.as_ref(), "keyword");
assert_eq!(decoded[1].0.start, Position::new(1, 2));
assert_eq!(decoded[1].0.end, Position::new(1, 7));
assert_eq!(decoded[1].1.as_ref(), "comment");
}
#[test]
fn test_decode_skips_out_of_legend_index() {
let tokens = SemanticTokens {
result_id: None,
data: vec![SemanticToken {
delta_line: 0,
delta_start: 0,
length: 3,
token_type: 99, token_modifiers_bitset: 0,
}],
};
assert!(decode_semantic_tokens(&tokens, &legend()).is_empty());
}
#[test]
fn test_for_range_resolves_and_windows() {
let text = Rope::from("SELECT * FROM users\n-- a comment line\n");
let theme = TestTheme;
let mut lsp = Lsp::default();
lsp.semantic_tokens = vec![
(
lsp_types::Range {
start: Position::new(0, 0),
end: Position::new(0, 6),
},
SharedString::from("keyword"),
),
(
lsp_types::Range {
start: Position::new(1, 0),
end: Position::new(1, 17),
},
SharedString::from("comment"),
),
];
let styles = lsp.semantic_tokens_for_range(&text, &(0..19), &theme);
assert_eq!(
styles.len(),
1,
"only the line-0 token should be windowed in"
);
assert_eq!(styles[0].0, 0..6, "keyword token maps to bytes 0..6");
assert!(
styles[0].1 != HighlightStyle::default(),
"'keyword' should resolve to a non-default style on default-dark"
);
}
#[test]
fn test_for_range_binary_search_window() {
let text = Rope::from("foo bar\n".repeat(100).as_str());
let theme = TestTheme;
let mut lsp = Lsp::default();
lsp.semantic_tokens = (0..100u32)
.map(|line| {
(
lsp_types::Range {
start: Position::new(line, 0),
end: Position::new(line, 3),
},
SharedString::from("keyword"),
)
})
.collect();
let line_bytes = "foo bar\n".len();
let start = 50 * line_bytes;
let styles = lsp.semantic_tokens_for_range(&text, &(start..start + 3), &theme);
assert_eq!(styles.len(), 1);
assert_eq!(styles[0].0, start..start + 3);
assert!(
lsp.semantic_tokens_for_range(&text, &(0..0), &theme)
.is_empty()
);
}
}