Skip to repository content2588 lines · 101.6 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T03:36:06.718Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
semantic_tokens.rs
1use std::{collections::hash_map, sync::Arc, time::Duration};
2
3use collections::{HashMap, HashSet};
4use futures::future::join_all;
5use gpui::{
6 App, Context, FontStyle, FontWeight, HighlightStyle, StrikethroughStyle, Task, UnderlineStyle,
7};
8use itertools::Itertools;
9use language::language_settings::LanguageSettings;
10use project::{
11 lsp_store::{BufferSemanticToken, BufferSemanticTokens, SemanticTokenStylizer, TokenType},
12 project_settings::ProjectSettings,
13};
14use settings::{
15 SemanticTokenColorOverride, SemanticTokenFontStyle, SemanticTokenFontWeight, SemanticTokenRule,
16 SemanticTokenRules, Settings as _,
17};
18use text::BufferId;
19use theme::SyntaxTheme;
20use ui::ActiveTheme as _;
21
22use crate::{
23 Editor,
24 actions::ToggleSemanticHighlights,
25 display_map::{HighlightStyleInterner, SemanticTokenHighlight},
26};
27
28pub(super) struct SemanticTokenState {
29 rules: SemanticTokenRules,
30 enabled: bool,
31 update_task: Task<()>,
32 fetched_for_buffers: HashMap<BufferId, clock::Global>,
33}
34
35impl SemanticTokenState {
36 pub(super) fn new(cx: &App, enabled: bool) -> Self {
37 Self {
38 rules: ProjectSettings::get_global(cx)
39 .global_lsp_settings
40 .semantic_token_rules
41 .clone(),
42 enabled,
43 update_task: Task::ready(()),
44 fetched_for_buffers: HashMap::default(),
45 }
46 }
47
48 pub(super) fn enabled(&self) -> bool {
49 self.enabled
50 }
51
52 pub(super) fn toggle_enabled(&mut self) {
53 self.enabled = !self.enabled;
54 }
55
56 #[cfg(test)]
57 pub(super) fn take_update_task(&mut self) -> Task<()> {
58 std::mem::replace(&mut self.update_task, Task::ready(()))
59 }
60
61 pub(super) fn invalidate_buffer(&mut self, buffer_id: &BufferId) {
62 self.fetched_for_buffers.remove(buffer_id);
63 }
64
65 pub(super) fn update_rules(&mut self, new_rules: SemanticTokenRules) -> bool {
66 if new_rules != self.rules {
67 self.rules = new_rules;
68 true
69 } else {
70 false
71 }
72 }
73}
74
75impl Editor {
76 pub fn supports_semantic_tokens(&self, cx: &mut App) -> bool {
77 let Some(provider) = self.semantics_provider.as_ref() else {
78 return false;
79 };
80
81 let mut supports = false;
82 self.buffer().update(cx, |this, cx| {
83 this.for_each_buffer(&mut |buffer| {
84 supports |= provider.supports_semantic_tokens(buffer, cx);
85 });
86 });
87
88 supports
89 }
90
91 pub fn semantic_highlights_enabled(&self) -> bool {
92 self.semantic_token_state.enabled()
93 }
94
95 pub fn toggle_semantic_highlights(
96 &mut self,
97 _: &ToggleSemanticHighlights,
98 _window: &mut gpui::Window,
99 cx: &mut Context<Self>,
100 ) {
101 self.semantic_token_state.toggle_enabled();
102 self.invalidate_semantic_tokens(None);
103 self.refresh_semantic_tokens(None, false, cx);
104 }
105
106 pub(super) fn invalidate_semantic_tokens(&mut self, for_buffer: Option<BufferId>) {
107 match for_buffer {
108 Some(for_buffer) => self.semantic_token_state.invalidate_buffer(&for_buffer),
109 None => self.semantic_token_state.fetched_for_buffers.clear(),
110 }
111 }
112
113 pub(super) fn refresh_semantic_tokens(
114 &mut self,
115 buffer_id: Option<BufferId>,
116 server_refreshed: bool,
117 cx: &mut Context<Self>,
118 ) {
119 if !self.lsp_data_enabled() || !self.semantic_token_state.enabled() {
120 self.invalidate_semantic_tokens(None);
121 self.display_map.update(cx, |display_map, _| {
122 match Arc::get_mut(&mut display_map.semantic_token_highlights) {
123 Some(highlights) => highlights.clear(),
124 None => display_map.semantic_token_highlights = Arc::new(Default::default()),
125 };
126 });
127 self.semantic_token_state.update_task = Task::ready(());
128 cx.notify();
129 return;
130 }
131
132 let mut invalidate_semantic_highlights_for_buffers = HashSet::default();
133 if server_refreshed {
134 invalidate_semantic_highlights_for_buffers.extend(
135 self.semantic_token_state
136 .fetched_for_buffers
137 .drain()
138 .map(|(buffer_id, _)| buffer_id),
139 );
140 }
141
142 let Some((sema, project)) = self
143 .semantics_provider
144 .clone()
145 .zip(self.project.as_ref().map(|p| p.downgrade()))
146 else {
147 return;
148 };
149
150 let buffers_to_query = self
151 .visible_buffers(cx)
152 .into_iter()
153 .filter(|buffer| self.is_lsp_relevant(buffer.read(cx).file(), cx))
154 .chain(buffer_id.and_then(|buffer_id| self.buffer.read(cx).buffer(buffer_id)))
155 .filter_map(|editor_buffer| {
156 let editor_buffer_id = editor_buffer.read(cx).remote_id();
157 if self.registered_buffers.contains_key(&editor_buffer_id)
158 && LanguageSettings::for_buffer(editor_buffer.read(cx), cx)
159 .semantic_tokens
160 .enabled()
161 {
162 Some((editor_buffer_id, editor_buffer))
163 } else {
164 None
165 }
166 })
167 .collect::<HashMap<_, _>>();
168
169 for buffer_with_disabled_tokens in self
170 .display_map
171 .read(cx)
172 .semantic_token_highlights
173 .keys()
174 .copied()
175 .filter(|buffer_id| !buffers_to_query.contains_key(buffer_id))
176 .filter(|buffer_id| {
177 !self
178 .buffer
179 .read(cx)
180 .buffer(*buffer_id)
181 .is_some_and(|buffer| {
182 let buffer = buffer.read(cx);
183 LanguageSettings::for_buffer(&buffer, cx)
184 .semantic_tokens
185 .enabled()
186 })
187 })
188 .collect::<Vec<_>>()
189 {
190 self.semantic_token_state
191 .invalidate_buffer(&buffer_with_disabled_tokens);
192 self.display_map.update(cx, |display_map, _| {
193 display_map.invalidate_semantic_highlights(buffer_with_disabled_tokens);
194 });
195 }
196
197 self.semantic_token_state.update_task = cx.spawn(async move |editor, cx| {
198 cx.background_executor()
199 .timer(Duration::from_millis(50))
200 .await;
201 let Some(all_semantic_tokens_task) = editor
202 .update(cx, |editor, cx| {
203 buffers_to_query
204 .into_iter()
205 .filter_map(|(buffer_id, buffer)| {
206 let known_version = editor
207 .semantic_token_state
208 .fetched_for_buffers
209 .get(&buffer_id);
210 let query_version = buffer.read(cx).version();
211 if known_version.is_some_and(|known_version| {
212 !query_version.changed_since(known_version)
213 }) {
214 None
215 } else {
216 sema.semantic_tokens(buffer, cx).map(|task| async move {
217 (buffer_id, query_version, task.await)
218 })
219 }
220 })
221 .collect::<Vec<_>>()
222 })
223 .ok()
224 else {
225 return;
226 };
227
228 let all_semantic_tokens = join_all(all_semantic_tokens_task).await;
229 editor
230 .update(cx, |editor, cx| {
231 editor.display_map.update(cx, |display_map, _| {
232 for buffer_id in invalidate_semantic_highlights_for_buffers {
233 display_map.invalidate_semantic_highlights(buffer_id);
234 editor.semantic_token_state.invalidate_buffer(&buffer_id);
235 }
236 });
237
238 if all_semantic_tokens.is_empty() {
239 return;
240 }
241 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
242
243 for (buffer_id, query_version, tokens) in all_semantic_tokens {
244 let tokens = match tokens {
245 Ok(BufferSemanticTokens {
246 tokens: Some(tokens),
247 }) => tokens,
248 Ok(BufferSemanticTokens { tokens: None }) => {
249 editor.display_map.update(cx, |display_map, _| {
250 display_map.invalidate_semantic_highlights(buffer_id);
251 });
252 continue;
253 }
254 Err(e) => {
255 log::error!(
256 "Failed to fetch semantic tokens for buffer \
257 {buffer_id:?}: {e:#}"
258 );
259 continue;
260 }
261 };
262
263 match editor
264 .semantic_token_state
265 .fetched_for_buffers
266 .entry(buffer_id)
267 {
268 hash_map::Entry::Occupied(mut o) => {
269 if query_version.changed_since(o.get()) {
270 o.insert(query_version);
271 } else {
272 continue;
273 }
274 }
275 hash_map::Entry::Vacant(v) => {
276 v.insert(query_version);
277 }
278 }
279
280 let language_name = editor
281 .buffer()
282 .read(cx)
283 .buffer(buffer_id)
284 .and_then(|buf| buf.read(cx).language().map(|l| l.name()));
285
286 let Some(project) = project.upgrade() else {
287 return;
288 };
289 editor.display_map.update(cx, |display_map, cx| {
290 project.read(cx).lsp_store().update(cx, |lsp_store, cx| {
291 let mut token_highlights = Vec::new();
292 let mut interner = HighlightStyleInterner::default();
293 for (server_id, server_tokens) in tokens {
294 let Some(stylizer) = lsp_store.get_or_create_token_stylizer(
295 server_id,
296 language_name.as_ref(),
297 cx,
298 ) else {
299 continue;
300 };
301 let theme = cx.theme().syntax();
302 token_highlights.reserve(2 * server_tokens.len());
303 token_highlights.extend(buffer_into_editor_highlights(
304 &server_tokens,
305 stylizer,
306 &multi_buffer_snapshot,
307 &mut interner,
308 theme,
309 ));
310 }
311
312 token_highlights.sort_by(|a, b| {
313 a.range.start.cmp(&b.range.start, &multi_buffer_snapshot)
314 });
315 Arc::make_mut(&mut display_map.semantic_token_highlights).insert(
316 buffer_id,
317 (Arc::from(token_highlights), Arc::new(interner)),
318 );
319 });
320 });
321 }
322
323 cx.notify();
324 })
325 .ok();
326 });
327 }
328}
329
330fn buffer_into_editor_highlights<'a, 'b>(
331 buffer_tokens: &'a [BufferSemanticToken],
332 stylizer: &'a SemanticTokenStylizer,
333 multi_buffer_snapshot: &'a multi_buffer::MultiBufferSnapshot,
334 interner: &'b mut HighlightStyleInterner,
335 theme: &'a SyntaxTheme,
336) -> impl Iterator<Item = SemanticTokenHighlight> + use<'a, 'b> {
337 multi_buffer_snapshot
338 .text_anchors_to_visible_anchors(
339 buffer_tokens
340 .iter()
341 .flat_map(|token| [token.range.start, token.range.end]),
342 )
343 .into_iter()
344 .tuples::<(_, _)>()
345 .zip(buffer_tokens)
346 .filter_map(|((multi_buffer_start, multi_buffer_end), token)| {
347 let range = multi_buffer_start?..multi_buffer_end?;
348 let style = convert_token(stylizer, theme, token.token_type, token.token_modifiers)?;
349 let style = interner.intern(style);
350 Some(SemanticTokenHighlight {
351 range,
352 style,
353 token_type: token.token_type,
354 token_modifiers: token.token_modifiers,
355 server_id: stylizer.server_id(),
356 })
357 })
358}
359
360fn convert_token(
361 stylizer: &SemanticTokenStylizer,
362 theme: &SyntaxTheme,
363 token_type: TokenType,
364 modifiers: u32,
365) -> Option<HighlightStyle> {
366 let rules = stylizer.rules_for_token(token_type)?;
367 let filter = |rule: &&SemanticTokenRule| {
368 rule.token_modifiers
369 .iter()
370 .all(|m| stylizer.has_modifier(modifiers, m))
371 };
372 let last = rules.last()?;
373 if last.no_style_defined() && filter(&last) {
374 return None;
375 }
376
377 let mut highlight = HighlightStyle::default();
378
379 for rule in rules.into_iter().filter(filter) {
380 let style = rule
381 .style
382 .iter()
383 .find_map(|style| theme.style_for_name(style));
384
385 macro_rules! overwrite {
386 (
387 highlight.$highlight_field:ident,
388 SemanticTokenRule::$rule_field:ident,
389 $transform:expr $(,)?
390 ) => {
391 highlight.$highlight_field = rule
392 .$rule_field
393 .map($transform)
394 .or_else(|| style.as_ref().and_then(|s| s.$highlight_field))
395 .or(highlight.$highlight_field)
396 };
397 }
398
399 overwrite!(
400 highlight.color,
401 SemanticTokenRule::foreground_color,
402 Into::into,
403 );
404
405 overwrite!(
406 highlight.background_color,
407 SemanticTokenRule::background_color,
408 Into::into,
409 );
410
411 overwrite!(
412 highlight.font_weight,
413 SemanticTokenRule::font_weight,
414 |w| match w {
415 SemanticTokenFontWeight::Normal => FontWeight::NORMAL,
416 SemanticTokenFontWeight::Bold => FontWeight::BOLD,
417 },
418 );
419
420 overwrite!(
421 highlight.font_style,
422 SemanticTokenRule::font_style,
423 |s| match s {
424 SemanticTokenFontStyle::Normal => FontStyle::Normal,
425 SemanticTokenFontStyle::Italic => FontStyle::Italic,
426 },
427 );
428
429 overwrite!(highlight.underline, SemanticTokenRule::underline, |u| {
430 UnderlineStyle {
431 thickness: 1.0.into(),
432 color: match u {
433 SemanticTokenColorOverride::InheritForeground(true) => highlight.color,
434 SemanticTokenColorOverride::InheritForeground(false) => None,
435 SemanticTokenColorOverride::Replace(c) => Some(c.into()),
436 },
437 ..UnderlineStyle::default()
438 }
439 });
440
441 overwrite!(
442 highlight.strikethrough,
443 SemanticTokenRule::strikethrough,
444 |s| StrikethroughStyle {
445 thickness: 1.0.into(),
446 color: match s {
447 SemanticTokenColorOverride::InheritForeground(true) => highlight.color,
448 SemanticTokenColorOverride::InheritForeground(false) => None,
449 SemanticTokenColorOverride::Replace(c) => Some(c.into()),
450 },
451 },
452 );
453 }
454 Some(highlight)
455}
456
457#[cfg(test)]
458mod tests {
459 use std::{
460 ops::Range,
461 sync::atomic::{self, AtomicUsize},
462 };
463
464 use futures::StreamExt as _;
465 use gpui::{
466 AppContext as _, Entity, Focusable as _, HighlightStyle, TestAppContext, UpdateGlobal as _,
467 };
468 use language::{
469 Diagnostic, DiagnosticEntry, DiagnosticSet, Language, LanguageAwareStyling, LanguageConfig,
470 LanguageMatcher,
471 };
472 use languages::FakeLspAdapter;
473 use lsp::LanguageServerId;
474 use multi_buffer::{
475 AnchorRangeExt, ExpandExcerptDirection, MultiBuffer, MultiBufferOffset, PathKey,
476 };
477 use project::Project;
478 use rope::{Point, PointUtf16};
479 use serde_json::json;
480 use settings::{
481 GlobalLspSettingsContent, LanguageSettingsContent, SemanticTokenRule, SemanticTokenRules,
482 SemanticTokens, SettingsStore,
483 };
484 use workspace::{MultiWorkspace, WorkspaceHandle as _};
485
486 use crate::{
487 Capability,
488 editor_tests::{init_test, update_test_language_settings},
489 test::{build_editor_with_project, editor_lsp_test_context::EditorLspTestContext},
490 };
491
492 use super::*;
493
494 #[gpui::test]
495 async fn lsp_semantic_tokens_full_capability(cx: &mut TestAppContext) {
496 init_test(cx, |_| {});
497
498 update_test_language_settings(cx, &|language_settings| {
499 language_settings.languages.0.insert(
500 "Rust".into(),
501 LanguageSettingsContent {
502 semantic_tokens: Some(SemanticTokens::Full),
503 ..LanguageSettingsContent::default()
504 },
505 );
506 });
507
508 let mut cx = EditorLspTestContext::new_rust(
509 lsp::ServerCapabilities {
510 semantic_tokens_provider: Some(
511 lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(
512 lsp::SemanticTokensOptions {
513 legend: lsp::SemanticTokensLegend {
514 token_types: vec!["function".into()],
515 token_modifiers: Vec::new(),
516 },
517 full: Some(lsp::SemanticTokensFullOptions::Delta { delta: None }),
518 ..lsp::SemanticTokensOptions::default()
519 },
520 ),
521 ),
522 ..lsp::ServerCapabilities::default()
523 },
524 cx,
525 )
526 .await;
527
528 let full_counter = Arc::new(AtomicUsize::new(0));
529 let full_counter_clone = full_counter.clone();
530
531 let mut full_request = cx
532 .set_request_handler::<lsp::request::SemanticTokensFullRequest, _, _>(
533 move |_, _, _| {
534 full_counter_clone.fetch_add(1, atomic::Ordering::Release);
535 async move {
536 Ok(Some(lsp::SemanticTokensResult::Tokens(
537 lsp::SemanticTokens {
538 data: vec![
539 0, // delta_line
540 3, // delta_start
541 4, // length
542 0, // token_type
543 0, // token_modifiers_bitset
544 ],
545 // The server isn't capable of deltas, so even though we sent back
546 // a result ID, the client shouldn't request a delta.
547 result_id: Some("a".into()),
548 },
549 )))
550 }
551 },
552 );
553
554 cx.set_state("ˇfn main() {}");
555 assert!(full_request.next().await.is_some());
556
557 cx.run_until_parked();
558
559 cx.set_state("ˇfn main() { a }");
560 assert!(full_request.next().await.is_some());
561
562 cx.run_until_parked();
563
564 assert_eq!(
565 extract_semantic_highlights(&cx.editor, &cx),
566 vec![MultiBufferOffset(3)..MultiBufferOffset(7)]
567 );
568
569 assert_eq!(full_counter.load(atomic::Ordering::Acquire), 2);
570 }
571
572 #[gpui::test]
573 async fn lsp_semantic_tokens_dynamic_registration_requeries_open_document(
574 cx: &mut TestAppContext,
575 ) {
576 init_test(cx, |_| {});
577
578 update_test_language_settings(cx, &|language_settings| {
579 language_settings.languages.0.insert(
580 "Rust".into(),
581 LanguageSettingsContent {
582 semantic_tokens: Some(SemanticTokens::Full),
583 ..LanguageSettingsContent::default()
584 },
585 );
586 });
587
588 // The server advertises no semantic tokens capability up front; it only
589 // registers `textDocument/semanticTokens` dynamically, after the document
590 // is already open (as Roslyn does).
591 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
592
593 let full_counter = Arc::new(AtomicUsize::new(0));
594 let _full_request = cx
595 .set_request_handler::<lsp::request::SemanticTokensFullRequest, _, _>({
596 let full_counter = full_counter.clone();
597 move |_, _, _| {
598 full_counter.fetch_add(1, atomic::Ordering::Release);
599 async move {
600 Ok(Some(lsp::SemanticTokensResult::Tokens(
601 lsp::SemanticTokens {
602 data: vec![0, 3, 4, 0, 0],
603 result_id: None,
604 },
605 )))
606 }
607 }
608 });
609
610 cx.set_state("ˇfn main() {}");
611 // Drain the refresh scheduled on open (while no capability exists yet), so a
612 // later request can only come from the dynamic-registration refresh itself.
613 cx.executor().advance_clock(Duration::from_millis(200));
614 cx.run_until_parked();
615 assert_eq!(
616 full_counter.load(atomic::Ordering::Acquire),
617 0,
618 "no semantic tokens should be requested before the capability is registered"
619 );
620 assert!(
621 extract_semantic_highlights(&cx.editor, &cx).is_empty(),
622 "no semantic highlights before the capability is registered"
623 );
624
625 cx.lsp
626 .request::<lsp::request::RegisterCapability>(
627 lsp::RegistrationParams {
628 registrations: vec![lsp::Registration {
629 id: "semantic-tokens".to_string(),
630 method: "textDocument/semanticTokens".to_string(),
631 register_options: Some(
632 serde_json::to_value(lsp::SemanticTokensRegistrationOptions {
633 text_document_registration_options:
634 lsp::TextDocumentRegistrationOptions {
635 document_selector: None,
636 },
637 semantic_tokens_options: lsp::SemanticTokensOptions {
638 legend: lsp::SemanticTokensLegend {
639 token_types: vec!["function".into()],
640 token_modifiers: Vec::new(),
641 },
642 full: Some(lsp::SemanticTokensFullOptions::Bool(true)),
643 ..lsp::SemanticTokensOptions::default()
644 },
645 static_registration_options: lsp::StaticRegistrationOptions {
646 id: None,
647 },
648 })
649 .unwrap(),
650 ),
651 }],
652 },
653 lsp::DEFAULT_LSP_REQUEST_TIMEOUT,
654 )
655 .await
656 .into_response()
657 .expect("register capability request failed");
658
659 cx.executor().advance_clock(Duration::from_millis(200));
660 cx.run_until_parked();
661 assert!(
662 full_counter.load(atomic::Ordering::Acquire) >= 1,
663 "dynamic registration should re-query semantic tokens for the open document"
664 );
665
666 assert_eq!(
667 extract_semantic_highlights(&cx.editor, &cx),
668 vec![MultiBufferOffset(3)..MultiBufferOffset(7)],
669 "the open document should display semantic tokens after dynamic registration"
670 );
671 }
672
673 #[gpui::test]
674 async fn lsp_semantic_tokens_full_none_result_id(cx: &mut TestAppContext) {
675 init_test(cx, |_| {});
676
677 update_test_language_settings(cx, &|language_settings| {
678 language_settings.languages.0.insert(
679 "Rust".into(),
680 LanguageSettingsContent {
681 semantic_tokens: Some(SemanticTokens::Full),
682 ..LanguageSettingsContent::default()
683 },
684 );
685 });
686
687 let mut cx = EditorLspTestContext::new_rust(
688 lsp::ServerCapabilities {
689 semantic_tokens_provider: Some(
690 lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(
691 lsp::SemanticTokensOptions {
692 legend: lsp::SemanticTokensLegend {
693 token_types: vec!["function".into()],
694 token_modifiers: Vec::new(),
695 },
696 full: Some(lsp::SemanticTokensFullOptions::Delta { delta: Some(true) }),
697 ..lsp::SemanticTokensOptions::default()
698 },
699 ),
700 ),
701 ..lsp::ServerCapabilities::default()
702 },
703 cx,
704 )
705 .await;
706
707 let full_counter = Arc::new(AtomicUsize::new(0));
708 let full_counter_clone = full_counter.clone();
709
710 let mut full_request = cx
711 .set_request_handler::<lsp::request::SemanticTokensFullRequest, _, _>(
712 move |_, _, _| {
713 full_counter_clone.fetch_add(1, atomic::Ordering::Release);
714 async move {
715 Ok(Some(lsp::SemanticTokensResult::Tokens(
716 lsp::SemanticTokens {
717 data: vec![
718 0, // delta_line
719 3, // delta_start
720 4, // length
721 0, // token_type
722 0, // token_modifiers_bitset
723 ],
724 result_id: None, // Sending back `None` forces the client to not use deltas.
725 },
726 )))
727 }
728 },
729 );
730
731 cx.set_state("ˇfn main() {}");
732 assert!(full_request.next().await.is_some());
733
734 let task = cx.update_editor(|e, _, _| e.semantic_token_state.take_update_task());
735 task.await;
736
737 cx.set_state("ˇfn main() { a }");
738 assert!(full_request.next().await.is_some());
739
740 let task = cx.update_editor(|e, _, _| e.semantic_token_state.take_update_task());
741 task.await;
742 assert_eq!(
743 extract_semantic_highlights(&cx.editor, &cx),
744 vec![MultiBufferOffset(3)..MultiBufferOffset(7)]
745 );
746 assert_eq!(full_counter.load(atomic::Ordering::Acquire), 2);
747 }
748
749 #[gpui::test]
750 async fn lsp_semantic_tokens_delta(cx: &mut TestAppContext) {
751 init_test(cx, |_| {});
752
753 update_test_language_settings(cx, &|language_settings| {
754 language_settings.languages.0.insert(
755 "Rust".into(),
756 LanguageSettingsContent {
757 semantic_tokens: Some(SemanticTokens::Full),
758 ..LanguageSettingsContent::default()
759 },
760 );
761 });
762
763 let mut cx = EditorLspTestContext::new_rust(
764 lsp::ServerCapabilities {
765 semantic_tokens_provider: Some(
766 lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(
767 lsp::SemanticTokensOptions {
768 legend: lsp::SemanticTokensLegend {
769 token_types: vec!["function".into()],
770 token_modifiers: Vec::new(),
771 },
772 full: Some(lsp::SemanticTokensFullOptions::Delta { delta: Some(true) }),
773 ..lsp::SemanticTokensOptions::default()
774 },
775 ),
776 ),
777 ..lsp::ServerCapabilities::default()
778 },
779 cx,
780 )
781 .await;
782
783 let full_counter = Arc::new(AtomicUsize::new(0));
784 let full_counter_clone = full_counter.clone();
785 let delta_counter = Arc::new(AtomicUsize::new(0));
786 let delta_counter_clone = delta_counter.clone();
787
788 let mut full_request = cx
789 .set_request_handler::<lsp::request::SemanticTokensFullRequest, _, _>(
790 move |_, _, _| {
791 full_counter_clone.fetch_add(1, atomic::Ordering::Release);
792 async move {
793 Ok(Some(lsp::SemanticTokensResult::Tokens(
794 lsp::SemanticTokens {
795 data: vec![
796 0, // delta_line
797 3, // delta_start
798 4, // length
799 0, // token_type
800 0, // token_modifiers_bitset
801 ],
802 result_id: Some("a".into()),
803 },
804 )))
805 }
806 },
807 );
808
809 let mut delta_request = cx
810 .set_request_handler::<lsp::request::SemanticTokensFullDeltaRequest, _, _>(
811 move |_, params, _| {
812 delta_counter_clone.fetch_add(1, atomic::Ordering::Release);
813 assert_eq!(params.previous_result_id, "a");
814 async move {
815 Ok(Some(lsp::SemanticTokensFullDeltaResult::TokensDelta(
816 lsp::SemanticTokensDelta {
817 edits: Vec::new(),
818 result_id: Some("b".into()),
819 },
820 )))
821 }
822 },
823 );
824
825 // Initial request, for the empty buffer.
826 cx.set_state("ˇfn main() {}");
827 assert!(full_request.next().await.is_some());
828 let task = cx.update_editor(|e, _, _| e.semantic_token_state.take_update_task());
829 task.await;
830
831 cx.set_state("ˇfn main() { a }");
832 assert!(delta_request.next().await.is_some());
833 let task = cx.update_editor(|e, _, _| e.semantic_token_state.take_update_task());
834 task.await;
835
836 assert_eq!(
837 extract_semantic_highlights(&cx.editor, &cx),
838 vec![MultiBufferOffset(3)..MultiBufferOffset(7)]
839 );
840
841 assert_eq!(full_counter.load(atomic::Ordering::Acquire), 1);
842 assert_eq!(delta_counter.load(atomic::Ordering::Acquire), 1);
843 }
844
845 #[gpui::test]
846 async fn lsp_semantic_tokens_multiserver_full(cx: &mut TestAppContext) {
847 init_test(cx, |_| {});
848
849 update_test_language_settings(cx, &|language_settings| {
850 language_settings.languages.0.insert(
851 "TOML".into(),
852 LanguageSettingsContent {
853 semantic_tokens: Some(SemanticTokens::Full),
854 ..LanguageSettingsContent::default()
855 },
856 );
857 });
858
859 let toml_language = Arc::new(Language::new(
860 LanguageConfig {
861 name: "TOML".into(),
862 matcher: (LanguageMatcher {
863 path_suffixes: vec!["toml".into()],
864 ..LanguageMatcher::default()
865 })
866 .into(),
867 ..LanguageConfig::default()
868 },
869 None,
870 ));
871
872 // We have 2 language servers for TOML in this test.
873 let toml_legend_1 = lsp::SemanticTokensLegend {
874 token_types: vec!["property".into()],
875 token_modifiers: Vec::new(),
876 };
877 let toml_legend_2 = lsp::SemanticTokensLegend {
878 token_types: vec!["number".into()],
879 token_modifiers: Vec::new(),
880 };
881
882 let app_state = cx.update(workspace::AppState::test);
883
884 cx.update(|cx| {
885 assets::Assets.load_test_fonts(cx);
886 crate::init(cx);
887 workspace::init(app_state.clone(), cx);
888 });
889
890 let project = Project::test(app_state.fs.clone(), [], cx).await;
891 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
892
893 let full_counter_toml_1 = Arc::new(AtomicUsize::new(0));
894 let full_counter_toml_1_clone = full_counter_toml_1.clone();
895 let full_counter_toml_2 = Arc::new(AtomicUsize::new(0));
896 let full_counter_toml_2_clone = full_counter_toml_2.clone();
897
898 let mut toml_server_1 = language_registry.register_fake_lsp(
899 toml_language.name(),
900 FakeLspAdapter {
901 name: "toml1",
902 capabilities: lsp::ServerCapabilities {
903 semantic_tokens_provider: Some(
904 lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(
905 lsp::SemanticTokensOptions {
906 legend: toml_legend_1,
907 full: Some(lsp::SemanticTokensFullOptions::Delta { delta: None }),
908 ..lsp::SemanticTokensOptions::default()
909 },
910 ),
911 ),
912 ..lsp::ServerCapabilities::default()
913 },
914 initializer: Some(Box::new({
915 let full_counter_toml_1_clone = full_counter_toml_1_clone.clone();
916 move |fake_server| {
917 let full_counter = full_counter_toml_1_clone.clone();
918 fake_server
919 .set_request_handler::<lsp::request::SemanticTokensFullRequest, _, _>(
920 move |_, _| {
921 full_counter.fetch_add(1, atomic::Ordering::Release);
922 async move {
923 Ok(Some(lsp::SemanticTokensResult::Tokens(
924 lsp::SemanticTokens {
925 // highlight 'a' as a property
926 data: vec![
927 0, // delta_line
928 0, // delta_start
929 1, // length
930 0, // token_type
931 0, // token_modifiers_bitset
932 ],
933 result_id: Some("a".into()),
934 },
935 )))
936 }
937 },
938 );
939 }
940 })),
941 ..FakeLspAdapter::default()
942 },
943 );
944 let mut toml_server_2 = language_registry.register_fake_lsp(
945 toml_language.name(),
946 FakeLspAdapter {
947 name: "toml2",
948 capabilities: lsp::ServerCapabilities {
949 semantic_tokens_provider: Some(
950 lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(
951 lsp::SemanticTokensOptions {
952 legend: toml_legend_2,
953 full: Some(lsp::SemanticTokensFullOptions::Delta { delta: None }),
954 ..lsp::SemanticTokensOptions::default()
955 },
956 ),
957 ),
958 ..lsp::ServerCapabilities::default()
959 },
960 initializer: Some(Box::new({
961 let full_counter_toml_2_clone = full_counter_toml_2_clone.clone();
962 move |fake_server| {
963 let full_counter = full_counter_toml_2_clone.clone();
964 fake_server
965 .set_request_handler::<lsp::request::SemanticTokensFullRequest, _, _>(
966 move |_, _| {
967 full_counter.fetch_add(1, atomic::Ordering::Release);
968 async move {
969 Ok(Some(lsp::SemanticTokensResult::Tokens(
970 lsp::SemanticTokens {
971 // highlight '3' as a literal
972 data: vec![
973 0, // delta_line
974 4, // delta_start
975 1, // length
976 0, // token_type
977 0, // token_modifiers_bitset
978 ],
979 result_id: Some("a".into()),
980 },
981 )))
982 }
983 },
984 );
985 }
986 })),
987 ..FakeLspAdapter::default()
988 },
989 );
990 language_registry.add(toml_language.clone());
991
992 app_state
993 .fs
994 .as_fake()
995 .insert_tree(
996 EditorLspTestContext::root_path(),
997 json!({
998 ".git": {},
999 "dir": {
1000 "foo.toml": "a = 1\nb = 2\n",
1001 }
1002 }),
1003 )
1004 .await;
1005
1006 let (multi_workspace, cx) =
1007 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1008 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1009 project
1010 .update(cx, |project, cx| {
1011 project.find_or_create_worktree(EditorLspTestContext::root_path(), true, cx)
1012 })
1013 .await
1014 .unwrap();
1015 cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
1016 .await;
1017
1018 let toml_file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
1019 let toml_item = workspace
1020 .update_in(cx, |workspace, window, cx| {
1021 workspace.open_path(toml_file, None, true, window, cx)
1022 })
1023 .await
1024 .expect("Could not open test file");
1025
1026 let editor = cx.update(|_, cx| {
1027 toml_item
1028 .act_as::<Editor>(cx)
1029 .expect("Opened test file wasn't an editor")
1030 });
1031
1032 editor.update_in(cx, |editor, window, cx| {
1033 let nav_history = workspace
1034 .read(cx)
1035 .active_pane()
1036 .read(cx)
1037 .nav_history_for_item(&cx.entity());
1038 editor.set_nav_history(Some(nav_history));
1039 window.focus(&editor.focus_handle(cx), cx)
1040 });
1041
1042 let _toml_server_1 = toml_server_1.next().await.unwrap();
1043 let _toml_server_2 = toml_server_2.next().await.unwrap();
1044
1045 // Trigger semantic tokens.
1046 editor.update_in(cx, |editor, _, cx| {
1047 editor.edit([(MultiBufferOffset(0)..MultiBufferOffset(1), "b")], cx);
1048 });
1049 cx.executor().advance_clock(Duration::from_millis(200));
1050 let task = editor.update_in(cx, |e, _, _| e.semantic_token_state.take_update_task());
1051 cx.run_until_parked();
1052 task.await;
1053
1054 assert_eq!(
1055 extract_semantic_highlights(&editor, &cx),
1056 vec![
1057 MultiBufferOffset(0)..MultiBufferOffset(1),
1058 MultiBufferOffset(4)..MultiBufferOffset(5),
1059 ]
1060 );
1061
1062 assert_eq!(full_counter_toml_1.load(atomic::Ordering::Acquire), 1);
1063 assert_eq!(full_counter_toml_2.load(atomic::Ordering::Acquire), 1);
1064 }
1065
1066 #[gpui::test]
1067 async fn lsp_semantic_tokens_multibuffer_part(cx: &mut TestAppContext) {
1068 init_test(cx, |_| {});
1069
1070 update_test_language_settings(cx, &|language_settings| {
1071 language_settings.languages.0.insert(
1072 "TOML".into(),
1073 LanguageSettingsContent {
1074 semantic_tokens: Some(SemanticTokens::Full),
1075 ..LanguageSettingsContent::default()
1076 },
1077 );
1078 language_settings.languages.0.insert(
1079 "Rust".into(),
1080 LanguageSettingsContent {
1081 semantic_tokens: Some(SemanticTokens::Full),
1082 ..LanguageSettingsContent::default()
1083 },
1084 );
1085 });
1086
1087 let toml_language = Arc::new(Language::new(
1088 LanguageConfig {
1089 name: "TOML".into(),
1090 matcher: (LanguageMatcher {
1091 path_suffixes: vec!["toml".into()],
1092 ..LanguageMatcher::default()
1093 })
1094 .into(),
1095 ..LanguageConfig::default()
1096 },
1097 None,
1098 ));
1099 let rust_language = Arc::new(Language::new(
1100 LanguageConfig {
1101 name: "Rust".into(),
1102 matcher: (LanguageMatcher {
1103 path_suffixes: vec!["rs".into()],
1104 ..LanguageMatcher::default()
1105 })
1106 .into(),
1107 ..LanguageConfig::default()
1108 },
1109 None,
1110 ));
1111
1112 let toml_legend = lsp::SemanticTokensLegend {
1113 token_types: vec!["property".into()],
1114 token_modifiers: Vec::new(),
1115 };
1116 let rust_legend = lsp::SemanticTokensLegend {
1117 token_types: vec!["constant".into()],
1118 token_modifiers: Vec::new(),
1119 };
1120
1121 let app_state = cx.update(workspace::AppState::test);
1122
1123 cx.update(|cx| {
1124 assets::Assets.load_test_fonts(cx);
1125 crate::init(cx);
1126 workspace::init(app_state.clone(), cx);
1127 });
1128
1129 let project = Project::test(app_state.fs.clone(), [], cx).await;
1130 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1131 let full_counter_toml = Arc::new(AtomicUsize::new(0));
1132 let full_counter_toml_clone = full_counter_toml.clone();
1133
1134 let mut toml_server = language_registry.register_fake_lsp(
1135 toml_language.name(),
1136 FakeLspAdapter {
1137 name: "toml",
1138 capabilities: lsp::ServerCapabilities {
1139 semantic_tokens_provider: Some(
1140 lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(
1141 lsp::SemanticTokensOptions {
1142 legend: toml_legend,
1143 full: Some(lsp::SemanticTokensFullOptions::Delta { delta: None }),
1144 ..lsp::SemanticTokensOptions::default()
1145 },
1146 ),
1147 ),
1148 ..lsp::ServerCapabilities::default()
1149 },
1150 initializer: Some(Box::new({
1151 let full_counter_toml_clone = full_counter_toml_clone.clone();
1152 move |fake_server| {
1153 let full_counter = full_counter_toml_clone.clone();
1154 fake_server
1155 .set_request_handler::<lsp::request::SemanticTokensFullRequest, _, _>(
1156 move |_, _| {
1157 full_counter.fetch_add(1, atomic::Ordering::Release);
1158 async move {
1159 Ok(Some(lsp::SemanticTokensResult::Tokens(
1160 lsp::SemanticTokens {
1161 // highlight 'a', 'b', 'c' as properties on lines 0, 1, 2
1162 data: vec![
1163 0, // delta_line (line 0)
1164 0, // delta_start
1165 1, // length
1166 0, // token_type
1167 0, // token_modifiers_bitset
1168 1, // delta_line (line 1)
1169 0, // delta_start
1170 1, // length
1171 0, // token_type
1172 0, // token_modifiers_bitset
1173 1, // delta_line (line 2)
1174 0, // delta_start
1175 1, // length
1176 0, // token_type
1177 0, // token_modifiers_bitset
1178 ],
1179 result_id: Some("a".into()),
1180 },
1181 )))
1182 }
1183 },
1184 );
1185 }
1186 })),
1187 ..FakeLspAdapter::default()
1188 },
1189 );
1190 language_registry.add(toml_language.clone());
1191 let mut rust_server = language_registry.register_fake_lsp(
1192 rust_language.name(),
1193 FakeLspAdapter {
1194 name: "rust",
1195 capabilities: lsp::ServerCapabilities {
1196 semantic_tokens_provider: Some(
1197 lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(
1198 lsp::SemanticTokensOptions {
1199 legend: rust_legend,
1200 full: Some(lsp::SemanticTokensFullOptions::Delta { delta: None }),
1201 ..lsp::SemanticTokensOptions::default()
1202 },
1203 ),
1204 ),
1205 ..lsp::ServerCapabilities::default()
1206 },
1207 ..FakeLspAdapter::default()
1208 },
1209 );
1210 language_registry.add(rust_language.clone());
1211
1212 app_state
1213 .fs
1214 .as_fake()
1215 .insert_tree(
1216 EditorLspTestContext::root_path(),
1217 json!({
1218 ".git": {},
1219 "dir": {
1220 "foo.toml": "a = 1\nb = 2\nc = 3\n",
1221 "bar.rs": "const c: usize = 3;\n",
1222 }
1223 }),
1224 )
1225 .await;
1226
1227 let (multi_workspace, cx) =
1228 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1229 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1230 project
1231 .update(cx, |project, cx| {
1232 project.find_or_create_worktree(EditorLspTestContext::root_path(), true, cx)
1233 })
1234 .await
1235 .unwrap();
1236 cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
1237 .await;
1238
1239 let toml_file = cx.read(|cx| workspace.file_project_paths(cx)[1].clone());
1240 let rust_file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
1241 let (toml_item, rust_item) = workspace.update_in(cx, |workspace, window, cx| {
1242 (
1243 workspace.open_path(toml_file, None, true, window, cx),
1244 workspace.open_path(rust_file, None, true, window, cx),
1245 )
1246 });
1247 let toml_item = toml_item.await.expect("Could not open test file");
1248 let rust_item = rust_item.await.expect("Could not open test file");
1249
1250 let (toml_editor, rust_editor) = cx.update(|_, cx| {
1251 (
1252 toml_item
1253 .act_as::<Editor>(cx)
1254 .expect("Opened test file wasn't an editor"),
1255 rust_item
1256 .act_as::<Editor>(cx)
1257 .expect("Opened test file wasn't an editor"),
1258 )
1259 });
1260 let toml_buffer = cx.read(|cx| {
1261 toml_editor
1262 .read(cx)
1263 .buffer()
1264 .read(cx)
1265 .as_singleton()
1266 .unwrap()
1267 });
1268 let rust_buffer = cx.read(|cx| {
1269 rust_editor
1270 .read(cx)
1271 .buffer()
1272 .read(cx)
1273 .as_singleton()
1274 .unwrap()
1275 });
1276 let multibuffer = cx.new(|cx| {
1277 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
1278 multibuffer.set_excerpts_for_path(
1279 PathKey::sorted(0),
1280 toml_buffer.clone(),
1281 [Point::new(0, 0)..Point::new(0, 4)],
1282 0,
1283 cx,
1284 );
1285 multibuffer.set_excerpts_for_path(
1286 PathKey::sorted(1),
1287 rust_buffer.clone(),
1288 [Point::new(0, 0)..Point::new(0, 4)],
1289 0,
1290 cx,
1291 );
1292 multibuffer
1293 });
1294
1295 let editor = workspace.update_in(cx, |workspace, window, cx| {
1296 let editor = cx.new(|cx| build_editor_with_project(project, multibuffer, window, cx));
1297 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
1298 editor
1299 });
1300 editor.update_in(cx, |editor, window, cx| {
1301 let nav_history = workspace
1302 .read(cx)
1303 .active_pane()
1304 .read(cx)
1305 .nav_history_for_item(&cx.entity());
1306 editor.set_nav_history(Some(nav_history));
1307 window.focus(&editor.focus_handle(cx), cx)
1308 });
1309
1310 let _toml_server = toml_server.next().await.unwrap();
1311 let _rust_server = rust_server.next().await.unwrap();
1312
1313 // Initial request.
1314 cx.executor().advance_clock(Duration::from_millis(200));
1315 let task = editor.update_in(cx, |e, _, _| e.semantic_token_state.take_update_task());
1316 cx.run_until_parked();
1317 task.await;
1318 assert_eq!(full_counter_toml.load(atomic::Ordering::Acquire), 1);
1319 cx.run_until_parked();
1320
1321 // Initially, excerpt only covers line 0, so only the 'a' token should be highlighted.
1322 // The excerpt content is "a = 1\n" (6 chars), so 'a' is at offset 0.
1323 assert_eq!(
1324 extract_semantic_highlights(&editor, &cx),
1325 vec![MultiBufferOffset(0)..MultiBufferOffset(1)]
1326 );
1327
1328 // Get the excerpt id for the TOML excerpt and expand it down by 2 lines.
1329 let toml_anchor = editor.read_with(cx, |editor, cx| {
1330 editor
1331 .buffer()
1332 .read(cx)
1333 .snapshot(cx)
1334 .anchor_in_excerpt(text::Anchor::min_for_buffer(
1335 toml_buffer.read(cx).remote_id(),
1336 ))
1337 .unwrap()
1338 });
1339 editor.update_in(cx, |editor, _, cx| {
1340 editor.buffer().update(cx, |buffer, cx| {
1341 buffer.expand_excerpts([toml_anchor], 2, ExpandExcerptDirection::Down, cx);
1342 });
1343 });
1344
1345 // Wait for semantic tokens to be re-fetched after expansion.
1346 cx.executor().advance_clock(Duration::from_millis(200));
1347 let task = editor.update_in(cx, |e, _, _| e.semantic_token_state.take_update_task());
1348 cx.run_until_parked();
1349 task.await;
1350
1351 // After expansion, the excerpt covers lines 0-2, so 'a', 'b', 'c' should all be highlighted.
1352 // Content is now "a = 1\nb = 2\nc = 3\n" (18 chars).
1353 // 'a' at offset 0, 'b' at offset 6, 'c' at offset 12.
1354 assert_eq!(
1355 extract_semantic_highlights(&editor, &cx),
1356 vec![
1357 MultiBufferOffset(0)..MultiBufferOffset(1),
1358 MultiBufferOffset(6)..MultiBufferOffset(7),
1359 MultiBufferOffset(12)..MultiBufferOffset(13),
1360 ]
1361 );
1362 }
1363
1364 #[gpui::test]
1365 async fn lsp_semantic_tokens_singleton_opened_from_multibuffer(cx: &mut TestAppContext) {
1366 init_test(cx, |_| {});
1367
1368 update_test_language_settings(cx, &|language_settings| {
1369 language_settings.languages.0.insert(
1370 "Rust".into(),
1371 LanguageSettingsContent {
1372 semantic_tokens: Some(SemanticTokens::Full),
1373 ..LanguageSettingsContent::default()
1374 },
1375 );
1376 });
1377
1378 let rust_language = Arc::new(Language::new(
1379 LanguageConfig {
1380 name: "Rust".into(),
1381 matcher: (LanguageMatcher {
1382 path_suffixes: vec!["rs".into()],
1383 ..LanguageMatcher::default()
1384 })
1385 .into(),
1386 ..LanguageConfig::default()
1387 },
1388 None,
1389 ));
1390
1391 let rust_legend = lsp::SemanticTokensLegend {
1392 token_types: vec!["function".into()],
1393 token_modifiers: Vec::new(),
1394 };
1395
1396 let app_state = cx.update(workspace::AppState::test);
1397 cx.update(|cx| {
1398 assets::Assets.load_test_fonts(cx);
1399 crate::init(cx);
1400 workspace::init(app_state.clone(), cx);
1401 });
1402
1403 let project = Project::test(app_state.fs.clone(), [], cx).await;
1404 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1405
1406 let mut rust_server = language_registry.register_fake_lsp(
1407 rust_language.name(),
1408 FakeLspAdapter {
1409 name: "rust",
1410 capabilities: lsp::ServerCapabilities {
1411 semantic_tokens_provider: Some(
1412 lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(
1413 lsp::SemanticTokensOptions {
1414 legend: rust_legend,
1415 full: Some(lsp::SemanticTokensFullOptions::Delta { delta: None }),
1416 ..lsp::SemanticTokensOptions::default()
1417 },
1418 ),
1419 ),
1420 ..lsp::ServerCapabilities::default()
1421 },
1422 initializer: Some(Box::new(move |fake_server| {
1423 fake_server
1424 .set_request_handler::<lsp::request::SemanticTokensFullRequest, _, _>(
1425 move |_, _| async move {
1426 Ok(Some(lsp::SemanticTokensResult::Tokens(
1427 lsp::SemanticTokens {
1428 data: vec![0, 3, 4, 0, 0],
1429 result_id: None,
1430 },
1431 )))
1432 },
1433 );
1434 })),
1435 ..FakeLspAdapter::default()
1436 },
1437 );
1438 language_registry.add(rust_language.clone());
1439
1440 // foo.rs must be long enough that autoscroll triggers an actual scroll
1441 // position change when opening from the multibuffer with cursor near
1442 // the end. This reproduces the race: set_visible_line_count spawns a
1443 // task, then autoscroll fires ScrollPositionChanged whose handler
1444 // replaces post_scroll_update with a debounced task that skips
1445 // update_lsp_data for singletons.
1446 let mut foo_content = String::from("fn test() {}\n");
1447 for i in 0..100 {
1448 foo_content.push_str(&format!("fn func_{i}() {{}}\n"));
1449 }
1450
1451 app_state
1452 .fs
1453 .as_fake()
1454 .insert_tree(
1455 EditorLspTestContext::root_path(),
1456 json!({
1457 ".git": {},
1458 "bar.rs": "fn main() {}\n",
1459 "foo.rs": foo_content,
1460 }),
1461 )
1462 .await;
1463
1464 let (multi_workspace, cx) =
1465 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
1466 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
1467 project
1468 .update(cx, |project, cx| {
1469 project.find_or_create_worktree(EditorLspTestContext::root_path(), true, cx)
1470 })
1471 .await
1472 .unwrap();
1473 cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
1474 .await;
1475
1476 // Open bar.rs as an editor to start the LSP server.
1477 let bar_file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
1478 let bar_item = workspace
1479 .update_in(cx, |workspace, window, cx| {
1480 workspace.open_path(bar_file, None, true, window, cx)
1481 })
1482 .await
1483 .expect("Could not open bar.rs");
1484 let bar_editor = cx.update(|_, cx| {
1485 bar_item
1486 .act_as::<Editor>(cx)
1487 .expect("Opened test file wasn't an editor")
1488 });
1489 let bar_buffer = cx.read(|cx| {
1490 bar_editor
1491 .read(cx)
1492 .buffer()
1493 .read(cx)
1494 .as_singleton()
1495 .unwrap()
1496 });
1497
1498 let _rust_server = rust_server.next().await.unwrap();
1499
1500 cx.executor().advance_clock(Duration::from_millis(200));
1501 let task = bar_editor.update_in(cx, |e, _, _| e.semantic_token_state.take_update_task());
1502 cx.run_until_parked();
1503 task.await;
1504 cx.run_until_parked();
1505
1506 assert!(
1507 !extract_semantic_highlights(&bar_editor, &cx).is_empty(),
1508 "bar.rs should have semantic tokens after initial open"
1509 );
1510
1511 // Get foo.rs buffer directly from the project. No editor has ever
1512 // fetched semantic tokens for this buffer.
1513 let foo_file = cx.read(|cx| workspace.file_project_paths(cx)[1].clone());
1514 let foo_buffer = project
1515 .update(cx, |project, cx| project.open_buffer(foo_file, cx))
1516 .await
1517 .expect("Could not open foo.rs buffer");
1518
1519 // Build a multibuffer with both files. The foo.rs excerpt covers a
1520 // range near the end of the file so that opening the singleton will
1521 // autoscroll to a position that requires changing scroll_position.
1522 let multibuffer = cx.new(|cx| {
1523 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
1524 multibuffer.set_excerpts_for_path(
1525 PathKey::sorted(0),
1526 bar_buffer.clone(),
1527 [Point::new(0, 0)..Point::new(0, 12)],
1528 0,
1529 cx,
1530 );
1531 multibuffer.set_excerpts_for_path(
1532 PathKey::sorted(1),
1533 foo_buffer.clone(),
1534 [Point::new(95, 0)..Point::new(100, 0)],
1535 0,
1536 cx,
1537 );
1538 multibuffer
1539 });
1540
1541 let mb_editor = workspace.update_in(cx, |workspace, window, cx| {
1542 let editor =
1543 cx.new(|cx| build_editor_with_project(project.clone(), multibuffer, window, cx));
1544 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
1545 editor
1546 });
1547 mb_editor.update_in(cx, |editor, window, cx| {
1548 let nav_history = workspace
1549 .read(cx)
1550 .active_pane()
1551 .read(cx)
1552 .nav_history_for_item(&cx.entity());
1553 editor.set_nav_history(Some(nav_history));
1554 window.focus(&editor.focus_handle(cx), cx)
1555 });
1556
1557 // Close bar.rs tab so only the multibuffer remains.
1558 workspace
1559 .update_in(cx, |workspace, window, cx| {
1560 let pane = workspace.active_pane().clone();
1561 pane.update(cx, |pane, cx| {
1562 pane.close_item_by_id(
1563 bar_editor.entity_id(),
1564 workspace::SaveIntent::Skip,
1565 window,
1566 cx,
1567 )
1568 })
1569 })
1570 .await
1571 .ok();
1572
1573 cx.run_until_parked();
1574
1575 // Position cursor in the foo.rs excerpt (near line 95+).
1576 mb_editor.update_in(cx, |editor, window, cx| {
1577 let snapshot = editor.display_snapshot(cx);
1578 let end = snapshot.buffer_snapshot().len();
1579 editor.change_selections(None.into(), window, cx, |s| {
1580 s.select_ranges([end..end]);
1581 });
1582 });
1583
1584 // Open the singleton from the multibuffer. open_buffers_in_workspace
1585 // creates the editor and calls change_selections with autoscroll.
1586 // During render, set_visible_line_count fires first (spawning a task),
1587 // then autoscroll_vertically scrolls to line ~95 which emits
1588 // ScrollPositionChanged, whose handler replaces post_scroll_update.
1589 mb_editor.update_in(cx, |editor, window, cx| {
1590 editor.open_excerpts(&crate::actions::OpenExcerpts, window, cx);
1591 });
1592
1593 cx.run_until_parked();
1594 cx.executor().advance_clock(Duration::from_millis(200));
1595 cx.run_until_parked();
1596
1597 let active_editor = workspace.read_with(cx, |workspace, cx| {
1598 workspace
1599 .active_item(cx)
1600 .and_then(|item| item.act_as::<Editor>(cx))
1601 .expect("Active item should be an editor")
1602 });
1603
1604 assert!(
1605 active_editor.read_with(cx, |editor, cx| editor.buffer().read(cx).is_singleton()),
1606 "Active editor should be a singleton buffer"
1607 );
1608
1609 // Wait for semantic tokens on the singleton.
1610 cx.executor().advance_clock(Duration::from_millis(200));
1611 let task = active_editor.update_in(cx, |e, _, _| e.semantic_token_state.take_update_task());
1612 task.await;
1613 cx.run_until_parked();
1614
1615 let highlights = extract_semantic_highlights(&active_editor, &cx);
1616 assert!(
1617 !highlights.is_empty(),
1618 "Singleton editor opened from multibuffer should have semantic tokens"
1619 );
1620 }
1621
1622 fn extract_semantic_highlights(
1623 editor: &Entity<Editor>,
1624 cx: &TestAppContext,
1625 ) -> Vec<Range<MultiBufferOffset>> {
1626 editor.read_with(cx, |editor, cx| {
1627 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
1628 editor
1629 .display_map
1630 .read(cx)
1631 .semantic_token_highlights
1632 .iter()
1633 .flat_map(|(_, (v, _))| v.iter())
1634 .map(|highlights| highlights.range.to_offset(&multi_buffer_snapshot))
1635 .collect()
1636 })
1637 }
1638
1639 #[gpui::test]
1640 async fn test_semantic_tokens_rules_changes_restyle_tokens(cx: &mut TestAppContext) {
1641 use gpui::{Hsla, Rgba, UpdateGlobal as _};
1642 use settings::{GlobalLspSettingsContent, SemanticTokenRule};
1643
1644 init_test(cx, |_| {});
1645
1646 update_test_language_settings(cx, &|language_settings| {
1647 language_settings.languages.0.insert(
1648 "Rust".into(),
1649 LanguageSettingsContent {
1650 semantic_tokens: Some(SemanticTokens::Full),
1651 ..LanguageSettingsContent::default()
1652 },
1653 );
1654 });
1655
1656 let mut cx = EditorLspTestContext::new_rust(
1657 lsp::ServerCapabilities {
1658 semantic_tokens_provider: Some(
1659 lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(
1660 lsp::SemanticTokensOptions {
1661 legend: lsp::SemanticTokensLegend {
1662 token_types: Vec::from(["function".into()]),
1663 token_modifiers: Vec::new(),
1664 },
1665 full: Some(lsp::SemanticTokensFullOptions::Delta { delta: None }),
1666 ..lsp::SemanticTokensOptions::default()
1667 },
1668 ),
1669 ),
1670 ..lsp::ServerCapabilities::default()
1671 },
1672 cx,
1673 )
1674 .await;
1675
1676 let mut full_request = cx
1677 .set_request_handler::<lsp::request::SemanticTokensFullRequest, _, _>(
1678 move |_, _, _| {
1679 async move {
1680 Ok(Some(lsp::SemanticTokensResult::Tokens(
1681 lsp::SemanticTokens {
1682 data: vec![
1683 0, // delta_line
1684 3, // delta_start
1685 4, // length
1686 0, // token_type (function)
1687 0, // token_modifiers_bitset
1688 ],
1689 result_id: None,
1690 },
1691 )))
1692 }
1693 },
1694 );
1695
1696 // Trigger initial semantic tokens fetch
1697 cx.set_state("ˇfn main() {}");
1698 full_request.next().await;
1699 cx.run_until_parked();
1700
1701 // Verify initial highlights exist (with no custom color yet)
1702 let initial_ranges = extract_semantic_highlights(&cx.editor, &cx);
1703 assert_eq!(
1704 initial_ranges,
1705 vec![MultiBufferOffset(3)..MultiBufferOffset(7)],
1706 "Should have initial semantic token highlights"
1707 );
1708 let initial_styles = extract_semantic_highlight_styles(&cx.editor, &cx);
1709 assert_eq!(initial_styles.len(), 1, "Should have one highlight style");
1710 // Initial color should be None or theme default (not red or blue)
1711 let initial_color = initial_styles[0].color;
1712
1713 // Set a custom foreground color for function tokens via settings.json
1714 let red_color = Rgba {
1715 r: 1.0,
1716 g: 0.0,
1717 b: 0.0,
1718 a: 1.0,
1719 };
1720 cx.update(|_, cx| {
1721 SettingsStore::update_global(cx, |store, cx| {
1722 store.update_user_settings(cx, |settings| {
1723 settings.global_lsp_settings = Some(GlobalLspSettingsContent {
1724 semantic_token_rules: Some(SemanticTokenRules {
1725 rules: Vec::from([SemanticTokenRule {
1726 token_type: Some("function".to_string()),
1727 foreground_color: Some(red_color),
1728 ..SemanticTokenRule::default()
1729 }]),
1730 }),
1731 ..GlobalLspSettingsContent::default()
1732 });
1733 });
1734 });
1735 });
1736
1737 // Trigger a refetch by making an edit (which forces semantic tokens update)
1738 cx.set_state("ˇfn main() { }");
1739 full_request.next().await;
1740 cx.run_until_parked();
1741
1742 // Verify the highlights now have the custom red color
1743 let styles_after_settings_change = extract_semantic_highlight_styles(&cx.editor, &cx);
1744 assert_eq!(
1745 styles_after_settings_change.len(),
1746 1,
1747 "Should still have one highlight"
1748 );
1749 assert_eq!(
1750 styles_after_settings_change[0].color,
1751 Some(Hsla::from(red_color)),
1752 "Highlight should have the custom red color from settings.json"
1753 );
1754 assert_ne!(
1755 styles_after_settings_change[0].color, initial_color,
1756 "Color should have changed from initial"
1757 );
1758 }
1759
1760 #[gpui::test]
1761 async fn test_theme_override_changes_restyle_semantic_tokens(cx: &mut TestAppContext) {
1762 use collections::IndexMap;
1763 use gpui::{Hsla, Rgba, UpdateGlobal as _};
1764 use theme_settings::{HighlightStyleContent, ThemeStyleContent};
1765
1766 init_test(cx, |_| {});
1767
1768 update_test_language_settings(cx, &|language_settings| {
1769 language_settings.languages.0.insert(
1770 "Rust".into(),
1771 LanguageSettingsContent {
1772 semantic_tokens: Some(SemanticTokens::Full),
1773 ..LanguageSettingsContent::default()
1774 },
1775 );
1776 });
1777
1778 let mut cx = EditorLspTestContext::new_rust(
1779 lsp::ServerCapabilities {
1780 semantic_tokens_provider: Some(
1781 lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(
1782 lsp::SemanticTokensOptions {
1783 legend: lsp::SemanticTokensLegend {
1784 token_types: Vec::from(["function".into()]),
1785 token_modifiers: Vec::new(),
1786 },
1787 full: Some(lsp::SemanticTokensFullOptions::Delta { delta: None }),
1788 ..lsp::SemanticTokensOptions::default()
1789 },
1790 ),
1791 ),
1792 ..lsp::ServerCapabilities::default()
1793 },
1794 cx,
1795 )
1796 .await;
1797
1798 let mut full_request = cx
1799 .set_request_handler::<lsp::request::SemanticTokensFullRequest, _, _>(
1800 move |_, _, _| async move {
1801 Ok(Some(lsp::SemanticTokensResult::Tokens(
1802 lsp::SemanticTokens {
1803 data: vec![
1804 0, // delta_line
1805 3, // delta_start
1806 4, // length
1807 0, // token_type (function)
1808 0, // token_modifiers_bitset
1809 ],
1810 result_id: None,
1811 },
1812 )))
1813 },
1814 );
1815
1816 cx.set_state("ˇfn main() {}");
1817 full_request.next().await;
1818 cx.run_until_parked();
1819
1820 let initial_styles = extract_semantic_highlight_styles(&cx.editor, &cx);
1821 assert_eq!(initial_styles.len(), 1, "Should have one highlight style");
1822 let initial_color = initial_styles[0].color;
1823
1824 // Changing experimental_theme_overrides triggers GlobalTheme reload,
1825 // which fires theme_changed → refresh_semantic_token_highlights.
1826 let red_color: Hsla = Rgba {
1827 r: 1.0,
1828 g: 0.0,
1829 b: 0.0,
1830 a: 1.0,
1831 }
1832 .into();
1833 cx.update(|_, cx| {
1834 SettingsStore::update_global(cx, |store, cx| {
1835 store.update_user_settings(cx, |settings| {
1836 settings.theme.experimental_theme_overrides = Some(ThemeStyleContent {
1837 syntax: IndexMap::from_iter([(
1838 "function".to_string(),
1839 HighlightStyleContent {
1840 color: Some("#ff0000".to_string()),
1841 background_color: None,
1842 font_style: None,
1843 font_weight: None,
1844 },
1845 )]),
1846 ..ThemeStyleContent::default()
1847 });
1848 });
1849 });
1850 });
1851
1852 cx.executor().advance_clock(Duration::from_millis(200));
1853 cx.run_until_parked();
1854
1855 let styles_after_override = extract_semantic_highlight_styles(&cx.editor, &cx);
1856 assert_eq!(styles_after_override.len(), 1);
1857 assert_eq!(
1858 styles_after_override[0].color,
1859 Some(red_color),
1860 "Highlight should have red color from theme override"
1861 );
1862 assert_ne!(
1863 styles_after_override[0].color, initial_color,
1864 "Color should have changed from initial"
1865 );
1866
1867 // Changing the override to a different color also restyles.
1868 let blue_color: Hsla = Rgba {
1869 r: 0.0,
1870 g: 0.0,
1871 b: 1.0,
1872 a: 1.0,
1873 }
1874 .into();
1875 cx.update(|_, cx| {
1876 SettingsStore::update_global(cx, |store, cx| {
1877 store.update_user_settings(cx, |settings| {
1878 settings.theme.experimental_theme_overrides = Some(ThemeStyleContent {
1879 syntax: IndexMap::from_iter([(
1880 "function".to_string(),
1881 HighlightStyleContent {
1882 color: Some("#0000ff".to_string()),
1883 background_color: None,
1884 font_style: None,
1885 font_weight: None,
1886 },
1887 )]),
1888 ..ThemeStyleContent::default()
1889 });
1890 });
1891 });
1892 });
1893
1894 cx.executor().advance_clock(Duration::from_millis(200));
1895 cx.run_until_parked();
1896
1897 let styles_after_second_override = extract_semantic_highlight_styles(&cx.editor, &cx);
1898 assert_eq!(styles_after_second_override.len(), 1);
1899 assert_eq!(
1900 styles_after_second_override[0].color,
1901 Some(blue_color),
1902 "Highlight should have blue color from updated theme override"
1903 );
1904
1905 // Removing overrides reverts to the original theme color.
1906 cx.update(|_, cx| {
1907 SettingsStore::update_global(cx, |store, cx| {
1908 store.update_user_settings(cx, |settings| {
1909 settings.theme.experimental_theme_overrides = None;
1910 });
1911 });
1912 });
1913
1914 cx.executor().advance_clock(Duration::from_millis(200));
1915 cx.run_until_parked();
1916
1917 let styles_after_clear = extract_semantic_highlight_styles(&cx.editor, &cx);
1918 assert_eq!(styles_after_clear.len(), 1);
1919 assert_eq!(
1920 styles_after_clear[0].color, initial_color,
1921 "Highlight should revert to initial color after clearing overrides"
1922 );
1923 }
1924
1925 #[gpui::test]
1926 async fn test_per_theme_overrides_restyle_semantic_tokens(cx: &mut TestAppContext) {
1927 use collections::IndexMap;
1928 use gpui::{Hsla, Rgba, UpdateGlobal as _};
1929 use theme_settings::{HighlightStyleContent, ThemeStyleContent};
1930 use ui::ActiveTheme as _;
1931
1932 init_test(cx, |_| {});
1933
1934 update_test_language_settings(cx, &|language_settings| {
1935 language_settings.languages.0.insert(
1936 "Rust".into(),
1937 LanguageSettingsContent {
1938 semantic_tokens: Some(SemanticTokens::Full),
1939 ..LanguageSettingsContent::default()
1940 },
1941 );
1942 });
1943
1944 let mut cx = EditorLspTestContext::new_rust(
1945 lsp::ServerCapabilities {
1946 semantic_tokens_provider: Some(
1947 lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(
1948 lsp::SemanticTokensOptions {
1949 legend: lsp::SemanticTokensLegend {
1950 token_types: Vec::from(["function".into()]),
1951 token_modifiers: Vec::new(),
1952 },
1953 full: Some(lsp::SemanticTokensFullOptions::Delta { delta: None }),
1954 ..lsp::SemanticTokensOptions::default()
1955 },
1956 ),
1957 ),
1958 ..lsp::ServerCapabilities::default()
1959 },
1960 cx,
1961 )
1962 .await;
1963
1964 let mut full_request = cx
1965 .set_request_handler::<lsp::request::SemanticTokensFullRequest, _, _>(
1966 move |_, _, _| async move {
1967 Ok(Some(lsp::SemanticTokensResult::Tokens(
1968 lsp::SemanticTokens {
1969 data: vec![
1970 0, // delta_line
1971 3, // delta_start
1972 4, // length
1973 0, // token_type (function)
1974 0, // token_modifiers_bitset
1975 ],
1976 result_id: None,
1977 },
1978 )))
1979 },
1980 );
1981
1982 cx.set_state("ˇfn main() {}");
1983 full_request.next().await;
1984 cx.run_until_parked();
1985
1986 let initial_styles = extract_semantic_highlight_styles(&cx.editor, &cx);
1987 assert_eq!(initial_styles.len(), 1, "Should have one highlight style");
1988 let initial_color = initial_styles[0].color;
1989
1990 // Per-theme overrides (theme_overrides keyed by theme name) also go through
1991 // GlobalTheme reload → theme_changed → refresh_semantic_token_highlights.
1992 let theme_name = cx.update(|_, cx| cx.theme().name.to_string());
1993 let green_color: Hsla = Rgba {
1994 r: 0.0,
1995 g: 1.0,
1996 b: 0.0,
1997 a: 1.0,
1998 }
1999 .into();
2000 cx.update(|_, cx| {
2001 SettingsStore::update_global(cx, |store, cx| {
2002 store.update_user_settings(cx, |settings| {
2003 settings.theme.theme_overrides = collections::HashMap::from_iter([(
2004 theme_name.clone(),
2005 ThemeStyleContent {
2006 syntax: IndexMap::from_iter([(
2007 "function".to_string(),
2008 HighlightStyleContent {
2009 color: Some("#00ff00".to_string()),
2010 background_color: None,
2011 font_style: None,
2012 font_weight: None,
2013 },
2014 )]),
2015 ..ThemeStyleContent::default()
2016 },
2017 )]);
2018 });
2019 });
2020 });
2021
2022 cx.executor().advance_clock(Duration::from_millis(200));
2023 cx.run_until_parked();
2024
2025 let styles_after_override = extract_semantic_highlight_styles(&cx.editor, &cx);
2026 assert_eq!(styles_after_override.len(), 1);
2027 assert_eq!(
2028 styles_after_override[0].color,
2029 Some(green_color),
2030 "Highlight should have green color from per-theme override"
2031 );
2032 assert_ne!(
2033 styles_after_override[0].color, initial_color,
2034 "Color should have changed from initial"
2035 );
2036 }
2037
2038 #[gpui::test]
2039 async fn test_stopping_language_server_clears_semantic_tokens(cx: &mut TestAppContext) {
2040 init_test(cx, |_| {});
2041
2042 update_test_language_settings(cx, &|language_settings| {
2043 language_settings.languages.0.insert(
2044 "Rust".into(),
2045 LanguageSettingsContent {
2046 semantic_tokens: Some(SemanticTokens::Full),
2047 ..LanguageSettingsContent::default()
2048 },
2049 );
2050 });
2051
2052 let mut cx = EditorLspTestContext::new_rust(
2053 lsp::ServerCapabilities {
2054 semantic_tokens_provider: Some(
2055 lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(
2056 lsp::SemanticTokensOptions {
2057 legend: lsp::SemanticTokensLegend {
2058 token_types: vec!["function".into()],
2059 token_modifiers: Vec::new(),
2060 },
2061 full: Some(lsp::SemanticTokensFullOptions::Delta { delta: None }),
2062 ..lsp::SemanticTokensOptions::default()
2063 },
2064 ),
2065 ),
2066 ..lsp::ServerCapabilities::default()
2067 },
2068 cx,
2069 )
2070 .await;
2071
2072 let mut full_request = cx
2073 .set_request_handler::<lsp::request::SemanticTokensFullRequest, _, _>(
2074 move |_, _, _| async move {
2075 Ok(Some(lsp::SemanticTokensResult::Tokens(
2076 lsp::SemanticTokens {
2077 data: vec![
2078 0, // delta_line
2079 3, // delta_start
2080 4, // length
2081 0, // token_type
2082 0, // token_modifiers_bitset
2083 ],
2084 result_id: None,
2085 },
2086 )))
2087 },
2088 );
2089
2090 cx.set_state("ˇfn main() {}");
2091 assert!(full_request.next().await.is_some());
2092 cx.run_until_parked();
2093
2094 assert_eq!(
2095 extract_semantic_highlights(&cx.editor, &cx),
2096 vec![MultiBufferOffset(3)..MultiBufferOffset(7)],
2097 "Semantic tokens should be present before stopping the server"
2098 );
2099
2100 cx.update_editor(|editor, _, cx| {
2101 let buffers = editor.buffer.read(cx).all_buffers().into_iter().collect();
2102 editor.project.as_ref().unwrap().update(cx, |project, cx| {
2103 project.stop_language_servers_for_buffers(buffers, HashSet::default(), cx);
2104 })
2105 });
2106 cx.executor().advance_clock(Duration::from_millis(200));
2107 cx.run_until_parked();
2108
2109 assert_eq!(
2110 extract_semantic_highlights(&cx.editor, &cx),
2111 Vec::new(),
2112 "Semantic tokens should be cleared after stopping the server"
2113 );
2114 }
2115
2116 #[gpui::test]
2117 async fn test_disabling_semantic_tokens_setting_clears_highlights(cx: &mut TestAppContext) {
2118 init_test(cx, |_| {});
2119
2120 update_test_language_settings(cx, &|language_settings| {
2121 language_settings.languages.0.insert(
2122 "Rust".into(),
2123 LanguageSettingsContent {
2124 semantic_tokens: Some(SemanticTokens::Full),
2125 ..LanguageSettingsContent::default()
2126 },
2127 );
2128 });
2129
2130 let mut cx = EditorLspTestContext::new_rust(
2131 lsp::ServerCapabilities {
2132 semantic_tokens_provider: Some(
2133 lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(
2134 lsp::SemanticTokensOptions {
2135 legend: lsp::SemanticTokensLegend {
2136 token_types: vec!["function".into()],
2137 token_modifiers: Vec::new(),
2138 },
2139 full: Some(lsp::SemanticTokensFullOptions::Delta { delta: None }),
2140 ..lsp::SemanticTokensOptions::default()
2141 },
2142 ),
2143 ),
2144 ..lsp::ServerCapabilities::default()
2145 },
2146 cx,
2147 )
2148 .await;
2149
2150 let mut full_request = cx
2151 .set_request_handler::<lsp::request::SemanticTokensFullRequest, _, _>(
2152 move |_, _, _| async move {
2153 Ok(Some(lsp::SemanticTokensResult::Tokens(
2154 lsp::SemanticTokens {
2155 data: vec![
2156 0, // delta_line
2157 3, // delta_start
2158 4, // length
2159 0, // token_type
2160 0, // token_modifiers_bitset
2161 ],
2162 result_id: None,
2163 },
2164 )))
2165 },
2166 );
2167
2168 cx.set_state("ˇfn main() {}");
2169 assert!(full_request.next().await.is_some());
2170 cx.run_until_parked();
2171
2172 assert_eq!(
2173 extract_semantic_highlights(&cx.editor, &cx),
2174 vec![MultiBufferOffset(3)..MultiBufferOffset(7)],
2175 "Semantic tokens should be present before disabling the setting"
2176 );
2177
2178 update_test_language_settings(&mut cx, &|language_settings| {
2179 language_settings.languages.0.insert(
2180 "Rust".into(),
2181 LanguageSettingsContent {
2182 semantic_tokens: Some(SemanticTokens::Off),
2183 ..LanguageSettingsContent::default()
2184 },
2185 );
2186 });
2187 cx.executor().advance_clock(Duration::from_millis(200));
2188 cx.run_until_parked();
2189
2190 assert_eq!(
2191 extract_semantic_highlights(&cx.editor, &cx),
2192 Vec::new(),
2193 "Semantic tokens should be cleared after disabling the setting"
2194 );
2195 }
2196
2197 #[gpui::test]
2198 async fn test_semantic_token_disabling_with_empty_rule(cx: &mut TestAppContext) {
2199 init_test(cx, |_| {});
2200 update_test_language_settings(cx, &|s| {
2201 s.languages.0.insert(
2202 "Rust".into(),
2203 LanguageSettingsContent {
2204 semantic_tokens: Some(SemanticTokens::Full),
2205 ..Default::default()
2206 },
2207 );
2208 });
2209
2210 let mut cx = EditorLspTestContext::new_rust(
2211 lsp::ServerCapabilities {
2212 semantic_tokens_provider: Some(
2213 lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(
2214 lsp::SemanticTokensOptions {
2215 legend: lsp::SemanticTokensLegend {
2216 token_types: vec!["function".into()],
2217 token_modifiers: vec![],
2218 },
2219 full: Some(lsp::SemanticTokensFullOptions::Delta { delta: None }),
2220 ..Default::default()
2221 },
2222 ),
2223 ),
2224 ..Default::default()
2225 },
2226 cx,
2227 )
2228 .await;
2229
2230 let mut full_request = cx
2231 .set_request_handler::<lsp::request::SemanticTokensFullRequest, _, _>(
2232 move |_, _, _| async move {
2233 Ok(Some(lsp::SemanticTokensResult::Tokens(
2234 lsp::SemanticTokens {
2235 data: vec![0, 3, 4, 0, 0],
2236 result_id: None,
2237 },
2238 )))
2239 },
2240 );
2241
2242 // Verify it highlights by default
2243 cx.set_state("ˇfn main() {}");
2244 full_request.next().await;
2245 cx.run_until_parked();
2246 assert_eq!(extract_semantic_highlights(&cx.editor, &cx).len(), 1);
2247
2248 // Apply EMPTY rule to disable it
2249 cx.update(|_, cx| {
2250 SettingsStore::update_global(cx, |store, cx| {
2251 store.update_user_settings(cx, |settings| {
2252 settings.global_lsp_settings = Some(GlobalLspSettingsContent {
2253 semantic_token_rules: Some(SemanticTokenRules {
2254 rules: vec![SemanticTokenRule {
2255 token_type: Some("function".to_string()),
2256 ..Default::default()
2257 }],
2258 }),
2259 ..Default::default()
2260 });
2261 });
2262 });
2263 });
2264
2265 cx.set_state("ˇfn main() { }");
2266 full_request.next().await;
2267 cx.run_until_parked();
2268
2269 assert!(
2270 extract_semantic_highlights(&cx.editor, &cx).is_empty(),
2271 "Highlighting should be disabled by empty style setting"
2272 );
2273 }
2274
2275 #[gpui::test]
2276 async fn test_semantic_token_broad_rule_disables_specific_token(cx: &mut TestAppContext) {
2277 init_test(cx, |_| {});
2278 update_test_language_settings(cx, &|s| {
2279 s.languages.0.insert(
2280 "Rust".into(),
2281 LanguageSettingsContent {
2282 semantic_tokens: Some(SemanticTokens::Full),
2283 ..Default::default()
2284 },
2285 );
2286 });
2287
2288 let mut cx = EditorLspTestContext::new_rust(
2289 lsp::ServerCapabilities {
2290 semantic_tokens_provider: Some(
2291 lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(
2292 lsp::SemanticTokensOptions {
2293 legend: lsp::SemanticTokensLegend {
2294 token_types: vec!["comment".into()],
2295 token_modifiers: vec!["documentation".into()],
2296 },
2297 full: Some(lsp::SemanticTokensFullOptions::Delta { delta: None }),
2298 ..Default::default()
2299 },
2300 ),
2301 ),
2302 ..Default::default()
2303 },
2304 cx,
2305 )
2306 .await;
2307
2308 let mut full_request = cx
2309 .set_request_handler::<lsp::request::SemanticTokensFullRequest, _, _>(
2310 move |_, _, _| async move {
2311 Ok(Some(lsp::SemanticTokensResult::Tokens(
2312 lsp::SemanticTokens {
2313 data: vec![0, 0, 5, 0, 1], // comment [documentation]
2314 result_id: None,
2315 },
2316 )))
2317 },
2318 );
2319
2320 cx.set_state("ˇ/// d\n");
2321 full_request.next().await;
2322 cx.run_until_parked();
2323 assert_eq!(
2324 extract_semantic_highlights(&cx.editor, &cx).len(),
2325 1,
2326 "Documentation comment should be highlighted"
2327 );
2328
2329 // Apply a BROAD empty rule for "comment" (no modifiers)
2330 cx.update(|_, cx| {
2331 SettingsStore::update_global(cx, |store, cx| {
2332 store.update_user_settings(cx, |settings| {
2333 settings.global_lsp_settings = Some(GlobalLspSettingsContent {
2334 semantic_token_rules: Some(SemanticTokenRules {
2335 rules: vec![SemanticTokenRule {
2336 token_type: Some("comment".to_string()),
2337 ..Default::default()
2338 }],
2339 }),
2340 ..Default::default()
2341 });
2342 });
2343 });
2344 });
2345
2346 cx.set_state("ˇ/// d\n");
2347 full_request.next().await;
2348 cx.run_until_parked();
2349
2350 assert!(
2351 extract_semantic_highlights(&cx.editor, &cx).is_empty(),
2352 "Broad empty rule should disable specific documentation comment"
2353 );
2354 }
2355
2356 #[gpui::test]
2357 async fn test_semantic_token_specific_rule_does_not_disable_broad_token(
2358 cx: &mut TestAppContext,
2359 ) {
2360 use gpui::UpdateGlobal as _;
2361 use settings::{GlobalLspSettingsContent, SemanticTokenRule};
2362
2363 init_test(cx, |_| {});
2364 update_test_language_settings(cx, &|s| {
2365 s.languages.0.insert(
2366 "Rust".into(),
2367 LanguageSettingsContent {
2368 semantic_tokens: Some(SemanticTokens::Full),
2369 ..Default::default()
2370 },
2371 );
2372 });
2373
2374 let mut cx = EditorLspTestContext::new_rust(
2375 lsp::ServerCapabilities {
2376 semantic_tokens_provider: Some(
2377 lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(
2378 lsp::SemanticTokensOptions {
2379 legend: lsp::SemanticTokensLegend {
2380 token_types: vec!["comment".into()],
2381 token_modifiers: vec!["documentation".into()],
2382 },
2383 full: Some(lsp::SemanticTokensFullOptions::Delta { delta: None }),
2384 ..Default::default()
2385 },
2386 ),
2387 ),
2388 ..Default::default()
2389 },
2390 cx,
2391 )
2392 .await;
2393
2394 let mut full_request = cx
2395 .set_request_handler::<lsp::request::SemanticTokensFullRequest, _, _>(
2396 move |_, _, _| async move {
2397 Ok(Some(lsp::SemanticTokensResult::Tokens(
2398 lsp::SemanticTokens {
2399 data: vec![
2400 0, 0, 5, 0, 1, // comment [documentation]
2401 1, 0, 5, 0, 0, // normal comment
2402 ],
2403 result_id: None,
2404 },
2405 )))
2406 },
2407 );
2408
2409 cx.set_state("ˇ/// d\n// n\n");
2410 full_request.next().await;
2411 cx.run_until_parked();
2412 assert_eq!(
2413 extract_semantic_highlights(&cx.editor, &cx).len(),
2414 2,
2415 "Both documentation and normal comments should be highlighted initially"
2416 );
2417
2418 // Apply a SPECIFIC empty rule for documentation only
2419 cx.update(|_, cx| {
2420 SettingsStore::update_global(cx, |store, cx| {
2421 store.update_user_settings(cx, |settings| {
2422 settings.global_lsp_settings = Some(GlobalLspSettingsContent {
2423 semantic_token_rules: Some(SemanticTokenRules {
2424 rules: vec![SemanticTokenRule {
2425 token_type: Some("comment".to_string()),
2426 token_modifiers: vec!["documentation".to_string()],
2427 ..Default::default()
2428 }],
2429 }),
2430 ..Default::default()
2431 });
2432 });
2433 });
2434 });
2435
2436 cx.set_state("ˇ/// d\n// n\n");
2437 full_request.next().await;
2438 cx.run_until_parked();
2439
2440 assert_eq!(
2441 extract_semantic_highlights(&cx.editor, &cx).len(),
2442 1,
2443 "Normal comment should still be highlighted (matched by default rule)"
2444 );
2445 }
2446
2447 #[gpui::test]
2448 async fn test_diagnostics_visible_when_semantic_token_set_to_full(cx: &mut TestAppContext) {
2449 init_test(cx, |_| {});
2450
2451 update_test_language_settings(cx, &|language_settings| {
2452 language_settings.languages.0.insert(
2453 "Rust".into(),
2454 LanguageSettingsContent {
2455 semantic_tokens: Some(SemanticTokens::Full),
2456 ..LanguageSettingsContent::default()
2457 },
2458 );
2459 });
2460
2461 let mut cx = EditorLspTestContext::new_rust(
2462 lsp::ServerCapabilities {
2463 semantic_tokens_provider: Some(
2464 lsp::SemanticTokensServerCapabilities::SemanticTokensOptions(
2465 lsp::SemanticTokensOptions {
2466 legend: lsp::SemanticTokensLegend {
2467 token_types: vec!["function".into()],
2468 token_modifiers: Vec::new(),
2469 },
2470 full: Some(lsp::SemanticTokensFullOptions::Delta { delta: None }),
2471 ..lsp::SemanticTokensOptions::default()
2472 },
2473 ),
2474 ),
2475 ..lsp::ServerCapabilities::default()
2476 },
2477 cx,
2478 )
2479 .await;
2480
2481 let mut full_request = cx
2482 .set_request_handler::<lsp::request::SemanticTokensFullRequest, _, _>(
2483 move |_, _, _| {
2484 async move {
2485 Ok(Some(lsp::SemanticTokensResult::Tokens(
2486 lsp::SemanticTokens {
2487 data: vec![
2488 0, // delta_line
2489 3, // delta_start
2490 4, // length
2491 0, // token_type
2492 0, // token_modifiers_bitset
2493 ],
2494 result_id: Some("a".into()),
2495 },
2496 )))
2497 }
2498 },
2499 );
2500
2501 cx.set_state("ˇfn main() {}");
2502 assert!(full_request.next().await.is_some());
2503
2504 let task = cx.update_editor(|e, _, _| e.semantic_token_state.take_update_task());
2505 task.await;
2506
2507 cx.update_buffer(|buffer, cx| {
2508 buffer.update_diagnostics(
2509 LanguageServerId(0),
2510 DiagnosticSet::new(
2511 [DiagnosticEntry {
2512 range: PointUtf16::new(0, 3)..PointUtf16::new(0, 7),
2513 diagnostic: Diagnostic {
2514 severity: lsp::DiagnosticSeverity::ERROR,
2515 group_id: 1,
2516 message: "unused function".into(),
2517 ..Default::default()
2518 },
2519 }],
2520 buffer,
2521 ),
2522 cx,
2523 )
2524 });
2525
2526 cx.run_until_parked();
2527 let chunks = cx.update_editor(|editor, window, cx| {
2528 editor
2529 .snapshot(window, cx)
2530 .display_snapshot
2531 .chunks(
2532 crate::display_map::DisplayRow(0)..crate::display_map::DisplayRow(1),
2533 LanguageAwareStyling {
2534 tree_sitter: false,
2535 diagnostics: true,
2536 },
2537 crate::HighlightStyles::default(),
2538 )
2539 .map(|chunk| {
2540 (
2541 chunk.text.to_string(),
2542 chunk.diagnostic_severity,
2543 chunk.highlight_style,
2544 )
2545 })
2546 .collect::<Vec<_>>()
2547 });
2548
2549 assert_eq!(
2550 extract_semantic_highlights(&cx.editor, &cx),
2551 vec![MultiBufferOffset(3)..MultiBufferOffset(7)]
2552 );
2553
2554 assert!(
2555 chunks.iter().any(
2556 |(text, severity, style): &(
2557 String,
2558 Option<lsp::DiagnosticSeverity>,
2559 Option<gpui::HighlightStyle>
2560 )| {
2561 text == "main"
2562 && *severity == Some(lsp::DiagnosticSeverity::ERROR)
2563 && style.is_some()
2564 }
2565 ),
2566 "expected 'main' chunk to have both diagnostic and semantic styling: {:?}",
2567 chunks
2568 );
2569 }
2570
2571 fn extract_semantic_highlight_styles(
2572 editor: &Entity<Editor>,
2573 cx: &TestAppContext,
2574 ) -> Vec<HighlightStyle> {
2575 editor.read_with(cx, |editor, cx| {
2576 editor
2577 .display_map
2578 .read(cx)
2579 .semantic_token_highlights
2580 .iter()
2581 .flat_map(|(_, (v, interner))| {
2582 v.iter().map(|highlights| interner[highlights.style])
2583 })
2584 .collect()
2585 })
2586 }
2587}
2588