mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-19 10:29:35 +03:00
Merge branch 'main' of github.com:zed-industries/zed into vector_store
This commit is contained in:
commit
1d737e490b
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -4277,6 +4277,7 @@ dependencies = [
|
||||
"async-tar",
|
||||
"futures 0.3.28",
|
||||
"gpui",
|
||||
"log",
|
||||
"parking_lot 0.11.2",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
|
@ -73,6 +73,16 @@
|
||||
// Whether to show git diff indicators in the scrollbar.
|
||||
"git_diff": true
|
||||
},
|
||||
// Inlay hint related settings
|
||||
"inlay_hints": {
|
||||
// Global switch to toggle hints on and off, switched off by default.
|
||||
"enabled": false,
|
||||
// Toggle certain types of hints on and off, all switched on by default.
|
||||
"show_type_hints": true,
|
||||
"show_parameter_hints": true,
|
||||
// Corresponds to null/None LSP hint type value.
|
||||
"show_other_hints": true
|
||||
},
|
||||
"project_panel": {
|
||||
// Whether to show the git status in the project panel.
|
||||
"git_status": true,
|
||||
|
@ -207,16 +207,11 @@ impl ActivityIndicator {
|
||||
let mut checking_for_update = SmallVec::<[_; 3]>::new();
|
||||
let mut failed = SmallVec::<[_; 3]>::new();
|
||||
for status in &self.statuses {
|
||||
let name = status.name.clone();
|
||||
match status.status {
|
||||
LanguageServerBinaryStatus::CheckingForUpdate => {
|
||||
checking_for_update.push(status.name.clone());
|
||||
}
|
||||
LanguageServerBinaryStatus::Downloading => {
|
||||
downloading.push(status.name.clone());
|
||||
}
|
||||
LanguageServerBinaryStatus::Failed { .. } => {
|
||||
failed.push(status.name.clone());
|
||||
}
|
||||
LanguageServerBinaryStatus::CheckingForUpdate => checking_for_update.push(name),
|
||||
LanguageServerBinaryStatus::Downloading => downloading.push(name),
|
||||
LanguageServerBinaryStatus::Failed { .. } => failed.push(name),
|
||||
LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {}
|
||||
}
|
||||
}
|
||||
|
@ -201,6 +201,7 @@ impl Server {
|
||||
.add_message_handler(update_language_server)
|
||||
.add_message_handler(update_diagnostic_summary)
|
||||
.add_message_handler(update_worktree_settings)
|
||||
.add_message_handler(refresh_inlay_hints)
|
||||
.add_request_handler(forward_project_request::<proto::GetHover>)
|
||||
.add_request_handler(forward_project_request::<proto::GetDefinition>)
|
||||
.add_request_handler(forward_project_request::<proto::GetTypeDefinition>)
|
||||
@ -226,6 +227,7 @@ impl Server {
|
||||
.add_request_handler(forward_project_request::<proto::DeleteProjectEntry>)
|
||||
.add_request_handler(forward_project_request::<proto::ExpandProjectEntry>)
|
||||
.add_request_handler(forward_project_request::<proto::OnTypeFormatting>)
|
||||
.add_request_handler(forward_project_request::<proto::InlayHints>)
|
||||
.add_message_handler(create_buffer_for_peer)
|
||||
.add_request_handler(update_buffer)
|
||||
.add_message_handler(update_buffer_file)
|
||||
@ -1574,6 +1576,10 @@ async fn update_worktree_settings(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn refresh_inlay_hints(request: proto::RefreshInlayHints, session: Session) -> Result<()> {
|
||||
broadcast_project_message(request.project_id, request, session).await
|
||||
}
|
||||
|
||||
async fn start_language_server(
|
||||
request: proto::StartLanguageServer,
|
||||
session: Session,
|
||||
@ -1750,7 +1756,15 @@ async fn buffer_reloaded(request: proto::BufferReloaded, session: Session) -> Re
|
||||
}
|
||||
|
||||
async fn buffer_saved(request: proto::BufferSaved, session: Session) -> Result<()> {
|
||||
let project_id = ProjectId::from_proto(request.project_id);
|
||||
broadcast_project_message(request.project_id, request, session).await
|
||||
}
|
||||
|
||||
async fn broadcast_project_message<T: EnvelopedMessage>(
|
||||
project_id: u64,
|
||||
request: T,
|
||||
session: Session,
|
||||
) -> Result<()> {
|
||||
let project_id = ProjectId::from_proto(project_id);
|
||||
let project_connection_ids = session
|
||||
.db()
|
||||
.await
|
||||
|
@ -18,7 +18,7 @@ use gpui::{
|
||||
};
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
language_settings::{AllLanguageSettings, Formatter},
|
||||
language_settings::{AllLanguageSettings, Formatter, InlayHintKind, InlayHintSettings},
|
||||
tree_sitter_rust, Anchor, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
|
||||
LanguageConfig, OffsetRangeExt, Point, Rope,
|
||||
};
|
||||
@ -34,7 +34,7 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
atomic::{AtomicBool, AtomicU32, Ordering::SeqCst},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
@ -7800,6 +7800,572 @@ async fn test_on_input_format_from_guest_to_host(
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_mutual_editor_inlay_hint_cache_update(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
cx_a.update(editor::init);
|
||||
cx_b.update(editor::init);
|
||||
|
||||
cx_a.update(|cx| {
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
|
||||
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
||||
enabled: true,
|
||||
show_type_hints: true,
|
||||
show_parameter_hints: false,
|
||||
show_other_hints: true,
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
cx_b.update(|cx| {
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
|
||||
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
||||
enabled: true,
|
||||
show_type_hints: true,
|
||||
show_parameter_hints: false,
|
||||
show_other_hints: true,
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
|
||||
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_language_servers = language
|
||||
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
let language = Arc::new(language);
|
||||
client_a.language_registry.add(Arc::clone(&language));
|
||||
client_b.language_registry.add(language);
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
"main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
|
||||
"other.rs": "// Test file",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let workspace_a = client_a.build_workspace(&project_a, cx_a);
|
||||
cx_a.foreground().start_waiting();
|
||||
|
||||
let _buffer_a = project_a
|
||||
.update(cx_a, |project, cx| {
|
||||
project.open_local_buffer("/a/main.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
let next_call_id = Arc::new(AtomicU32::new(0));
|
||||
let editor_a = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
fake_language_server
|
||||
.handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
|
||||
let task_next_call_id = Arc::clone(&next_call_id);
|
||||
async move {
|
||||
assert_eq!(
|
||||
params.text_document.uri,
|
||||
lsp::Url::from_file_path("/a/main.rs").unwrap(),
|
||||
);
|
||||
let mut current_call_id = Arc::clone(&task_next_call_id).fetch_add(1, SeqCst);
|
||||
let mut new_hints = Vec::with_capacity(current_call_id as usize);
|
||||
loop {
|
||||
new_hints.push(lsp::InlayHint {
|
||||
position: lsp::Position::new(0, current_call_id),
|
||||
label: lsp::InlayHintLabel::String(current_call_id.to_string()),
|
||||
kind: None,
|
||||
text_edits: None,
|
||||
tooltip: None,
|
||||
padding_left: None,
|
||||
padding_right: None,
|
||||
data: None,
|
||||
});
|
||||
if current_call_id == 0 {
|
||||
break;
|
||||
}
|
||||
current_call_id -= 1;
|
||||
}
|
||||
Ok(Some(new_hints))
|
||||
}
|
||||
})
|
||||
.next()
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
cx_a.foreground().finish_waiting();
|
||||
cx_a.foreground().run_until_parked();
|
||||
|
||||
let mut edits_made = 1;
|
||||
editor_a.update(cx_a, |editor, _| {
|
||||
assert_eq!(
|
||||
vec!["0".to_string()],
|
||||
extract_hint_labels(editor),
|
||||
"Host should get its first hints when opens an editor"
|
||||
);
|
||||
let inlay_cache = editor.inlay_hint_cache();
|
||||
assert_eq!(
|
||||
inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
|
||||
"Cache should use editor settings to get the allowed hint kinds"
|
||||
);
|
||||
assert_eq!(
|
||||
inlay_cache.version, edits_made,
|
||||
"Host editor update the cache version after every cache/view change",
|
||||
);
|
||||
});
|
||||
let workspace_b = client_b.build_workspace(&project_b, cx_b);
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
cx_b.foreground().run_until_parked();
|
||||
editor_b.update(cx_b, |editor, _| {
|
||||
assert_eq!(
|
||||
vec!["0".to_string(), "1".to_string()],
|
||||
extract_hint_labels(editor),
|
||||
"Client should get its first hints when opens an editor"
|
||||
);
|
||||
let inlay_cache = editor.inlay_hint_cache();
|
||||
assert_eq!(
|
||||
inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
|
||||
"Cache should use editor settings to get the allowed hint kinds"
|
||||
);
|
||||
assert_eq!(
|
||||
inlay_cache.version, edits_made,
|
||||
"Guest editor update the cache version after every cache/view change"
|
||||
);
|
||||
});
|
||||
|
||||
editor_b.update(cx_b, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([13..13].clone()));
|
||||
editor.handle_input(":", cx);
|
||||
cx.focus(&editor_b);
|
||||
edits_made += 1;
|
||||
});
|
||||
cx_a.foreground().run_until_parked();
|
||||
cx_b.foreground().run_until_parked();
|
||||
editor_a.update(cx_a, |editor, _| {
|
||||
assert_eq!(
|
||||
vec!["0".to_string(), "1".to_string(), "2".to_string()],
|
||||
extract_hint_labels(editor),
|
||||
"Host should get hints from the 1st edit and 1st LSP query"
|
||||
);
|
||||
let inlay_cache = editor.inlay_hint_cache();
|
||||
assert_eq!(
|
||||
inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
|
||||
"Inlay kinds settings never change during the test"
|
||||
);
|
||||
assert_eq!(inlay_cache.version, edits_made);
|
||||
});
|
||||
editor_b.update(cx_b, |editor, _| {
|
||||
assert_eq!(
|
||||
vec![
|
||||
"0".to_string(),
|
||||
"1".to_string(),
|
||||
"2".to_string(),
|
||||
"3".to_string()
|
||||
],
|
||||
extract_hint_labels(editor),
|
||||
"Guest should get hints the 1st edit and 2nd LSP query"
|
||||
);
|
||||
let inlay_cache = editor.inlay_hint_cache();
|
||||
assert_eq!(
|
||||
inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
|
||||
"Inlay kinds settings never change during the test"
|
||||
);
|
||||
assert_eq!(inlay_cache.version, edits_made);
|
||||
});
|
||||
|
||||
editor_a.update(cx_a, |editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
|
||||
editor.handle_input("a change to increment both buffers' versions", cx);
|
||||
cx.focus(&editor_a);
|
||||
edits_made += 1;
|
||||
});
|
||||
cx_a.foreground().run_until_parked();
|
||||
cx_b.foreground().run_until_parked();
|
||||
editor_a.update(cx_a, |editor, _| {
|
||||
assert_eq!(
|
||||
vec![
|
||||
"0".to_string(),
|
||||
"1".to_string(),
|
||||
"2".to_string(),
|
||||
"3".to_string(),
|
||||
"4".to_string()
|
||||
],
|
||||
extract_hint_labels(editor),
|
||||
"Host should get hints from 3rd edit, 5th LSP query: \
|
||||
4th query was made by guest (but not applied) due to cache invalidation logic"
|
||||
);
|
||||
let inlay_cache = editor.inlay_hint_cache();
|
||||
assert_eq!(
|
||||
inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
|
||||
"Inlay kinds settings never change during the test"
|
||||
);
|
||||
assert_eq!(inlay_cache.version, edits_made);
|
||||
});
|
||||
editor_b.update(cx_b, |editor, _| {
|
||||
assert_eq!(
|
||||
vec![
|
||||
"0".to_string(),
|
||||
"1".to_string(),
|
||||
"2".to_string(),
|
||||
"3".to_string(),
|
||||
"4".to_string(),
|
||||
"5".to_string(),
|
||||
],
|
||||
extract_hint_labels(editor),
|
||||
"Guest should get hints from 3rd edit, 6th LSP query"
|
||||
);
|
||||
let inlay_cache = editor.inlay_hint_cache();
|
||||
assert_eq!(
|
||||
inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
|
||||
"Inlay kinds settings never change during the test"
|
||||
);
|
||||
assert_eq!(inlay_cache.version, edits_made);
|
||||
});
|
||||
|
||||
fake_language_server
|
||||
.request::<lsp::request::InlayHintRefreshRequest>(())
|
||||
.await
|
||||
.expect("inlay refresh request failed");
|
||||
edits_made += 1;
|
||||
cx_a.foreground().run_until_parked();
|
||||
cx_b.foreground().run_until_parked();
|
||||
editor_a.update(cx_a, |editor, _| {
|
||||
assert_eq!(
|
||||
vec![
|
||||
"0".to_string(),
|
||||
"1".to_string(),
|
||||
"2".to_string(),
|
||||
"3".to_string(),
|
||||
"4".to_string(),
|
||||
"5".to_string(),
|
||||
"6".to_string(),
|
||||
],
|
||||
extract_hint_labels(editor),
|
||||
"Host should react to /refresh LSP request and get new hints from 7th LSP query"
|
||||
);
|
||||
let inlay_cache = editor.inlay_hint_cache();
|
||||
assert_eq!(
|
||||
inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
|
||||
"Inlay kinds settings never change during the test"
|
||||
);
|
||||
assert_eq!(
|
||||
inlay_cache.version, edits_made,
|
||||
"Host should accepted all edits and bump its cache version every time"
|
||||
);
|
||||
});
|
||||
editor_b.update(cx_b, |editor, _| {
|
||||
assert_eq!(
|
||||
vec![
|
||||
"0".to_string(),
|
||||
"1".to_string(),
|
||||
"2".to_string(),
|
||||
"3".to_string(),
|
||||
"4".to_string(),
|
||||
"5".to_string(),
|
||||
"6".to_string(),
|
||||
"7".to_string(),
|
||||
],
|
||||
extract_hint_labels(editor),
|
||||
"Guest should get a /refresh LSP request propagated by host and get new hints from 8th LSP query"
|
||||
);
|
||||
let inlay_cache = editor.inlay_hint_cache();
|
||||
assert_eq!(
|
||||
inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
|
||||
"Inlay kinds settings never change during the test"
|
||||
);
|
||||
assert_eq!(
|
||||
inlay_cache.version,
|
||||
edits_made,
|
||||
"Guest should accepted all edits and bump its cache version every time"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_inlay_hint_refresh_is_forwarded(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx_a: &mut TestAppContext,
|
||||
cx_b: &mut TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
let mut server = TestServer::start(&deterministic).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
server
|
||||
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
|
||||
.await;
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
cx_a.update(editor::init);
|
||||
cx_b.update(editor::init);
|
||||
|
||||
cx_a.update(|cx| {
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
|
||||
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
||||
enabled: false,
|
||||
show_type_hints: true,
|
||||
show_parameter_hints: false,
|
||||
show_other_hints: true,
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
cx_b.update(|cx| {
|
||||
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
|
||||
settings.defaults.inlay_hints = Some(InlayHintSettings {
|
||||
enabled: true,
|
||||
show_type_hints: true,
|
||||
show_parameter_hints: false,
|
||||
show_other_hints: true,
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
|
||||
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let mut fake_language_servers = language
|
||||
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
inlay_hint_provider: Some(lsp::OneOf::Left(true)),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}))
|
||||
.await;
|
||||
let language = Arc::new(language);
|
||||
client_a.language_registry.add(Arc::clone(&language));
|
||||
client_b.language_registry.add(language);
|
||||
|
||||
client_a
|
||||
.fs
|
||||
.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
"main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
|
||||
"other.rs": "// Test file",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
|
||||
active_call_a
|
||||
.update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
let project_id = active_call_a
|
||||
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let project_b = client_b.build_remote_project(project_id, cx_b).await;
|
||||
active_call_b
|
||||
.update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let workspace_a = client_a.build_workspace(&project_a, cx_a);
|
||||
let workspace_b = client_b.build_workspace(&project_b, cx_b);
|
||||
cx_a.foreground().start_waiting();
|
||||
cx_b.foreground().start_waiting();
|
||||
|
||||
let editor_a = workspace_a
|
||||
.update(cx_a, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
let editor_b = workspace_b
|
||||
.update(cx_b, |workspace, cx| {
|
||||
workspace.open_path((worktree_id, "main.rs"), None, true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
|
||||
let fake_language_server = fake_language_servers.next().await.unwrap();
|
||||
let next_call_id = Arc::new(AtomicU32::new(0));
|
||||
fake_language_server
|
||||
.handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
|
||||
let task_next_call_id = Arc::clone(&next_call_id);
|
||||
async move {
|
||||
assert_eq!(
|
||||
params.text_document.uri,
|
||||
lsp::Url::from_file_path("/a/main.rs").unwrap(),
|
||||
);
|
||||
let mut current_call_id = Arc::clone(&task_next_call_id).fetch_add(1, SeqCst);
|
||||
let mut new_hints = Vec::with_capacity(current_call_id as usize);
|
||||
loop {
|
||||
new_hints.push(lsp::InlayHint {
|
||||
position: lsp::Position::new(0, current_call_id),
|
||||
label: lsp::InlayHintLabel::String(current_call_id.to_string()),
|
||||
kind: None,
|
||||
text_edits: None,
|
||||
tooltip: None,
|
||||
padding_left: None,
|
||||
padding_right: None,
|
||||
data: None,
|
||||
});
|
||||
if current_call_id == 0 {
|
||||
break;
|
||||
}
|
||||
current_call_id -= 1;
|
||||
}
|
||||
Ok(Some(new_hints))
|
||||
}
|
||||
})
|
||||
.next()
|
||||
.await
|
||||
.unwrap();
|
||||
cx_a.foreground().finish_waiting();
|
||||
cx_b.foreground().finish_waiting();
|
||||
|
||||
cx_a.foreground().run_until_parked();
|
||||
editor_a.update(cx_a, |editor, _| {
|
||||
assert!(
|
||||
extract_hint_labels(editor).is_empty(),
|
||||
"Host should get no hints due to them turned off"
|
||||
);
|
||||
let inlay_cache = editor.inlay_hint_cache();
|
||||
assert_eq!(
|
||||
inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
|
||||
"Host should have allowed hint kinds set despite hints are off"
|
||||
);
|
||||
assert_eq!(
|
||||
inlay_cache.version, 0,
|
||||
"Host should not increment its cache version due to no changes",
|
||||
);
|
||||
});
|
||||
|
||||
let mut edits_made = 1;
|
||||
cx_b.foreground().run_until_parked();
|
||||
editor_b.update(cx_b, |editor, _| {
|
||||
assert_eq!(
|
||||
vec!["0".to_string()],
|
||||
extract_hint_labels(editor),
|
||||
"Client should get its first hints when opens an editor"
|
||||
);
|
||||
let inlay_cache = editor.inlay_hint_cache();
|
||||
assert_eq!(
|
||||
inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
|
||||
"Cache should use editor settings to get the allowed hint kinds"
|
||||
);
|
||||
assert_eq!(
|
||||
inlay_cache.version, edits_made,
|
||||
"Guest editor update the cache version after every cache/view change"
|
||||
);
|
||||
});
|
||||
|
||||
fake_language_server
|
||||
.request::<lsp::request::InlayHintRefreshRequest>(())
|
||||
.await
|
||||
.expect("inlay refresh request failed");
|
||||
cx_a.foreground().run_until_parked();
|
||||
editor_a.update(cx_a, |editor, _| {
|
||||
assert!(
|
||||
extract_hint_labels(editor).is_empty(),
|
||||
"Host should get nop hints due to them turned off, even after the /refresh"
|
||||
);
|
||||
let inlay_cache = editor.inlay_hint_cache();
|
||||
assert_eq!(inlay_cache.allowed_hint_kinds, allowed_hint_kinds);
|
||||
assert_eq!(
|
||||
inlay_cache.version, 0,
|
||||
"Host should not increment its cache version due to no changes",
|
||||
);
|
||||
});
|
||||
|
||||
edits_made += 1;
|
||||
cx_b.foreground().run_until_parked();
|
||||
editor_b.update(cx_b, |editor, _| {
|
||||
assert_eq!(
|
||||
vec!["0".to_string(), "1".to_string(),],
|
||||
extract_hint_labels(editor),
|
||||
"Guest should get a /refresh LSP request propagated by host despite host hints are off"
|
||||
);
|
||||
let inlay_cache = editor.inlay_hint_cache();
|
||||
assert_eq!(
|
||||
inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
|
||||
"Inlay kinds settings never change during the test"
|
||||
);
|
||||
assert_eq!(
|
||||
inlay_cache.version, edits_made,
|
||||
"Guest should accepted all edits and bump its cache version every time"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
struct RoomParticipants {
|
||||
remote: Vec<String>,
|
||||
@ -7823,3 +8389,17 @@ fn room_participants(room: &ModelHandle<Room>, cx: &mut TestAppContext) -> RoomP
|
||||
RoomParticipants { remote, pending }
|
||||
})
|
||||
}
|
||||
|
||||
fn extract_hint_labels(editor: &Editor) -> Vec<String> {
|
||||
let mut labels = Vec::new();
|
||||
for (_, excerpt_hints) in &editor.inlay_hint_cache().hints {
|
||||
let excerpt_hints = excerpt_hints.read();
|
||||
for (_, inlay) in excerpt_hints.hints.iter() {
|
||||
match &inlay.label {
|
||||
project::InlayHintLabel::String(s) => labels.push(s.to_string()),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
labels
|
||||
}
|
||||
|
@ -317,7 +317,7 @@ impl CollabTitlebarItem {
|
||||
),
|
||||
]
|
||||
};
|
||||
user_menu.show(Default::default(), AnchorCorner::TopRight, items, cx);
|
||||
user_menu.toggle(Default::default(), AnchorCorner::TopRight, items, cx);
|
||||
});
|
||||
}
|
||||
|
||||
@ -683,6 +683,9 @@ impl CollabTitlebarItem {
|
||||
.into_any()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_down(MouseButton::Left, move |_, this, cx| {
|
||||
this.user_menu.update(cx, |menu, _| menu.delay_cancel());
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.toggle_user_menu(&Default::default(), cx)
|
||||
})
|
||||
|
@ -124,6 +124,7 @@ pub struct ContextMenu {
|
||||
items: Vec<ContextMenuItem>,
|
||||
selected_index: Option<usize>,
|
||||
visible: bool,
|
||||
delay_cancel: bool,
|
||||
previously_focused_view_id: Option<usize>,
|
||||
parent_view_id: usize,
|
||||
_actions_observation: Subscription,
|
||||
@ -178,6 +179,7 @@ impl ContextMenu {
|
||||
pub fn new(parent_view_id: usize, cx: &mut ViewContext<Self>) -> Self {
|
||||
Self {
|
||||
show_count: 0,
|
||||
delay_cancel: false,
|
||||
anchor_position: Default::default(),
|
||||
anchor_corner: AnchorCorner::TopLeft,
|
||||
position_mode: OverlayPositionMode::Window,
|
||||
@ -232,15 +234,23 @@ impl ContextMenu {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delay_cancel(&mut self) {
|
||||
self.delay_cancel = true;
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
||||
self.reset(cx);
|
||||
let show_count = self.show_count;
|
||||
cx.defer(move |this, cx| {
|
||||
if cx.handle().is_focused(cx) && this.show_count == show_count {
|
||||
let window_id = cx.window_id();
|
||||
(**cx).focus(window_id, this.previously_focused_view_id.take());
|
||||
}
|
||||
});
|
||||
if !self.delay_cancel {
|
||||
self.reset(cx);
|
||||
let show_count = self.show_count;
|
||||
cx.defer(move |this, cx| {
|
||||
if cx.handle().is_focused(cx) && this.show_count == show_count {
|
||||
let window_id = cx.window_id();
|
||||
(**cx).focus(window_id, this.previously_focused_view_id.take());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
self.delay_cancel = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self, cx: &mut ViewContext<Self>) {
|
||||
@ -293,6 +303,34 @@ impl ContextMenu {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle(
|
||||
&mut self,
|
||||
anchor_position: Vector2F,
|
||||
anchor_corner: AnchorCorner,
|
||||
items: Vec<ContextMenuItem>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if self.visible() {
|
||||
self.cancel(&Cancel, cx);
|
||||
} else {
|
||||
let mut items = items.into_iter().peekable();
|
||||
if items.peek().is_some() {
|
||||
self.items = items.collect();
|
||||
self.anchor_position = anchor_position;
|
||||
self.anchor_corner = anchor_corner;
|
||||
self.visible = true;
|
||||
self.show_count += 1;
|
||||
if !cx.is_self_focused() {
|
||||
self.previously_focused_view_id = cx.focused_view_id();
|
||||
}
|
||||
cx.focus_self();
|
||||
} else {
|
||||
self.visible = false;
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn show(
|
||||
&mut self,
|
||||
anchor_position: Vector2F,
|
||||
|
@ -15,7 +15,7 @@ use language::{
|
||||
ToPointUtf16,
|
||||
};
|
||||
use log::{debug, error};
|
||||
use lsp::{LanguageServer, LanguageServerId};
|
||||
use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId};
|
||||
use node_runtime::NodeRuntime;
|
||||
use request::{LogMessage, StatusNotification};
|
||||
use settings::SettingsStore;
|
||||
@ -340,7 +340,7 @@ impl Copilot {
|
||||
let http = util::http::FakeHttpClient::create(|_| async { unreachable!() });
|
||||
let this = cx.add_model(|cx| Self {
|
||||
http: http.clone(),
|
||||
node_runtime: NodeRuntime::new(http, cx.background().clone()),
|
||||
node_runtime: NodeRuntime::instance(http, cx.background().clone()),
|
||||
server: CopilotServer::Running(RunningCopilotServer {
|
||||
lsp: Arc::new(server),
|
||||
sign_in_status: SignInStatus::Authorized,
|
||||
@ -361,11 +361,14 @@ impl Copilot {
|
||||
let start_language_server = async {
|
||||
let server_path = get_copilot_lsp(http).await?;
|
||||
let node_path = node_runtime.binary_path().await?;
|
||||
let arguments: &[OsString] = &[server_path.into(), "--stdio".into()];
|
||||
let arguments: Vec<OsString> = vec![server_path.into(), "--stdio".into()];
|
||||
let binary = LanguageServerBinary {
|
||||
path: node_path,
|
||||
arguments,
|
||||
};
|
||||
let server = LanguageServer::new(
|
||||
LanguageServerId(0),
|
||||
&node_path,
|
||||
arguments,
|
||||
binary,
|
||||
Path::new("/"),
|
||||
None,
|
||||
cx.clone(),
|
||||
|
@ -102,6 +102,9 @@ impl View for CopilotButton {
|
||||
}
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_down(MouseButton::Left, |_, this, cx| {
|
||||
this.popup_menu.update(cx, |menu, _| menu.delay_cancel());
|
||||
})
|
||||
.on_click(MouseButton::Left, {
|
||||
let status = status.clone();
|
||||
move |_, this, cx| match status {
|
||||
@ -186,7 +189,7 @@ impl CopilotButton {
|
||||
}));
|
||||
|
||||
self.popup_menu.update(cx, |menu, cx| {
|
||||
menu.show(
|
||||
menu.toggle(
|
||||
Default::default(),
|
||||
AnchorCorner::BottomRight,
|
||||
menu_options,
|
||||
@ -266,7 +269,7 @@ impl CopilotButton {
|
||||
menu_options.push(ContextMenuItem::action("Sign Out", SignOut));
|
||||
|
||||
self.popup_menu.update(cx, |menu, cx| {
|
||||
menu.show(
|
||||
menu.toggle(
|
||||
Default::default(),
|
||||
AnchorCorner::BottomRight,
|
||||
menu_options,
|
||||
|
@ -1,26 +1,26 @@
|
||||
mod block_map;
|
||||
mod fold_map;
|
||||
mod suggestion_map;
|
||||
mod inlay_map;
|
||||
mod tab_map;
|
||||
mod wrap_map;
|
||||
|
||||
use crate::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
|
||||
use crate::{Anchor, AnchorRangeExt, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
|
||||
pub use block_map::{BlockMap, BlockPoint};
|
||||
use collections::{HashMap, HashSet};
|
||||
use fold_map::{FoldMap, FoldOffset};
|
||||
use fold_map::FoldMap;
|
||||
use gpui::{
|
||||
color::Color,
|
||||
fonts::{FontId, HighlightStyle},
|
||||
Entity, ModelContext, ModelHandle,
|
||||
};
|
||||
use inlay_map::InlayMap;
|
||||
use language::{
|
||||
language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription,
|
||||
};
|
||||
use std::{any::TypeId, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
|
||||
pub use suggestion_map::Suggestion;
|
||||
use suggestion_map::SuggestionMap;
|
||||
use sum_tree::{Bias, TreeMap};
|
||||
use tab_map::TabMap;
|
||||
use text::Rope;
|
||||
use wrap_map::WrapMap;
|
||||
|
||||
pub use block_map::{
|
||||
@ -28,6 +28,8 @@ pub use block_map::{
|
||||
BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
|
||||
};
|
||||
|
||||
pub use self::inlay_map::{Inlay, InlayProperties};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum FoldStatus {
|
||||
Folded,
|
||||
@ -44,7 +46,7 @@ pub struct DisplayMap {
|
||||
buffer: ModelHandle<MultiBuffer>,
|
||||
buffer_subscription: BufferSubscription,
|
||||
fold_map: FoldMap,
|
||||
suggestion_map: SuggestionMap,
|
||||
inlay_map: InlayMap,
|
||||
tab_map: TabMap,
|
||||
wrap_map: ModelHandle<WrapMap>,
|
||||
block_map: BlockMap,
|
||||
@ -69,8 +71,8 @@ impl DisplayMap {
|
||||
let buffer_subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
|
||||
|
||||
let tab_size = Self::tab_size(&buffer, cx);
|
||||
let (fold_map, snapshot) = FoldMap::new(buffer.read(cx).snapshot(cx));
|
||||
let (suggestion_map, snapshot) = SuggestionMap::new(snapshot);
|
||||
let (inlay_map, snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx));
|
||||
let (fold_map, snapshot) = FoldMap::new(snapshot);
|
||||
let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
|
||||
let (wrap_map, snapshot) = WrapMap::new(snapshot, font_id, font_size, wrap_width, cx);
|
||||
let block_map = BlockMap::new(snapshot, buffer_header_height, excerpt_header_height);
|
||||
@ -79,7 +81,7 @@ impl DisplayMap {
|
||||
buffer,
|
||||
buffer_subscription,
|
||||
fold_map,
|
||||
suggestion_map,
|
||||
inlay_map,
|
||||
tab_map,
|
||||
wrap_map,
|
||||
block_map,
|
||||
@ -88,16 +90,13 @@ impl DisplayMap {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn snapshot(&self, cx: &mut ModelContext<Self>) -> DisplaySnapshot {
|
||||
pub fn snapshot(&mut self, cx: &mut ModelContext<Self>) -> DisplaySnapshot {
|
||||
let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let edits = self.buffer_subscription.consume().into_inner();
|
||||
let (fold_snapshot, edits) = self.fold_map.read(buffer_snapshot, edits);
|
||||
let (suggestion_snapshot, edits) = self.suggestion_map.sync(fold_snapshot.clone(), edits);
|
||||
|
||||
let (inlay_snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
|
||||
let (fold_snapshot, edits) = self.fold_map.read(inlay_snapshot.clone(), edits);
|
||||
let tab_size = Self::tab_size(&self.buffer, cx);
|
||||
let (tab_snapshot, edits) = self
|
||||
.tab_map
|
||||
.sync(suggestion_snapshot.clone(), edits, tab_size);
|
||||
let (tab_snapshot, edits) = self.tab_map.sync(fold_snapshot.clone(), edits, tab_size);
|
||||
let (wrap_snapshot, edits) = self
|
||||
.wrap_map
|
||||
.update(cx, |map, cx| map.sync(tab_snapshot.clone(), edits, cx));
|
||||
@ -106,7 +105,7 @@ impl DisplayMap {
|
||||
DisplaySnapshot {
|
||||
buffer_snapshot: self.buffer.read(cx).snapshot(cx),
|
||||
fold_snapshot,
|
||||
suggestion_snapshot,
|
||||
inlay_snapshot,
|
||||
tab_snapshot,
|
||||
wrap_snapshot,
|
||||
block_snapshot,
|
||||
@ -132,15 +131,14 @@ impl DisplayMap {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let edits = self.buffer_subscription.consume().into_inner();
|
||||
let tab_size = Self::tab_size(&self.buffer, cx);
|
||||
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
|
||||
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
|
||||
let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
|
||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self
|
||||
.wrap_map
|
||||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||||
self.block_map.read(snapshot, edits);
|
||||
let (snapshot, edits) = fold_map.fold(ranges);
|
||||
let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
|
||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self
|
||||
.wrap_map
|
||||
@ -157,15 +155,14 @@ impl DisplayMap {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let edits = self.buffer_subscription.consume().into_inner();
|
||||
let tab_size = Self::tab_size(&self.buffer, cx);
|
||||
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
|
||||
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
|
||||
let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
|
||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self
|
||||
.wrap_map
|
||||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||||
self.block_map.read(snapshot, edits);
|
||||
let (snapshot, edits) = fold_map.unfold(ranges, inclusive);
|
||||
let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
|
||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self
|
||||
.wrap_map
|
||||
@ -181,8 +178,8 @@ impl DisplayMap {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let edits = self.buffer_subscription.consume().into_inner();
|
||||
let tab_size = Self::tab_size(&self.buffer, cx);
|
||||
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
|
||||
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
|
||||
let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
|
||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self
|
||||
.wrap_map
|
||||
@ -199,8 +196,8 @@ impl DisplayMap {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let edits = self.buffer_subscription.consume().into_inner();
|
||||
let tab_size = Self::tab_size(&self.buffer, cx);
|
||||
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
|
||||
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
|
||||
let (snapshot, edits) = self.suggestion_map.sync(snapshot, edits);
|
||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self
|
||||
.wrap_map
|
||||
@ -231,32 +228,6 @@ impl DisplayMap {
|
||||
self.text_highlights.remove(&Some(type_id))
|
||||
}
|
||||
|
||||
pub fn has_suggestion(&self) -> bool {
|
||||
self.suggestion_map.has_suggestion()
|
||||
}
|
||||
|
||||
pub fn replace_suggestion<T>(
|
||||
&self,
|
||||
new_suggestion: Option<Suggestion<T>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<Suggestion<FoldOffset>>
|
||||
where
|
||||
T: ToPoint,
|
||||
{
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let edits = self.buffer_subscription.consume().into_inner();
|
||||
let tab_size = Self::tab_size(&self.buffer, cx);
|
||||
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
|
||||
let (snapshot, edits, old_suggestion) =
|
||||
self.suggestion_map.replace(new_suggestion, snapshot, edits);
|
||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self
|
||||
.wrap_map
|
||||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||||
self.block_map.read(snapshot, edits);
|
||||
old_suggestion
|
||||
}
|
||||
|
||||
pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) -> bool {
|
||||
self.wrap_map
|
||||
.update(cx, |map, cx| map.set_font(font_id, font_size, cx))
|
||||
@ -271,6 +242,39 @@ impl DisplayMap {
|
||||
.update(cx, |map, cx| map.set_wrap_width(width, cx))
|
||||
}
|
||||
|
||||
pub fn current_inlays(&self) -> impl Iterator<Item = &Inlay> {
|
||||
self.inlay_map.current_inlays()
|
||||
}
|
||||
|
||||
pub fn splice_inlays<T: Into<Rope>>(
|
||||
&mut self,
|
||||
to_remove: Vec<InlayId>,
|
||||
to_insert: Vec<(InlayId, InlayProperties<T>)>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if to_remove.is_empty() && to_insert.is_empty() {
|
||||
return;
|
||||
}
|
||||
let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let edits = self.buffer_subscription.consume().into_inner();
|
||||
let (snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
|
||||
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
|
||||
let tab_size = Self::tab_size(&self.buffer, cx);
|
||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self
|
||||
.wrap_map
|
||||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||||
self.block_map.read(snapshot, edits);
|
||||
|
||||
let (snapshot, edits) = self.inlay_map.splice(to_remove, to_insert);
|
||||
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
|
||||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||||
let (snapshot, edits) = self
|
||||
.wrap_map
|
||||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||||
self.block_map.read(snapshot, edits);
|
||||
}
|
||||
|
||||
fn tab_size(buffer: &ModelHandle<MultiBuffer>, cx: &mut ModelContext<Self>) -> NonZeroU32 {
|
||||
let language = buffer
|
||||
.read(cx)
|
||||
@ -288,7 +292,7 @@ impl DisplayMap {
|
||||
pub struct DisplaySnapshot {
|
||||
pub buffer_snapshot: MultiBufferSnapshot,
|
||||
fold_snapshot: fold_map::FoldSnapshot,
|
||||
suggestion_snapshot: suggestion_map::SuggestionSnapshot,
|
||||
inlay_snapshot: inlay_map::InlaySnapshot,
|
||||
tab_snapshot: tab_map::TabSnapshot,
|
||||
wrap_snapshot: wrap_map::WrapSnapshot,
|
||||
block_snapshot: block_map::BlockSnapshot,
|
||||
@ -316,9 +320,11 @@ impl DisplaySnapshot {
|
||||
|
||||
pub fn prev_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) {
|
||||
loop {
|
||||
let mut fold_point = self.fold_snapshot.to_fold_point(point, Bias::Left);
|
||||
*fold_point.column_mut() = 0;
|
||||
point = fold_point.to_buffer_point(&self.fold_snapshot);
|
||||
let mut inlay_point = self.inlay_snapshot.to_inlay_point(point);
|
||||
let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Left);
|
||||
fold_point.0.column = 0;
|
||||
inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
|
||||
point = self.inlay_snapshot.to_buffer_point(inlay_point);
|
||||
|
||||
let mut display_point = self.point_to_display_point(point, Bias::Left);
|
||||
*display_point.column_mut() = 0;
|
||||
@ -332,9 +338,11 @@ impl DisplaySnapshot {
|
||||
|
||||
pub fn next_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) {
|
||||
loop {
|
||||
let mut fold_point = self.fold_snapshot.to_fold_point(point, Bias::Right);
|
||||
*fold_point.column_mut() = self.fold_snapshot.line_len(fold_point.row());
|
||||
point = fold_point.to_buffer_point(&self.fold_snapshot);
|
||||
let mut inlay_point = self.inlay_snapshot.to_inlay_point(point);
|
||||
let mut fold_point = self.fold_snapshot.to_fold_point(inlay_point, Bias::Right);
|
||||
fold_point.0.column = self.fold_snapshot.line_len(fold_point.row());
|
||||
inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
|
||||
point = self.inlay_snapshot.to_buffer_point(inlay_point);
|
||||
|
||||
let mut display_point = self.point_to_display_point(point, Bias::Right);
|
||||
*display_point.column_mut() = self.line_len(display_point.row());
|
||||
@ -364,9 +372,9 @@ impl DisplaySnapshot {
|
||||
}
|
||||
|
||||
fn point_to_display_point(&self, point: Point, bias: Bias) -> DisplayPoint {
|
||||
let fold_point = self.fold_snapshot.to_fold_point(point, bias);
|
||||
let suggestion_point = self.suggestion_snapshot.to_suggestion_point(fold_point);
|
||||
let tab_point = self.tab_snapshot.to_tab_point(suggestion_point);
|
||||
let inlay_point = self.inlay_snapshot.to_inlay_point(point);
|
||||
let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
|
||||
let tab_point = self.tab_snapshot.to_tab_point(fold_point);
|
||||
let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point);
|
||||
let block_point = self.block_snapshot.to_block_point(wrap_point);
|
||||
DisplayPoint(block_point)
|
||||
@ -376,9 +384,9 @@ impl DisplaySnapshot {
|
||||
let block_point = point.0;
|
||||
let wrap_point = self.block_snapshot.to_wrap_point(block_point);
|
||||
let tab_point = self.wrap_snapshot.to_tab_point(wrap_point);
|
||||
let suggestion_point = self.tab_snapshot.to_suggestion_point(tab_point, bias).0;
|
||||
let fold_point = self.suggestion_snapshot.to_fold_point(suggestion_point);
|
||||
fold_point.to_buffer_point(&self.fold_snapshot)
|
||||
let fold_point = self.tab_snapshot.to_fold_point(tab_point, bias).0;
|
||||
let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
|
||||
self.inlay_snapshot.to_buffer_point(inlay_point)
|
||||
}
|
||||
|
||||
pub fn max_point(&self) -> DisplayPoint {
|
||||
@ -388,7 +396,13 @@ impl DisplaySnapshot {
|
||||
/// Returns text chunks starting at the given display row until the end of the file
|
||||
pub fn text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
|
||||
self.block_snapshot
|
||||
.chunks(display_row..self.max_point().row() + 1, false, None, None)
|
||||
.chunks(
|
||||
display_row..self.max_point().row() + 1,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.map(|h| h.text)
|
||||
}
|
||||
|
||||
@ -396,7 +410,7 @@ impl DisplaySnapshot {
|
||||
pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
|
||||
(0..=display_row).into_iter().rev().flat_map(|row| {
|
||||
self.block_snapshot
|
||||
.chunks(row..row + 1, false, None, None)
|
||||
.chunks(row..row + 1, false, None, None, None)
|
||||
.map(|h| h.text)
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
@ -408,13 +422,15 @@ impl DisplaySnapshot {
|
||||
&self,
|
||||
display_rows: Range<u32>,
|
||||
language_aware: bool,
|
||||
suggestion_highlight: Option<HighlightStyle>,
|
||||
hint_highlights: Option<HighlightStyle>,
|
||||
suggestion_highlights: Option<HighlightStyle>,
|
||||
) -> DisplayChunks<'_> {
|
||||
self.block_snapshot.chunks(
|
||||
display_rows,
|
||||
language_aware,
|
||||
Some(&self.text_highlights),
|
||||
suggestion_highlight,
|
||||
hint_highlights,
|
||||
suggestion_highlights,
|
||||
)
|
||||
}
|
||||
|
||||
@ -790,9 +806,10 @@ impl DisplayPoint {
|
||||
pub fn to_offset(self, map: &DisplaySnapshot, bias: Bias) -> usize {
|
||||
let wrap_point = map.block_snapshot.to_wrap_point(self.0);
|
||||
let tab_point = map.wrap_snapshot.to_tab_point(wrap_point);
|
||||
let suggestion_point = map.tab_snapshot.to_suggestion_point(tab_point, bias).0;
|
||||
let fold_point = map.suggestion_snapshot.to_fold_point(suggestion_point);
|
||||
fold_point.to_buffer_offset(&map.fold_snapshot)
|
||||
let fold_point = map.tab_snapshot.to_fold_point(tab_point, bias).0;
|
||||
let inlay_point = fold_point.to_inlay_point(&map.fold_snapshot);
|
||||
map.inlay_snapshot
|
||||
.to_buffer_offset(map.inlay_snapshot.to_offset(inlay_point))
|
||||
}
|
||||
}
|
||||
|
||||
@ -1706,7 +1723,7 @@ pub mod tests {
|
||||
) -> Vec<(String, Option<Color>, Option<Color>)> {
|
||||
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
|
||||
let mut chunks: Vec<(String, Option<Color>, Option<Color>)> = Vec::new();
|
||||
for chunk in snapshot.chunks(rows, true, None) {
|
||||
for chunk in snapshot.chunks(rows, true, None, None) {
|
||||
let syntax_color = chunk
|
||||
.syntax_highlight_id
|
||||
.and_then(|id| id.style(theme)?.color);
|
||||
|
@ -573,9 +573,15 @@ impl<'a> BlockMapWriter<'a> {
|
||||
impl BlockSnapshot {
|
||||
#[cfg(test)]
|
||||
pub fn text(&self) -> String {
|
||||
self.chunks(0..self.transforms.summary().output_rows, false, None, None)
|
||||
.map(|chunk| chunk.text)
|
||||
.collect()
|
||||
self.chunks(
|
||||
0..self.transforms.summary().output_rows,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.map(|chunk| chunk.text)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn chunks<'a>(
|
||||
@ -583,7 +589,8 @@ impl BlockSnapshot {
|
||||
rows: Range<u32>,
|
||||
language_aware: bool,
|
||||
text_highlights: Option<&'a TextHighlights>,
|
||||
suggestion_highlight: Option<HighlightStyle>,
|
||||
hint_highlights: Option<HighlightStyle>,
|
||||
suggestion_highlights: Option<HighlightStyle>,
|
||||
) -> BlockChunks<'a> {
|
||||
let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows);
|
||||
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
|
||||
@ -616,7 +623,8 @@ impl BlockSnapshot {
|
||||
input_start..input_end,
|
||||
language_aware,
|
||||
text_highlights,
|
||||
suggestion_highlight,
|
||||
hint_highlights,
|
||||
suggestion_highlights,
|
||||
),
|
||||
input_chunk: Default::default(),
|
||||
transforms: cursor,
|
||||
@ -989,7 +997,7 @@ fn offset_for_row(s: &str, target: u32) -> (u32, usize) {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::display_map::suggestion_map::SuggestionMap;
|
||||
use crate::display_map::inlay_map::InlayMap;
|
||||
use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap};
|
||||
use crate::multi_buffer::MultiBuffer;
|
||||
use gpui::{elements::Empty, Element};
|
||||
@ -1030,9 +1038,9 @@ mod tests {
|
||||
let buffer = MultiBuffer::build_simple(text, cx);
|
||||
let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||
let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
|
||||
let (fold_map, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
|
||||
let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
|
||||
let (tab_map, tab_snapshot) = TabMap::new(suggestion_snapshot, 1.try_into().unwrap());
|
||||
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
|
||||
let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
|
||||
let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap());
|
||||
let (wrap_map, wraps_snapshot) = WrapMap::new(tab_snapshot, font_id, 14.0, None, cx);
|
||||
let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
|
||||
|
||||
@ -1175,12 +1183,11 @@ mod tests {
|
||||
buffer.snapshot(cx)
|
||||
});
|
||||
|
||||
let (fold_snapshot, fold_edits) =
|
||||
fold_map.read(buffer_snapshot, subscription.consume().into_inner());
|
||||
let (suggestion_snapshot, suggestion_edits) =
|
||||
suggestion_map.sync(fold_snapshot, fold_edits);
|
||||
let (inlay_snapshot, inlay_edits) =
|
||||
inlay_map.sync(buffer_snapshot, subscription.consume().into_inner());
|
||||
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
|
||||
let (tab_snapshot, tab_edits) =
|
||||
tab_map.sync(suggestion_snapshot, suggestion_edits, 4.try_into().unwrap());
|
||||
tab_map.sync(fold_snapshot, fold_edits, 4.try_into().unwrap());
|
||||
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
|
||||
wrap_map.sync(tab_snapshot, tab_edits, cx)
|
||||
});
|
||||
@ -1205,9 +1212,9 @@ mod tests {
|
||||
|
||||
let buffer = MultiBuffer::build_simple(text, cx);
|
||||
let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||
let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
|
||||
let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
|
||||
let (_, tab_snapshot) = TabMap::new(suggestion_snapshot, 1.try_into().unwrap());
|
||||
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
|
||||
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
|
||||
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
|
||||
let (_, wraps_snapshot) = WrapMap::new(tab_snapshot, font_id, 14.0, Some(60.), cx);
|
||||
let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
|
||||
|
||||
@ -1277,9 +1284,9 @@ mod tests {
|
||||
};
|
||||
|
||||
let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||
let (fold_map, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
|
||||
let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
|
||||
let (tab_map, tab_snapshot) = TabMap::new(suggestion_snapshot, tab_size);
|
||||
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
|
||||
let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
|
||||
let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
|
||||
let (wrap_map, wraps_snapshot) =
|
||||
WrapMap::new(tab_snapshot, font_id, font_size, wrap_width, cx);
|
||||
let mut block_map = BlockMap::new(
|
||||
@ -1332,12 +1339,11 @@ mod tests {
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let (fold_snapshot, fold_edits) =
|
||||
fold_map.read(buffer_snapshot.clone(), vec![]);
|
||||
let (suggestion_snapshot, suggestion_edits) =
|
||||
suggestion_map.sync(fold_snapshot, fold_edits);
|
||||
let (inlay_snapshot, inlay_edits) =
|
||||
inlay_map.sync(buffer_snapshot.clone(), vec![]);
|
||||
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
|
||||
let (tab_snapshot, tab_edits) =
|
||||
tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
|
||||
tab_map.sync(fold_snapshot, fold_edits, tab_size);
|
||||
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
|
||||
wrap_map.sync(tab_snapshot, tab_edits, cx)
|
||||
});
|
||||
@ -1357,12 +1363,11 @@ mod tests {
|
||||
})
|
||||
.collect();
|
||||
|
||||
let (fold_snapshot, fold_edits) =
|
||||
fold_map.read(buffer_snapshot.clone(), vec![]);
|
||||
let (suggestion_snapshot, suggestion_edits) =
|
||||
suggestion_map.sync(fold_snapshot, fold_edits);
|
||||
let (inlay_snapshot, inlay_edits) =
|
||||
inlay_map.sync(buffer_snapshot.clone(), vec![]);
|
||||
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
|
||||
let (tab_snapshot, tab_edits) =
|
||||
tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
|
||||
tab_map.sync(fold_snapshot, fold_edits, tab_size);
|
||||
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
|
||||
wrap_map.sync(tab_snapshot, tab_edits, cx)
|
||||
});
|
||||
@ -1381,11 +1386,10 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
let (fold_snapshot, fold_edits) = fold_map.read(buffer_snapshot.clone(), buffer_edits);
|
||||
let (suggestion_snapshot, suggestion_edits) =
|
||||
suggestion_map.sync(fold_snapshot, fold_edits);
|
||||
let (tab_snapshot, tab_edits) =
|
||||
tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
|
||||
let (inlay_snapshot, inlay_edits) =
|
||||
inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
|
||||
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
|
||||
let (tab_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
|
||||
let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
|
||||
wrap_map.sync(tab_snapshot, tab_edits, cx)
|
||||
});
|
||||
@ -1499,6 +1503,7 @@ mod tests {
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.map(|chunk| chunk.text)
|
||||
.collect::<String>();
|
||||
|
File diff suppressed because it is too large
Load Diff
1697
crates/editor/src/display_map/inlay_map.rs
Normal file
1697
crates/editor/src/display_map/inlay_map.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,871 +0,0 @@
|
||||
use super::{
|
||||
fold_map::{FoldBufferRows, FoldChunks, FoldEdit, FoldOffset, FoldPoint, FoldSnapshot},
|
||||
TextHighlights,
|
||||
};
|
||||
use crate::{MultiBufferSnapshot, ToPoint};
|
||||
use gpui::fonts::HighlightStyle;
|
||||
use language::{Bias, Chunk, Edit, Patch, Point, Rope, TextSummary};
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
cmp,
|
||||
ops::{Add, AddAssign, Range, Sub},
|
||||
};
|
||||
use util::post_inc;
|
||||
|
||||
pub type SuggestionEdit = Edit<SuggestionOffset>;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
|
||||
pub struct SuggestionOffset(pub usize);
|
||||
|
||||
impl Add for SuggestionOffset {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 + rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub for SuggestionOffset {
|
||||
type Output = Self;
|
||||
|
||||
fn sub(self, rhs: Self) -> Self::Output {
|
||||
Self(self.0 - rhs.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl AddAssign for SuggestionOffset {
|
||||
fn add_assign(&mut self, rhs: Self) {
|
||||
self.0 += rhs.0;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
|
||||
pub struct SuggestionPoint(pub Point);
|
||||
|
||||
impl SuggestionPoint {
|
||||
pub fn new(row: u32, column: u32) -> Self {
|
||||
Self(Point::new(row, column))
|
||||
}
|
||||
|
||||
pub fn row(self) -> u32 {
|
||||
self.0.row
|
||||
}
|
||||
|
||||
pub fn column(self) -> u32 {
|
||||
self.0.column
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Suggestion<T> {
|
||||
pub position: T,
|
||||
pub text: Rope,
|
||||
}
|
||||
|
||||
pub struct SuggestionMap(Mutex<SuggestionSnapshot>);
|
||||
|
||||
impl SuggestionMap {
|
||||
pub fn new(fold_snapshot: FoldSnapshot) -> (Self, SuggestionSnapshot) {
|
||||
let snapshot = SuggestionSnapshot {
|
||||
fold_snapshot,
|
||||
suggestion: None,
|
||||
version: 0,
|
||||
};
|
||||
(Self(Mutex::new(snapshot.clone())), snapshot)
|
||||
}
|
||||
|
||||
pub fn replace<T>(
|
||||
&self,
|
||||
new_suggestion: Option<Suggestion<T>>,
|
||||
fold_snapshot: FoldSnapshot,
|
||||
fold_edits: Vec<FoldEdit>,
|
||||
) -> (
|
||||
SuggestionSnapshot,
|
||||
Vec<SuggestionEdit>,
|
||||
Option<Suggestion<FoldOffset>>,
|
||||
)
|
||||
where
|
||||
T: ToPoint,
|
||||
{
|
||||
let new_suggestion = new_suggestion.map(|new_suggestion| {
|
||||
let buffer_point = new_suggestion
|
||||
.position
|
||||
.to_point(fold_snapshot.buffer_snapshot());
|
||||
let fold_point = fold_snapshot.to_fold_point(buffer_point, Bias::Left);
|
||||
let fold_offset = fold_point.to_offset(&fold_snapshot);
|
||||
Suggestion {
|
||||
position: fold_offset,
|
||||
text: new_suggestion.text,
|
||||
}
|
||||
});
|
||||
|
||||
let (_, edits) = self.sync(fold_snapshot, fold_edits);
|
||||
let mut snapshot = self.0.lock();
|
||||
|
||||
let mut patch = Patch::new(edits);
|
||||
let old_suggestion = snapshot.suggestion.take();
|
||||
if let Some(suggestion) = &old_suggestion {
|
||||
patch = patch.compose([SuggestionEdit {
|
||||
old: SuggestionOffset(suggestion.position.0)
|
||||
..SuggestionOffset(suggestion.position.0 + suggestion.text.len()),
|
||||
new: SuggestionOffset(suggestion.position.0)
|
||||
..SuggestionOffset(suggestion.position.0),
|
||||
}]);
|
||||
}
|
||||
|
||||
if let Some(suggestion) = new_suggestion.as_ref() {
|
||||
patch = patch.compose([SuggestionEdit {
|
||||
old: SuggestionOffset(suggestion.position.0)
|
||||
..SuggestionOffset(suggestion.position.0),
|
||||
new: SuggestionOffset(suggestion.position.0)
|
||||
..SuggestionOffset(suggestion.position.0 + suggestion.text.len()),
|
||||
}]);
|
||||
}
|
||||
|
||||
snapshot.suggestion = new_suggestion;
|
||||
snapshot.version += 1;
|
||||
(snapshot.clone(), patch.into_inner(), old_suggestion)
|
||||
}
|
||||
|
||||
pub fn sync(
|
||||
&self,
|
||||
fold_snapshot: FoldSnapshot,
|
||||
fold_edits: Vec<FoldEdit>,
|
||||
) -> (SuggestionSnapshot, Vec<SuggestionEdit>) {
|
||||
let mut snapshot = self.0.lock();
|
||||
|
||||
if snapshot.fold_snapshot.version != fold_snapshot.version {
|
||||
snapshot.version += 1;
|
||||
}
|
||||
|
||||
let mut suggestion_edits = Vec::new();
|
||||
|
||||
let mut suggestion_old_len = 0;
|
||||
let mut suggestion_new_len = 0;
|
||||
for fold_edit in fold_edits {
|
||||
let start = fold_edit.new.start;
|
||||
let end = FoldOffset(start.0 + fold_edit.old_len().0);
|
||||
if let Some(suggestion) = snapshot.suggestion.as_mut() {
|
||||
if end <= suggestion.position {
|
||||
suggestion.position.0 += fold_edit.new_len().0;
|
||||
suggestion.position.0 -= fold_edit.old_len().0;
|
||||
} else if start > suggestion.position {
|
||||
suggestion_old_len = suggestion.text.len();
|
||||
suggestion_new_len = suggestion_old_len;
|
||||
} else {
|
||||
suggestion_old_len = suggestion.text.len();
|
||||
snapshot.suggestion.take();
|
||||
suggestion_edits.push(SuggestionEdit {
|
||||
old: SuggestionOffset(fold_edit.old.start.0)
|
||||
..SuggestionOffset(fold_edit.old.end.0 + suggestion_old_len),
|
||||
new: SuggestionOffset(fold_edit.new.start.0)
|
||||
..SuggestionOffset(fold_edit.new.end.0),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
suggestion_edits.push(SuggestionEdit {
|
||||
old: SuggestionOffset(fold_edit.old.start.0 + suggestion_old_len)
|
||||
..SuggestionOffset(fold_edit.old.end.0 + suggestion_old_len),
|
||||
new: SuggestionOffset(fold_edit.new.start.0 + suggestion_new_len)
|
||||
..SuggestionOffset(fold_edit.new.end.0 + suggestion_new_len),
|
||||
});
|
||||
}
|
||||
snapshot.fold_snapshot = fold_snapshot;
|
||||
|
||||
(snapshot.clone(), suggestion_edits)
|
||||
}
|
||||
|
||||
pub fn has_suggestion(&self) -> bool {
|
||||
let snapshot = self.0.lock();
|
||||
snapshot.suggestion.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SuggestionSnapshot {
|
||||
pub fold_snapshot: FoldSnapshot,
|
||||
pub suggestion: Option<Suggestion<FoldOffset>>,
|
||||
pub version: usize,
|
||||
}
|
||||
|
||||
impl SuggestionSnapshot {
|
||||
pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
|
||||
self.fold_snapshot.buffer_snapshot()
|
||||
}
|
||||
|
||||
pub fn max_point(&self) -> SuggestionPoint {
|
||||
if let Some(suggestion) = self.suggestion.as_ref() {
|
||||
let suggestion_point = suggestion.position.to_point(&self.fold_snapshot);
|
||||
let mut max_point = suggestion_point.0;
|
||||
max_point += suggestion.text.max_point();
|
||||
max_point += self.fold_snapshot.max_point().0 - suggestion_point.0;
|
||||
SuggestionPoint(max_point)
|
||||
} else {
|
||||
SuggestionPoint(self.fold_snapshot.max_point().0)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> SuggestionOffset {
|
||||
if let Some(suggestion) = self.suggestion.as_ref() {
|
||||
let mut len = suggestion.position.0;
|
||||
len += suggestion.text.len();
|
||||
len += self.fold_snapshot.len().0 - suggestion.position.0;
|
||||
SuggestionOffset(len)
|
||||
} else {
|
||||
SuggestionOffset(self.fold_snapshot.len().0)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn line_len(&self, row: u32) -> u32 {
|
||||
if let Some(suggestion) = &self.suggestion {
|
||||
let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
|
||||
let suggestion_end = suggestion_start + suggestion.text.max_point();
|
||||
|
||||
if row < suggestion_start.row {
|
||||
self.fold_snapshot.line_len(row)
|
||||
} else if row > suggestion_end.row {
|
||||
self.fold_snapshot
|
||||
.line_len(suggestion_start.row + (row - suggestion_end.row))
|
||||
} else {
|
||||
let mut result = suggestion.text.line_len(row - suggestion_start.row);
|
||||
if row == suggestion_start.row {
|
||||
result += suggestion_start.column;
|
||||
}
|
||||
if row == suggestion_end.row {
|
||||
result +=
|
||||
self.fold_snapshot.line_len(suggestion_start.row) - suggestion_start.column;
|
||||
}
|
||||
result
|
||||
}
|
||||
} else {
|
||||
self.fold_snapshot.line_len(row)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clip_point(&self, point: SuggestionPoint, bias: Bias) -> SuggestionPoint {
|
||||
if let Some(suggestion) = self.suggestion.as_ref() {
|
||||
let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
|
||||
let suggestion_end = suggestion_start + suggestion.text.max_point();
|
||||
if point.0 <= suggestion_start {
|
||||
SuggestionPoint(self.fold_snapshot.clip_point(FoldPoint(point.0), bias).0)
|
||||
} else if point.0 > suggestion_end {
|
||||
let fold_point = self.fold_snapshot.clip_point(
|
||||
FoldPoint(suggestion_start + (point.0 - suggestion_end)),
|
||||
bias,
|
||||
);
|
||||
let suggestion_point = suggestion_end + (fold_point.0 - suggestion_start);
|
||||
if bias == Bias::Left && suggestion_point == suggestion_end {
|
||||
SuggestionPoint(suggestion_start)
|
||||
} else {
|
||||
SuggestionPoint(suggestion_point)
|
||||
}
|
||||
} else if bias == Bias::Left || suggestion_start == self.fold_snapshot.max_point().0 {
|
||||
SuggestionPoint(suggestion_start)
|
||||
} else {
|
||||
let fold_point = if self.fold_snapshot.line_len(suggestion_start.row)
|
||||
> suggestion_start.column
|
||||
{
|
||||
FoldPoint(suggestion_start + Point::new(0, 1))
|
||||
} else {
|
||||
FoldPoint(suggestion_start + Point::new(1, 0))
|
||||
};
|
||||
let clipped_fold_point = self.fold_snapshot.clip_point(fold_point, bias);
|
||||
SuggestionPoint(suggestion_end + (clipped_fold_point.0 - suggestion_start))
|
||||
}
|
||||
} else {
|
||||
SuggestionPoint(self.fold_snapshot.clip_point(FoldPoint(point.0), bias).0)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_offset(&self, point: SuggestionPoint) -> SuggestionOffset {
|
||||
if let Some(suggestion) = self.suggestion.as_ref() {
|
||||
let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
|
||||
let suggestion_end = suggestion_start + suggestion.text.max_point();
|
||||
|
||||
if point.0 <= suggestion_start {
|
||||
SuggestionOffset(FoldPoint(point.0).to_offset(&self.fold_snapshot).0)
|
||||
} else if point.0 > suggestion_end {
|
||||
let fold_offset = FoldPoint(suggestion_start + (point.0 - suggestion_end))
|
||||
.to_offset(&self.fold_snapshot);
|
||||
SuggestionOffset(fold_offset.0 + suggestion.text.len())
|
||||
} else {
|
||||
let offset_in_suggestion =
|
||||
suggestion.text.point_to_offset(point.0 - suggestion_start);
|
||||
SuggestionOffset(suggestion.position.0 + offset_in_suggestion)
|
||||
}
|
||||
} else {
|
||||
SuggestionOffset(FoldPoint(point.0).to_offset(&self.fold_snapshot).0)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_point(&self, offset: SuggestionOffset) -> SuggestionPoint {
|
||||
if let Some(suggestion) = self.suggestion.as_ref() {
|
||||
let suggestion_point_start = suggestion.position.to_point(&self.fold_snapshot).0;
|
||||
if offset.0 <= suggestion.position.0 {
|
||||
SuggestionPoint(FoldOffset(offset.0).to_point(&self.fold_snapshot).0)
|
||||
} else if offset.0 > (suggestion.position.0 + suggestion.text.len()) {
|
||||
let fold_point = FoldOffset(offset.0 - suggestion.text.len())
|
||||
.to_point(&self.fold_snapshot)
|
||||
.0;
|
||||
|
||||
SuggestionPoint(
|
||||
suggestion_point_start
|
||||
+ suggestion.text.max_point()
|
||||
+ (fold_point - suggestion_point_start),
|
||||
)
|
||||
} else {
|
||||
let point_in_suggestion = suggestion
|
||||
.text
|
||||
.offset_to_point(offset.0 - suggestion.position.0);
|
||||
SuggestionPoint(suggestion_point_start + point_in_suggestion)
|
||||
}
|
||||
} else {
|
||||
SuggestionPoint(FoldOffset(offset.0).to_point(&self.fold_snapshot).0)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_fold_point(&self, point: SuggestionPoint) -> FoldPoint {
|
||||
if let Some(suggestion) = self.suggestion.as_ref() {
|
||||
let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
|
||||
let suggestion_end = suggestion_start + suggestion.text.max_point();
|
||||
|
||||
if point.0 <= suggestion_start {
|
||||
FoldPoint(point.0)
|
||||
} else if point.0 > suggestion_end {
|
||||
FoldPoint(suggestion_start + (point.0 - suggestion_end))
|
||||
} else {
|
||||
FoldPoint(suggestion_start)
|
||||
}
|
||||
} else {
|
||||
FoldPoint(point.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_suggestion_point(&self, point: FoldPoint) -> SuggestionPoint {
|
||||
if let Some(suggestion) = self.suggestion.as_ref() {
|
||||
let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
|
||||
|
||||
if point.0 <= suggestion_start {
|
||||
SuggestionPoint(point.0)
|
||||
} else {
|
||||
let suggestion_end = suggestion_start + suggestion.text.max_point();
|
||||
SuggestionPoint(suggestion_end + (point.0 - suggestion_start))
|
||||
}
|
||||
} else {
|
||||
SuggestionPoint(point.0)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn text_summary_for_range(&self, range: Range<SuggestionPoint>) -> TextSummary {
|
||||
if let Some(suggestion) = self.suggestion.as_ref() {
|
||||
let suggestion_start = suggestion.position.to_point(&self.fold_snapshot).0;
|
||||
let suggestion_end = suggestion_start + suggestion.text.max_point();
|
||||
let mut summary = TextSummary::default();
|
||||
|
||||
let prefix_range =
|
||||
cmp::min(range.start.0, suggestion_start)..cmp::min(range.end.0, suggestion_start);
|
||||
if prefix_range.start < prefix_range.end {
|
||||
summary += self.fold_snapshot.text_summary_for_range(
|
||||
FoldPoint(prefix_range.start)..FoldPoint(prefix_range.end),
|
||||
);
|
||||
}
|
||||
|
||||
let suggestion_range =
|
||||
cmp::max(range.start.0, suggestion_start)..cmp::min(range.end.0, suggestion_end);
|
||||
if suggestion_range.start < suggestion_range.end {
|
||||
let point_range = suggestion_range.start - suggestion_start
|
||||
..suggestion_range.end - suggestion_start;
|
||||
let offset_range = suggestion.text.point_to_offset(point_range.start)
|
||||
..suggestion.text.point_to_offset(point_range.end);
|
||||
summary += suggestion
|
||||
.text
|
||||
.cursor(offset_range.start)
|
||||
.summary::<TextSummary>(offset_range.end);
|
||||
}
|
||||
|
||||
let suffix_range = cmp::max(range.start.0, suggestion_end)..range.end.0;
|
||||
if suffix_range.start < suffix_range.end {
|
||||
let start = suggestion_start + (suffix_range.start - suggestion_end);
|
||||
let end = suggestion_start + (suffix_range.end - suggestion_end);
|
||||
summary += self
|
||||
.fold_snapshot
|
||||
.text_summary_for_range(FoldPoint(start)..FoldPoint(end));
|
||||
}
|
||||
|
||||
summary
|
||||
} else {
|
||||
self.fold_snapshot
|
||||
.text_summary_for_range(FoldPoint(range.start.0)..FoldPoint(range.end.0))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn chars_at(&self, start: SuggestionPoint) -> impl '_ + Iterator<Item = char> {
|
||||
let start = self.to_offset(start);
|
||||
self.chunks(start..self.len(), false, None, None)
|
||||
.flat_map(|chunk| chunk.text.chars())
|
||||
}
|
||||
|
||||
pub fn chunks<'a>(
|
||||
&'a self,
|
||||
range: Range<SuggestionOffset>,
|
||||
language_aware: bool,
|
||||
text_highlights: Option<&'a TextHighlights>,
|
||||
suggestion_highlight: Option<HighlightStyle>,
|
||||
) -> SuggestionChunks<'a> {
|
||||
if let Some(suggestion) = self.suggestion.as_ref() {
|
||||
let suggestion_range =
|
||||
suggestion.position.0..suggestion.position.0 + suggestion.text.len();
|
||||
|
||||
let prefix_chunks = if range.start.0 < suggestion_range.start {
|
||||
Some(self.fold_snapshot.chunks(
|
||||
FoldOffset(range.start.0)
|
||||
..cmp::min(FoldOffset(suggestion_range.start), FoldOffset(range.end.0)),
|
||||
language_aware,
|
||||
text_highlights,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let clipped_suggestion_range = cmp::max(range.start.0, suggestion_range.start)
|
||||
..cmp::min(range.end.0, suggestion_range.end);
|
||||
let suggestion_chunks = if clipped_suggestion_range.start < clipped_suggestion_range.end
|
||||
{
|
||||
let start = clipped_suggestion_range.start - suggestion_range.start;
|
||||
let end = clipped_suggestion_range.end - suggestion_range.start;
|
||||
Some(suggestion.text.chunks_in_range(start..end))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let suffix_chunks = if range.end.0 > suggestion_range.end {
|
||||
let start = cmp::max(suggestion_range.end, range.start.0) - suggestion_range.len();
|
||||
let end = range.end.0 - suggestion_range.len();
|
||||
Some(self.fold_snapshot.chunks(
|
||||
FoldOffset(start)..FoldOffset(end),
|
||||
language_aware,
|
||||
text_highlights,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
SuggestionChunks {
|
||||
prefix_chunks,
|
||||
suggestion_chunks,
|
||||
suffix_chunks,
|
||||
highlight_style: suggestion_highlight,
|
||||
}
|
||||
} else {
|
||||
SuggestionChunks {
|
||||
prefix_chunks: Some(self.fold_snapshot.chunks(
|
||||
FoldOffset(range.start.0)..FoldOffset(range.end.0),
|
||||
language_aware,
|
||||
text_highlights,
|
||||
)),
|
||||
suggestion_chunks: None,
|
||||
suffix_chunks: None,
|
||||
highlight_style: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buffer_rows<'a>(&'a self, row: u32) -> SuggestionBufferRows<'a> {
|
||||
let suggestion_range = if let Some(suggestion) = self.suggestion.as_ref() {
|
||||
let start = suggestion.position.to_point(&self.fold_snapshot).0;
|
||||
let end = start + suggestion.text.max_point();
|
||||
start.row..end.row
|
||||
} else {
|
||||
u32::MAX..u32::MAX
|
||||
};
|
||||
|
||||
let fold_buffer_rows = if row <= suggestion_range.start {
|
||||
self.fold_snapshot.buffer_rows(row)
|
||||
} else if row > suggestion_range.end {
|
||||
self.fold_snapshot
|
||||
.buffer_rows(row - (suggestion_range.end - suggestion_range.start))
|
||||
} else {
|
||||
let mut rows = self.fold_snapshot.buffer_rows(suggestion_range.start);
|
||||
rows.next();
|
||||
rows
|
||||
};
|
||||
|
||||
SuggestionBufferRows {
|
||||
current_row: row,
|
||||
suggestion_row_start: suggestion_range.start,
|
||||
suggestion_row_end: suggestion_range.end,
|
||||
fold_buffer_rows,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn text(&self) -> String {
|
||||
self.chunks(Default::default()..self.len(), false, None, None)
|
||||
.map(|chunk| chunk.text)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SuggestionChunks<'a> {
|
||||
prefix_chunks: Option<FoldChunks<'a>>,
|
||||
suggestion_chunks: Option<text::Chunks<'a>>,
|
||||
suffix_chunks: Option<FoldChunks<'a>>,
|
||||
highlight_style: Option<HighlightStyle>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for SuggestionChunks<'a> {
|
||||
type Item = Chunk<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if let Some(chunks) = self.prefix_chunks.as_mut() {
|
||||
if let Some(chunk) = chunks.next() {
|
||||
return Some(chunk);
|
||||
} else {
|
||||
self.prefix_chunks = None;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(chunks) = self.suggestion_chunks.as_mut() {
|
||||
if let Some(chunk) = chunks.next() {
|
||||
return Some(Chunk {
|
||||
text: chunk,
|
||||
highlight_style: self.highlight_style,
|
||||
..Default::default()
|
||||
});
|
||||
} else {
|
||||
self.suggestion_chunks = None;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(chunks) = self.suffix_chunks.as_mut() {
|
||||
if let Some(chunk) = chunks.next() {
|
||||
return Some(chunk);
|
||||
} else {
|
||||
self.suffix_chunks = None;
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SuggestionBufferRows<'a> {
|
||||
current_row: u32,
|
||||
suggestion_row_start: u32,
|
||||
suggestion_row_end: u32,
|
||||
fold_buffer_rows: FoldBufferRows<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for SuggestionBufferRows<'a> {
|
||||
type Item = Option<u32>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let row = post_inc(&mut self.current_row);
|
||||
if row <= self.suggestion_row_start || row > self.suggestion_row_end {
|
||||
self.fold_buffer_rows.next()
|
||||
} else {
|
||||
Some(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{display_map::fold_map::FoldMap, MultiBuffer};
|
||||
use gpui::AppContext;
|
||||
use rand::{prelude::StdRng, Rng};
|
||||
use settings::SettingsStore;
|
||||
use std::{
|
||||
env,
|
||||
ops::{Bound, RangeBounds},
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
fn test_basic(cx: &mut AppContext) {
|
||||
let buffer = MultiBuffer::build_simple("abcdefghi", cx);
|
||||
let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe());
|
||||
let (mut fold_map, fold_snapshot) = FoldMap::new(buffer.read(cx).snapshot(cx));
|
||||
let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone());
|
||||
assert_eq!(suggestion_snapshot.text(), "abcdefghi");
|
||||
|
||||
let (suggestion_snapshot, _, _) = suggestion_map.replace(
|
||||
Some(Suggestion {
|
||||
position: 3,
|
||||
text: "123\n456".into(),
|
||||
}),
|
||||
fold_snapshot,
|
||||
Default::default(),
|
||||
);
|
||||
assert_eq!(suggestion_snapshot.text(), "abc123\n456defghi");
|
||||
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
[(0..0, "ABC"), (3..3, "DEF"), (4..4, "GHI"), (9..9, "JKL")],
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let (fold_snapshot, fold_edits) = fold_map.read(
|
||||
buffer.read(cx).snapshot(cx),
|
||||
buffer_edits.consume().into_inner(),
|
||||
);
|
||||
let (suggestion_snapshot, _) = suggestion_map.sync(fold_snapshot.clone(), fold_edits);
|
||||
assert_eq!(suggestion_snapshot.text(), "ABCabcDEF123\n456dGHIefghiJKL");
|
||||
|
||||
let (mut fold_map_writer, _, _) =
|
||||
fold_map.write(buffer.read(cx).snapshot(cx), Default::default());
|
||||
let (fold_snapshot, fold_edits) = fold_map_writer.fold([0..3]);
|
||||
let (suggestion_snapshot, _) = suggestion_map.sync(fold_snapshot, fold_edits);
|
||||
assert_eq!(suggestion_snapshot.text(), "⋯abcDEF123\n456dGHIefghiJKL");
|
||||
|
||||
let (mut fold_map_writer, _, _) =
|
||||
fold_map.write(buffer.read(cx).snapshot(cx), Default::default());
|
||||
let (fold_snapshot, fold_edits) = fold_map_writer.fold([6..10]);
|
||||
let (suggestion_snapshot, _) = suggestion_map.sync(fold_snapshot, fold_edits);
|
||||
assert_eq!(suggestion_snapshot.text(), "⋯abc⋯GHIefghiJKL");
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_random_suggestions(cx: &mut AppContext, mut rng: StdRng) {
|
||||
init_test(cx);
|
||||
|
||||
let operations = env::var("OPERATIONS")
|
||||
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
|
||||
.unwrap_or(10);
|
||||
|
||||
let len = rng.gen_range(0..30);
|
||||
let buffer = if rng.gen() {
|
||||
let text = util::RandomCharIter::new(&mut rng)
|
||||
.take(len)
|
||||
.collect::<String>();
|
||||
MultiBuffer::build_simple(&text, cx)
|
||||
} else {
|
||||
MultiBuffer::build_random(&mut rng, cx)
|
||||
};
|
||||
let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||
log::info!("buffer text: {:?}", buffer_snapshot.text());
|
||||
|
||||
let (mut fold_map, mut fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
|
||||
let (suggestion_map, mut suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone());
|
||||
|
||||
for _ in 0..operations {
|
||||
let mut suggestion_edits = Patch::default();
|
||||
|
||||
let mut prev_suggestion_text = suggestion_snapshot.text();
|
||||
let mut buffer_edits = Vec::new();
|
||||
match rng.gen_range(0..=100) {
|
||||
0..=29 => {
|
||||
let (_, edits) = suggestion_map.randomly_mutate(&mut rng);
|
||||
suggestion_edits = suggestion_edits.compose(edits);
|
||||
}
|
||||
30..=59 => {
|
||||
for (new_fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
|
||||
fold_snapshot = new_fold_snapshot;
|
||||
let (_, edits) = suggestion_map.sync(fold_snapshot.clone(), fold_edits);
|
||||
suggestion_edits = suggestion_edits.compose(edits);
|
||||
}
|
||||
}
|
||||
_ => buffer.update(cx, |buffer, cx| {
|
||||
let subscription = buffer.subscribe();
|
||||
let edit_count = rng.gen_range(1..=5);
|
||||
buffer.randomly_mutate(&mut rng, edit_count, cx);
|
||||
buffer_snapshot = buffer.snapshot(cx);
|
||||
let edits = subscription.consume().into_inner();
|
||||
log::info!("editing {:?}", edits);
|
||||
buffer_edits.extend(edits);
|
||||
}),
|
||||
};
|
||||
|
||||
let (new_fold_snapshot, fold_edits) =
|
||||
fold_map.read(buffer_snapshot.clone(), buffer_edits);
|
||||
fold_snapshot = new_fold_snapshot;
|
||||
let (new_suggestion_snapshot, edits) =
|
||||
suggestion_map.sync(fold_snapshot.clone(), fold_edits);
|
||||
suggestion_snapshot = new_suggestion_snapshot;
|
||||
suggestion_edits = suggestion_edits.compose(edits);
|
||||
|
||||
log::info!("buffer text: {:?}", buffer_snapshot.text());
|
||||
log::info!("folds text: {:?}", fold_snapshot.text());
|
||||
log::info!("suggestions text: {:?}", suggestion_snapshot.text());
|
||||
|
||||
let mut expected_text = Rope::from(fold_snapshot.text().as_str());
|
||||
let mut expected_buffer_rows = fold_snapshot.buffer_rows(0).collect::<Vec<_>>();
|
||||
if let Some(suggestion) = suggestion_snapshot.suggestion.as_ref() {
|
||||
expected_text.replace(
|
||||
suggestion.position.0..suggestion.position.0,
|
||||
&suggestion.text.to_string(),
|
||||
);
|
||||
let suggestion_start = suggestion.position.to_point(&fold_snapshot).0;
|
||||
let suggestion_end = suggestion_start + suggestion.text.max_point();
|
||||
expected_buffer_rows.splice(
|
||||
(suggestion_start.row + 1) as usize..(suggestion_start.row + 1) as usize,
|
||||
(0..suggestion_end.row - suggestion_start.row).map(|_| None),
|
||||
);
|
||||
}
|
||||
assert_eq!(suggestion_snapshot.text(), expected_text.to_string());
|
||||
for row_start in 0..expected_buffer_rows.len() {
|
||||
assert_eq!(
|
||||
suggestion_snapshot
|
||||
.buffer_rows(row_start as u32)
|
||||
.collect::<Vec<_>>(),
|
||||
&expected_buffer_rows[row_start..],
|
||||
"incorrect buffer rows starting at {}",
|
||||
row_start
|
||||
);
|
||||
}
|
||||
|
||||
for _ in 0..5 {
|
||||
let mut end = rng.gen_range(0..=suggestion_snapshot.len().0);
|
||||
end = expected_text.clip_offset(end, Bias::Right);
|
||||
let mut start = rng.gen_range(0..=end);
|
||||
start = expected_text.clip_offset(start, Bias::Right);
|
||||
|
||||
let actual_text = suggestion_snapshot
|
||||
.chunks(
|
||||
SuggestionOffset(start)..SuggestionOffset(end),
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.map(|chunk| chunk.text)
|
||||
.collect::<String>();
|
||||
assert_eq!(
|
||||
actual_text,
|
||||
expected_text.slice(start..end).to_string(),
|
||||
"incorrect text in range {:?}",
|
||||
start..end
|
||||
);
|
||||
|
||||
let start_point = SuggestionPoint(expected_text.offset_to_point(start));
|
||||
let end_point = SuggestionPoint(expected_text.offset_to_point(end));
|
||||
assert_eq!(
|
||||
suggestion_snapshot.text_summary_for_range(start_point..end_point),
|
||||
expected_text.slice(start..end).summary()
|
||||
);
|
||||
}
|
||||
|
||||
for edit in suggestion_edits.into_inner() {
|
||||
prev_suggestion_text.replace_range(
|
||||
edit.new.start.0..edit.new.start.0 + edit.old_len().0,
|
||||
&suggestion_snapshot.text()[edit.new.start.0..edit.new.end.0],
|
||||
);
|
||||
}
|
||||
assert_eq!(prev_suggestion_text, suggestion_snapshot.text());
|
||||
|
||||
assert_eq!(expected_text.max_point(), suggestion_snapshot.max_point().0);
|
||||
assert_eq!(expected_text.len(), suggestion_snapshot.len().0);
|
||||
|
||||
let mut suggestion_point = SuggestionPoint::default();
|
||||
let mut suggestion_offset = SuggestionOffset::default();
|
||||
for ch in expected_text.chars() {
|
||||
assert_eq!(
|
||||
suggestion_snapshot.to_offset(suggestion_point),
|
||||
suggestion_offset,
|
||||
"invalid to_offset({:?})",
|
||||
suggestion_point
|
||||
);
|
||||
assert_eq!(
|
||||
suggestion_snapshot.to_point(suggestion_offset),
|
||||
suggestion_point,
|
||||
"invalid to_point({:?})",
|
||||
suggestion_offset
|
||||
);
|
||||
assert_eq!(
|
||||
suggestion_snapshot
|
||||
.to_suggestion_point(suggestion_snapshot.to_fold_point(suggestion_point)),
|
||||
suggestion_snapshot.clip_point(suggestion_point, Bias::Left),
|
||||
);
|
||||
|
||||
let mut bytes = [0; 4];
|
||||
for byte in ch.encode_utf8(&mut bytes).as_bytes() {
|
||||
suggestion_offset.0 += 1;
|
||||
if *byte == b'\n' {
|
||||
suggestion_point.0 += Point::new(1, 0);
|
||||
} else {
|
||||
suggestion_point.0 += Point::new(0, 1);
|
||||
}
|
||||
|
||||
let clipped_left_point =
|
||||
suggestion_snapshot.clip_point(suggestion_point, Bias::Left);
|
||||
let clipped_right_point =
|
||||
suggestion_snapshot.clip_point(suggestion_point, Bias::Right);
|
||||
assert!(
|
||||
clipped_left_point <= clipped_right_point,
|
||||
"clipped left point {:?} is greater than clipped right point {:?}",
|
||||
clipped_left_point,
|
||||
clipped_right_point
|
||||
);
|
||||
assert_eq!(
|
||||
clipped_left_point.0,
|
||||
expected_text.clip_point(clipped_left_point.0, Bias::Left)
|
||||
);
|
||||
assert_eq!(
|
||||
clipped_right_point.0,
|
||||
expected_text.clip_point(clipped_right_point.0, Bias::Right)
|
||||
);
|
||||
assert!(clipped_left_point <= suggestion_snapshot.max_point());
|
||||
assert!(clipped_right_point <= suggestion_snapshot.max_point());
|
||||
|
||||
if let Some(suggestion) = suggestion_snapshot.suggestion.as_ref() {
|
||||
let suggestion_start = suggestion.position.to_point(&fold_snapshot).0;
|
||||
let suggestion_end = suggestion_start + suggestion.text.max_point();
|
||||
let invalid_range = (
|
||||
Bound::Excluded(suggestion_start),
|
||||
Bound::Included(suggestion_end),
|
||||
);
|
||||
assert!(
|
||||
!invalid_range.contains(&clipped_left_point.0),
|
||||
"clipped left point {:?} is inside invalid suggestion range {:?}",
|
||||
clipped_left_point,
|
||||
invalid_range
|
||||
);
|
||||
assert!(
|
||||
!invalid_range.contains(&clipped_right_point.0),
|
||||
"clipped right point {:?} is inside invalid suggestion range {:?}",
|
||||
clipped_right_point,
|
||||
invalid_range
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut AppContext) {
|
||||
cx.set_global(SettingsStore::test(cx));
|
||||
theme::init((), cx);
|
||||
}
|
||||
|
||||
impl SuggestionMap {
|
||||
pub fn randomly_mutate(
|
||||
&self,
|
||||
rng: &mut impl Rng,
|
||||
) -> (SuggestionSnapshot, Vec<SuggestionEdit>) {
|
||||
let fold_snapshot = self.0.lock().fold_snapshot.clone();
|
||||
let new_suggestion = if rng.gen_bool(0.3) {
|
||||
None
|
||||
} else {
|
||||
let index = rng.gen_range(0..=fold_snapshot.buffer_snapshot().len());
|
||||
let len = rng.gen_range(0..30);
|
||||
Some(Suggestion {
|
||||
position: index,
|
||||
text: util::RandomCharIter::new(rng)
|
||||
.take(len)
|
||||
.filter(|ch| *ch != '\r')
|
||||
.collect::<String>()
|
||||
.as_str()
|
||||
.into(),
|
||||
})
|
||||
};
|
||||
|
||||
log::info!("replacing suggestion with {:?}", new_suggestion);
|
||||
let (snapshot, edits, _) =
|
||||
self.replace(new_suggestion, fold_snapshot, Default::default());
|
||||
(snapshot, edits)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,80 +1,76 @@
|
||||
use super::{
|
||||
suggestion_map::{self, SuggestionChunks, SuggestionEdit, SuggestionPoint, SuggestionSnapshot},
|
||||
fold_map::{self, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot},
|
||||
TextHighlights,
|
||||
};
|
||||
use crate::MultiBufferSnapshot;
|
||||
use gpui::fonts::HighlightStyle;
|
||||
use language::{Chunk, Point};
|
||||
use parking_lot::Mutex;
|
||||
use std::{cmp, mem, num::NonZeroU32, ops::Range};
|
||||
use sum_tree::Bias;
|
||||
|
||||
const MAX_EXPANSION_COLUMN: u32 = 256;
|
||||
|
||||
pub struct TabMap(Mutex<TabSnapshot>);
|
||||
pub struct TabMap(TabSnapshot);
|
||||
|
||||
impl TabMap {
|
||||
pub fn new(input: SuggestionSnapshot, tab_size: NonZeroU32) -> (Self, TabSnapshot) {
|
||||
pub fn new(fold_snapshot: FoldSnapshot, tab_size: NonZeroU32) -> (Self, TabSnapshot) {
|
||||
let snapshot = TabSnapshot {
|
||||
suggestion_snapshot: input,
|
||||
fold_snapshot,
|
||||
tab_size,
|
||||
max_expansion_column: MAX_EXPANSION_COLUMN,
|
||||
version: 0,
|
||||
};
|
||||
(Self(Mutex::new(snapshot.clone())), snapshot)
|
||||
(Self(snapshot.clone()), snapshot)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn set_max_expansion_column(&self, column: u32) -> TabSnapshot {
|
||||
self.0.lock().max_expansion_column = column;
|
||||
self.0.lock().clone()
|
||||
pub fn set_max_expansion_column(&mut self, column: u32) -> TabSnapshot {
|
||||
self.0.max_expansion_column = column;
|
||||
self.0.clone()
|
||||
}
|
||||
|
||||
pub fn sync(
|
||||
&self,
|
||||
suggestion_snapshot: SuggestionSnapshot,
|
||||
mut suggestion_edits: Vec<SuggestionEdit>,
|
||||
&mut self,
|
||||
fold_snapshot: FoldSnapshot,
|
||||
mut fold_edits: Vec<FoldEdit>,
|
||||
tab_size: NonZeroU32,
|
||||
) -> (TabSnapshot, Vec<TabEdit>) {
|
||||
let mut old_snapshot = self.0.lock();
|
||||
let old_snapshot = &mut self.0;
|
||||
let mut new_snapshot = TabSnapshot {
|
||||
suggestion_snapshot,
|
||||
fold_snapshot,
|
||||
tab_size,
|
||||
max_expansion_column: old_snapshot.max_expansion_column,
|
||||
version: old_snapshot.version,
|
||||
};
|
||||
|
||||
if old_snapshot.suggestion_snapshot.version != new_snapshot.suggestion_snapshot.version {
|
||||
if old_snapshot.fold_snapshot.version != new_snapshot.fold_snapshot.version {
|
||||
new_snapshot.version += 1;
|
||||
}
|
||||
|
||||
let mut tab_edits = Vec::with_capacity(suggestion_edits.len());
|
||||
let mut tab_edits = Vec::with_capacity(fold_edits.len());
|
||||
|
||||
if old_snapshot.tab_size == new_snapshot.tab_size {
|
||||
// Expand each edit to include the next tab on the same line as the edit,
|
||||
// and any subsequent tabs on that line that moved across the tab expansion
|
||||
// boundary.
|
||||
for suggestion_edit in &mut suggestion_edits {
|
||||
let old_end = old_snapshot
|
||||
.suggestion_snapshot
|
||||
.to_point(suggestion_edit.old.end);
|
||||
let old_end_row_successor_offset =
|
||||
old_snapshot.suggestion_snapshot.to_offset(cmp::min(
|
||||
SuggestionPoint::new(old_end.row() + 1, 0),
|
||||
old_snapshot.suggestion_snapshot.max_point(),
|
||||
));
|
||||
let new_end = new_snapshot
|
||||
.suggestion_snapshot
|
||||
.to_point(suggestion_edit.new.end);
|
||||
for fold_edit in &mut fold_edits {
|
||||
let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot);
|
||||
let old_end_row_successor_offset = cmp::min(
|
||||
FoldPoint::new(old_end.row() + 1, 0),
|
||||
old_snapshot.fold_snapshot.max_point(),
|
||||
)
|
||||
.to_offset(&old_snapshot.fold_snapshot);
|
||||
let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
|
||||
|
||||
let mut offset_from_edit = 0;
|
||||
let mut first_tab_offset = None;
|
||||
let mut last_tab_with_changed_expansion_offset = None;
|
||||
'outer: for chunk in old_snapshot.suggestion_snapshot.chunks(
|
||||
suggestion_edit.old.end..old_end_row_successor_offset,
|
||||
'outer: for chunk in old_snapshot.fold_snapshot.chunks(
|
||||
fold_edit.old.end..old_end_row_successor_offset,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
) {
|
||||
for (ix, _) in chunk.text.match_indices('\t') {
|
||||
let offset_from_edit = offset_from_edit + (ix as u32);
|
||||
@ -102,39 +98,31 @@ impl TabMap {
|
||||
}
|
||||
|
||||
if let Some(offset) = last_tab_with_changed_expansion_offset.or(first_tab_offset) {
|
||||
suggestion_edit.old.end.0 += offset as usize + 1;
|
||||
suggestion_edit.new.end.0 += offset as usize + 1;
|
||||
fold_edit.old.end.0 += offset as usize + 1;
|
||||
fold_edit.new.end.0 += offset as usize + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Combine any edits that overlap due to the expansion.
|
||||
let mut ix = 1;
|
||||
while ix < suggestion_edits.len() {
|
||||
let (prev_edits, next_edits) = suggestion_edits.split_at_mut(ix);
|
||||
while ix < fold_edits.len() {
|
||||
let (prev_edits, next_edits) = fold_edits.split_at_mut(ix);
|
||||
let prev_edit = prev_edits.last_mut().unwrap();
|
||||
let edit = &next_edits[0];
|
||||
if prev_edit.old.end >= edit.old.start {
|
||||
prev_edit.old.end = edit.old.end;
|
||||
prev_edit.new.end = edit.new.end;
|
||||
suggestion_edits.remove(ix);
|
||||
fold_edits.remove(ix);
|
||||
} else {
|
||||
ix += 1;
|
||||
}
|
||||
}
|
||||
|
||||
for suggestion_edit in suggestion_edits {
|
||||
let old_start = old_snapshot
|
||||
.suggestion_snapshot
|
||||
.to_point(suggestion_edit.old.start);
|
||||
let old_end = old_snapshot
|
||||
.suggestion_snapshot
|
||||
.to_point(suggestion_edit.old.end);
|
||||
let new_start = new_snapshot
|
||||
.suggestion_snapshot
|
||||
.to_point(suggestion_edit.new.start);
|
||||
let new_end = new_snapshot
|
||||
.suggestion_snapshot
|
||||
.to_point(suggestion_edit.new.end);
|
||||
for fold_edit in fold_edits {
|
||||
let old_start = fold_edit.old.start.to_point(&old_snapshot.fold_snapshot);
|
||||
let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot);
|
||||
let new_start = fold_edit.new.start.to_point(&new_snapshot.fold_snapshot);
|
||||
let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
|
||||
tab_edits.push(TabEdit {
|
||||
old: old_snapshot.to_tab_point(old_start)..old_snapshot.to_tab_point(old_end),
|
||||
new: new_snapshot.to_tab_point(new_start)..new_snapshot.to_tab_point(new_end),
|
||||
@ -155,7 +143,7 @@ impl TabMap {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TabSnapshot {
|
||||
pub suggestion_snapshot: SuggestionSnapshot,
|
||||
pub fold_snapshot: FoldSnapshot,
|
||||
pub tab_size: NonZeroU32,
|
||||
pub max_expansion_column: u32,
|
||||
pub version: usize,
|
||||
@ -163,18 +151,15 @@ pub struct TabSnapshot {
|
||||
|
||||
impl TabSnapshot {
|
||||
pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
|
||||
self.suggestion_snapshot.buffer_snapshot()
|
||||
&self.fold_snapshot.inlay_snapshot.buffer
|
||||
}
|
||||
|
||||
pub fn line_len(&self, row: u32) -> u32 {
|
||||
let max_point = self.max_point();
|
||||
if row < max_point.row() {
|
||||
self.to_tab_point(SuggestionPoint::new(
|
||||
row,
|
||||
self.suggestion_snapshot.line_len(row),
|
||||
))
|
||||
.0
|
||||
.column
|
||||
self.to_tab_point(FoldPoint::new(row, self.fold_snapshot.line_len(row)))
|
||||
.0
|
||||
.column
|
||||
} else {
|
||||
max_point.column()
|
||||
}
|
||||
@ -185,10 +170,10 @@ impl TabSnapshot {
|
||||
}
|
||||
|
||||
pub fn text_summary_for_range(&self, range: Range<TabPoint>) -> TextSummary {
|
||||
let input_start = self.to_suggestion_point(range.start, Bias::Left).0;
|
||||
let input_end = self.to_suggestion_point(range.end, Bias::Right).0;
|
||||
let input_start = self.to_fold_point(range.start, Bias::Left).0;
|
||||
let input_end = self.to_fold_point(range.end, Bias::Right).0;
|
||||
let input_summary = self
|
||||
.suggestion_snapshot
|
||||
.fold_snapshot
|
||||
.text_summary_for_range(input_start..input_end);
|
||||
|
||||
let mut first_line_chars = 0;
|
||||
@ -198,7 +183,7 @@ impl TabSnapshot {
|
||||
self.max_point()
|
||||
};
|
||||
for c in self
|
||||
.chunks(range.start..line_end, false, None, None)
|
||||
.chunks(range.start..line_end, false, None, None, None)
|
||||
.flat_map(|chunk| chunk.text.chars())
|
||||
{
|
||||
if c == '\n' {
|
||||
@ -217,6 +202,7 @@ impl TabSnapshot {
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.flat_map(|chunk| chunk.text.chars())
|
||||
{
|
||||
@ -238,15 +224,17 @@ impl TabSnapshot {
|
||||
range: Range<TabPoint>,
|
||||
language_aware: bool,
|
||||
text_highlights: Option<&'a TextHighlights>,
|
||||
suggestion_highlight: Option<HighlightStyle>,
|
||||
hint_highlights: Option<HighlightStyle>,
|
||||
suggestion_highlights: Option<HighlightStyle>,
|
||||
) -> TabChunks<'a> {
|
||||
let (input_start, expanded_char_column, to_next_stop) =
|
||||
self.to_suggestion_point(range.start, Bias::Left);
|
||||
self.to_fold_point(range.start, Bias::Left);
|
||||
let input_column = input_start.column();
|
||||
let input_start = self.suggestion_snapshot.to_offset(input_start);
|
||||
let input_start = input_start.to_offset(&self.fold_snapshot);
|
||||
let input_end = self
|
||||
.suggestion_snapshot
|
||||
.to_offset(self.to_suggestion_point(range.end, Bias::Right).0);
|
||||
.to_fold_point(range.end, Bias::Right)
|
||||
.0
|
||||
.to_offset(&self.fold_snapshot);
|
||||
let to_next_stop = if range.start.0 + Point::new(0, to_next_stop) > range.end.0 {
|
||||
range.end.column() - range.start.column()
|
||||
} else {
|
||||
@ -254,11 +242,12 @@ impl TabSnapshot {
|
||||
};
|
||||
|
||||
TabChunks {
|
||||
suggestion_chunks: self.suggestion_snapshot.chunks(
|
||||
fold_chunks: self.fold_snapshot.chunks(
|
||||
input_start..input_end,
|
||||
language_aware,
|
||||
text_highlights,
|
||||
suggestion_highlight,
|
||||
hint_highlights,
|
||||
suggestion_highlights,
|
||||
),
|
||||
input_column,
|
||||
column: expanded_char_column,
|
||||
@ -275,63 +264,58 @@ impl TabSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buffer_rows(&self, row: u32) -> suggestion_map::SuggestionBufferRows {
|
||||
self.suggestion_snapshot.buffer_rows(row)
|
||||
pub fn buffer_rows(&self, row: u32) -> fold_map::FoldBufferRows<'_> {
|
||||
self.fold_snapshot.buffer_rows(row)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn text(&self) -> String {
|
||||
self.chunks(TabPoint::zero()..self.max_point(), false, None, None)
|
||||
self.chunks(TabPoint::zero()..self.max_point(), false, None, None, None)
|
||||
.map(|chunk| chunk.text)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn max_point(&self) -> TabPoint {
|
||||
self.to_tab_point(self.suggestion_snapshot.max_point())
|
||||
self.to_tab_point(self.fold_snapshot.max_point())
|
||||
}
|
||||
|
||||
pub fn clip_point(&self, point: TabPoint, bias: Bias) -> TabPoint {
|
||||
self.to_tab_point(
|
||||
self.suggestion_snapshot
|
||||
.clip_point(self.to_suggestion_point(point, bias).0, bias),
|
||||
self.fold_snapshot
|
||||
.clip_point(self.to_fold_point(point, bias).0, bias),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn to_tab_point(&self, input: SuggestionPoint) -> TabPoint {
|
||||
let chars = self
|
||||
.suggestion_snapshot
|
||||
.chars_at(SuggestionPoint::new(input.row(), 0));
|
||||
pub fn to_tab_point(&self, input: FoldPoint) -> TabPoint {
|
||||
let chars = self.fold_snapshot.chars_at(FoldPoint::new(input.row(), 0));
|
||||
let expanded = self.expand_tabs(chars, input.column());
|
||||
TabPoint::new(input.row(), expanded)
|
||||
}
|
||||
|
||||
pub fn to_suggestion_point(&self, output: TabPoint, bias: Bias) -> (SuggestionPoint, u32, u32) {
|
||||
let chars = self
|
||||
.suggestion_snapshot
|
||||
.chars_at(SuggestionPoint::new(output.row(), 0));
|
||||
pub fn to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, u32, u32) {
|
||||
let chars = self.fold_snapshot.chars_at(FoldPoint::new(output.row(), 0));
|
||||
let expanded = output.column();
|
||||
let (collapsed, expanded_char_column, to_next_stop) =
|
||||
self.collapse_tabs(chars, expanded, bias);
|
||||
(
|
||||
SuggestionPoint::new(output.row(), collapsed as u32),
|
||||
FoldPoint::new(output.row(), collapsed as u32),
|
||||
expanded_char_column,
|
||||
to_next_stop,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn make_tab_point(&self, point: Point, bias: Bias) -> TabPoint {
|
||||
let fold_point = self
|
||||
.suggestion_snapshot
|
||||
.fold_snapshot
|
||||
.to_fold_point(point, bias);
|
||||
let suggestion_point = self.suggestion_snapshot.to_suggestion_point(fold_point);
|
||||
self.to_tab_point(suggestion_point)
|
||||
let inlay_point = self.fold_snapshot.inlay_snapshot.to_inlay_point(point);
|
||||
let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
|
||||
self.to_tab_point(fold_point)
|
||||
}
|
||||
|
||||
pub fn to_point(&self, point: TabPoint, bias: Bias) -> Point {
|
||||
let suggestion_point = self.to_suggestion_point(point, bias).0;
|
||||
let fold_point = self.suggestion_snapshot.to_fold_point(suggestion_point);
|
||||
fold_point.to_buffer_point(&self.suggestion_snapshot.fold_snapshot)
|
||||
let fold_point = self.to_fold_point(point, bias).0;
|
||||
let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
|
||||
self.fold_snapshot
|
||||
.inlay_snapshot
|
||||
.to_buffer_point(inlay_point)
|
||||
}
|
||||
|
||||
fn expand_tabs(&self, chars: impl Iterator<Item = char>, column: u32) -> u32 {
|
||||
@ -490,7 +474,7 @@ impl<'a> std::ops::AddAssign<&'a Self> for TextSummary {
|
||||
const SPACES: &str = " ";
|
||||
|
||||
pub struct TabChunks<'a> {
|
||||
suggestion_chunks: SuggestionChunks<'a>,
|
||||
fold_chunks: FoldChunks<'a>,
|
||||
chunk: Chunk<'a>,
|
||||
column: u32,
|
||||
max_expansion_column: u32,
|
||||
@ -506,7 +490,7 @@ impl<'a> Iterator for TabChunks<'a> {
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.chunk.text.is_empty() {
|
||||
if let Some(chunk) = self.suggestion_chunks.next() {
|
||||
if let Some(chunk) = self.fold_chunks.next() {
|
||||
self.chunk = chunk;
|
||||
if self.inside_leading_tab {
|
||||
self.chunk.text = &self.chunk.text[1..];
|
||||
@ -574,7 +558,7 @@ impl<'a> Iterator for TabChunks<'a> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
display_map::{fold_map::FoldMap, suggestion_map::SuggestionMap},
|
||||
display_map::{fold_map::FoldMap, inlay_map::InlayMap},
|
||||
MultiBuffer,
|
||||
};
|
||||
use rand::{prelude::StdRng, Rng};
|
||||
@ -583,9 +567,9 @@ mod tests {
|
||||
fn test_expand_tabs(cx: &mut gpui::AppContext) {
|
||||
let buffer = MultiBuffer::build_simple("", cx);
|
||||
let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||
let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
|
||||
let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
|
||||
let (_, tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
|
||||
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
|
||||
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
|
||||
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
|
||||
|
||||
assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 0), 0);
|
||||
assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 1), 4);
|
||||
@ -600,9 +584,9 @@ mod tests {
|
||||
|
||||
let buffer = MultiBuffer::build_simple(input, cx);
|
||||
let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||
let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
|
||||
let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
|
||||
let (_, mut tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
|
||||
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
|
||||
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
|
||||
let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
|
||||
|
||||
tab_snapshot.max_expansion_column = max_expansion_column;
|
||||
assert_eq!(tab_snapshot.text(), output);
|
||||
@ -615,6 +599,7 @@ mod tests {
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.map(|c| c.text)
|
||||
.collect::<String>(),
|
||||
@ -626,16 +611,16 @@ mod tests {
|
||||
let input_point = Point::new(0, ix as u32);
|
||||
let output_point = Point::new(0, output.find(c).unwrap() as u32);
|
||||
assert_eq!(
|
||||
tab_snapshot.to_tab_point(SuggestionPoint(input_point)),
|
||||
tab_snapshot.to_tab_point(FoldPoint(input_point)),
|
||||
TabPoint(output_point),
|
||||
"to_tab_point({input_point:?})"
|
||||
);
|
||||
assert_eq!(
|
||||
tab_snapshot
|
||||
.to_suggestion_point(TabPoint(output_point), Bias::Left)
|
||||
.to_fold_point(TabPoint(output_point), Bias::Left)
|
||||
.0,
|
||||
SuggestionPoint(input_point),
|
||||
"to_suggestion_point({output_point:?})"
|
||||
FoldPoint(input_point),
|
||||
"to_fold_point({output_point:?})"
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -648,9 +633,9 @@ mod tests {
|
||||
|
||||
let buffer = MultiBuffer::build_simple(input, cx);
|
||||
let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||
let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
|
||||
let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
|
||||
let (_, mut tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
|
||||
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
|
||||
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
|
||||
let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
|
||||
|
||||
tab_snapshot.max_expansion_column = max_expansion_column;
|
||||
assert_eq!(tab_snapshot.text(), input);
|
||||
@ -662,9 +647,9 @@ mod tests {
|
||||
|
||||
let buffer = MultiBuffer::build_simple(&input, cx);
|
||||
let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||
let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
|
||||
let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
|
||||
let (_, tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
|
||||
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
|
||||
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
|
||||
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
|
||||
|
||||
assert_eq!(
|
||||
chunks(&tab_snapshot, TabPoint::zero()),
|
||||
@ -689,7 +674,7 @@ mod tests {
|
||||
let mut chunks = Vec::new();
|
||||
let mut was_tab = false;
|
||||
let mut text = String::new();
|
||||
for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None) {
|
||||
for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None, None) {
|
||||
if chunk.is_tab != was_tab {
|
||||
if !text.is_empty() {
|
||||
chunks.push((mem::take(&mut text), was_tab));
|
||||
@ -721,15 +706,16 @@ mod tests {
|
||||
let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||
log::info!("Buffer text: {:?}", buffer_snapshot.text());
|
||||
|
||||
let (mut fold_map, _) = FoldMap::new(buffer_snapshot.clone());
|
||||
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
|
||||
log::info!("InlayMap text: {:?}", inlay_snapshot.text());
|
||||
let (mut fold_map, _) = FoldMap::new(inlay_snapshot.clone());
|
||||
fold_map.randomly_mutate(&mut rng);
|
||||
let (fold_snapshot, _) = fold_map.read(buffer_snapshot, vec![]);
|
||||
let (fold_snapshot, _) = fold_map.read(inlay_snapshot, vec![]);
|
||||
log::info!("FoldMap text: {:?}", fold_snapshot.text());
|
||||
let (suggestion_map, _) = SuggestionMap::new(fold_snapshot);
|
||||
let (suggestion_snapshot, _) = suggestion_map.randomly_mutate(&mut rng);
|
||||
log::info!("SuggestionMap text: {:?}", suggestion_snapshot.text());
|
||||
let (inlay_snapshot, _) = inlay_map.randomly_mutate(&mut 0, &mut rng);
|
||||
log::info!("InlayMap text: {:?}", inlay_snapshot.text());
|
||||
|
||||
let (tab_map, _) = TabMap::new(suggestion_snapshot.clone(), tab_size);
|
||||
let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
|
||||
let tabs_snapshot = tab_map.set_max_expansion_column(32);
|
||||
|
||||
let text = text::Rope::from(tabs_snapshot.text().as_str());
|
||||
@ -757,7 +743,7 @@ mod tests {
|
||||
let expected_summary = TextSummary::from(expected_text.as_str());
|
||||
assert_eq!(
|
||||
tabs_snapshot
|
||||
.chunks(start..end, false, None, None)
|
||||
.chunks(start..end, false, None, None, None)
|
||||
.map(|c| c.text)
|
||||
.collect::<String>(),
|
||||
expected_text,
|
||||
@ -767,7 +753,7 @@ mod tests {
|
||||
);
|
||||
|
||||
let mut actual_summary = tabs_snapshot.text_summary_for_range(start..end);
|
||||
if tab_size.get() > 1 && suggestion_snapshot.text().contains('\t') {
|
||||
if tab_size.get() > 1 && inlay_snapshot.text().contains('\t') {
|
||||
actual_summary.longest_row = expected_summary.longest_row;
|
||||
actual_summary.longest_row_chars = expected_summary.longest_row_chars;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
use super::{
|
||||
suggestion_map::SuggestionBufferRows,
|
||||
fold_map::FoldBufferRows,
|
||||
tab_map::{self, TabEdit, TabPoint, TabSnapshot},
|
||||
TextHighlights,
|
||||
};
|
||||
@ -65,7 +65,7 @@ pub struct WrapChunks<'a> {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WrapBufferRows<'a> {
|
||||
input_buffer_rows: SuggestionBufferRows<'a>,
|
||||
input_buffer_rows: FoldBufferRows<'a>,
|
||||
input_buffer_row: Option<u32>,
|
||||
output_row: u32,
|
||||
soft_wrapped: bool,
|
||||
@ -446,6 +446,7 @@ impl WrapSnapshot {
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let mut edit_transforms = Vec::<Transform>::new();
|
||||
for _ in edit.new_rows.start..edit.new_rows.end {
|
||||
@ -575,7 +576,8 @@ impl WrapSnapshot {
|
||||
rows: Range<u32>,
|
||||
language_aware: bool,
|
||||
text_highlights: Option<&'a TextHighlights>,
|
||||
suggestion_highlight: Option<HighlightStyle>,
|
||||
hint_highlights: Option<HighlightStyle>,
|
||||
suggestion_highlights: Option<HighlightStyle>,
|
||||
) -> WrapChunks<'a> {
|
||||
let output_start = WrapPoint::new(rows.start, 0);
|
||||
let output_end = WrapPoint::new(rows.end, 0);
|
||||
@ -593,7 +595,8 @@ impl WrapSnapshot {
|
||||
input_start..input_end,
|
||||
language_aware,
|
||||
text_highlights,
|
||||
suggestion_highlight,
|
||||
hint_highlights,
|
||||
suggestion_highlights,
|
||||
),
|
||||
input_chunk: Default::default(),
|
||||
output_position: output_start,
|
||||
@ -757,28 +760,18 @@ impl WrapSnapshot {
|
||||
}
|
||||
|
||||
let text = language::Rope::from(self.text().as_str());
|
||||
let input_buffer_rows = self.buffer_snapshot().buffer_rows(0).collect::<Vec<_>>();
|
||||
let mut input_buffer_rows = self.tab_snapshot.buffer_rows(0);
|
||||
let mut expected_buffer_rows = Vec::new();
|
||||
let mut prev_fold_row = 0;
|
||||
let mut prev_tab_row = 0;
|
||||
for display_row in 0..=self.max_point().row() {
|
||||
let tab_point = self.to_tab_point(WrapPoint::new(display_row, 0));
|
||||
let suggestion_point = self
|
||||
.tab_snapshot
|
||||
.to_suggestion_point(tab_point, Bias::Left)
|
||||
.0;
|
||||
let fold_point = self
|
||||
.tab_snapshot
|
||||
.suggestion_snapshot
|
||||
.to_fold_point(suggestion_point);
|
||||
if fold_point.row() == prev_fold_row && display_row != 0 {
|
||||
if tab_point.row() == prev_tab_row && display_row != 0 {
|
||||
expected_buffer_rows.push(None);
|
||||
} else {
|
||||
let buffer_point = fold_point
|
||||
.to_buffer_point(&self.tab_snapshot.suggestion_snapshot.fold_snapshot);
|
||||
expected_buffer_rows.push(input_buffer_rows[buffer_point.row as usize]);
|
||||
prev_fold_row = fold_point.row();
|
||||
expected_buffer_rows.push(input_buffer_rows.next().unwrap());
|
||||
}
|
||||
|
||||
prev_tab_row = tab_point.row();
|
||||
assert_eq!(self.line_len(display_row), text.line_len(display_row));
|
||||
}
|
||||
|
||||
@ -1038,7 +1031,7 @@ fn consolidate_wrap_edits(edits: &mut Vec<WrapEdit>) {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
display_map::{fold_map::FoldMap, suggestion_map::SuggestionMap, tab_map::TabMap},
|
||||
display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap},
|
||||
MultiBuffer,
|
||||
};
|
||||
use gpui::test::observe;
|
||||
@ -1089,11 +1082,11 @@ mod tests {
|
||||
});
|
||||
let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
|
||||
log::info!("Buffer text: {:?}", buffer_snapshot.text());
|
||||
let (mut fold_map, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
|
||||
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
|
||||
log::info!("InlayMap text: {:?}", inlay_snapshot.text());
|
||||
let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone());
|
||||
log::info!("FoldMap text: {:?}", fold_snapshot.text());
|
||||
let (suggestion_map, suggestion_snapshot) = SuggestionMap::new(fold_snapshot.clone());
|
||||
log::info!("SuggestionMap text: {:?}", suggestion_snapshot.text());
|
||||
let (tab_map, _) = TabMap::new(suggestion_snapshot.clone(), tab_size);
|
||||
let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
|
||||
let tabs_snapshot = tab_map.set_max_expansion_column(32);
|
||||
log::info!("TabMap text: {:?}", tabs_snapshot.text());
|
||||
|
||||
@ -1122,6 +1115,7 @@ mod tests {
|
||||
);
|
||||
log::info!("Wrapped text: {:?}", actual_text);
|
||||
|
||||
let mut next_inlay_id = 0;
|
||||
let mut edits = Vec::new();
|
||||
for _i in 0..operations {
|
||||
log::info!("{} ==============================================", _i);
|
||||
@ -1139,10 +1133,8 @@ mod tests {
|
||||
}
|
||||
20..=39 => {
|
||||
for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
|
||||
let (suggestion_snapshot, suggestion_edits) =
|
||||
suggestion_map.sync(fold_snapshot, fold_edits);
|
||||
let (tabs_snapshot, tab_edits) =
|
||||
tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
|
||||
tab_map.sync(fold_snapshot, fold_edits, tab_size);
|
||||
let (mut snapshot, wrap_edits) =
|
||||
wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
|
||||
snapshot.check_invariants();
|
||||
@ -1151,10 +1143,11 @@ mod tests {
|
||||
}
|
||||
}
|
||||
40..=59 => {
|
||||
let (suggestion_snapshot, suggestion_edits) =
|
||||
suggestion_map.randomly_mutate(&mut rng);
|
||||
let (inlay_snapshot, inlay_edits) =
|
||||
inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
|
||||
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
|
||||
let (tabs_snapshot, tab_edits) =
|
||||
tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
|
||||
tab_map.sync(fold_snapshot, fold_edits, tab_size);
|
||||
let (mut snapshot, wrap_edits) =
|
||||
wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
|
||||
snapshot.check_invariants();
|
||||
@ -1173,13 +1166,12 @@ mod tests {
|
||||
}
|
||||
|
||||
log::info!("Buffer text: {:?}", buffer_snapshot.text());
|
||||
let (fold_snapshot, fold_edits) = fold_map.read(buffer_snapshot.clone(), buffer_edits);
|
||||
let (inlay_snapshot, inlay_edits) =
|
||||
inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
|
||||
log::info!("InlayMap text: {:?}", inlay_snapshot.text());
|
||||
let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
|
||||
log::info!("FoldMap text: {:?}", fold_snapshot.text());
|
||||
let (suggestion_snapshot, suggestion_edits) =
|
||||
suggestion_map.sync(fold_snapshot, fold_edits);
|
||||
log::info!("SuggestionMap text: {:?}", suggestion_snapshot.text());
|
||||
let (tabs_snapshot, tab_edits) =
|
||||
tab_map.sync(suggestion_snapshot, suggestion_edits, tab_size);
|
||||
let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
|
||||
log::info!("TabMap text: {:?}", tabs_snapshot.text());
|
||||
|
||||
let unwrapped_text = tabs_snapshot.text();
|
||||
@ -1227,7 +1219,7 @@ mod tests {
|
||||
if tab_size.get() == 1
|
||||
|| !wrapped_snapshot
|
||||
.tab_snapshot
|
||||
.suggestion_snapshot
|
||||
.fold_snapshot
|
||||
.text()
|
||||
.contains('\t')
|
||||
{
|
||||
@ -1328,8 +1320,14 @@ mod tests {
|
||||
}
|
||||
|
||||
pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator<Item = &str> {
|
||||
self.chunks(wrap_row..self.max_point().row() + 1, false, None, None)
|
||||
.map(|h| h.text)
|
||||
self.chunks(
|
||||
wrap_row..self.max_point().row() + 1,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.map(|h| h.text)
|
||||
}
|
||||
|
||||
fn verify_chunks(&mut self, rng: &mut impl Rng) {
|
||||
@ -1352,7 +1350,7 @@ mod tests {
|
||||
}
|
||||
|
||||
let actual_text = self
|
||||
.chunks(start_row..end_row, true, None, None)
|
||||
.chunks(start_row..end_row, true, None, None, None)
|
||||
.map(|c| c.text)
|
||||
.collect::<String>();
|
||||
assert_eq!(
|
||||
|
@ -2,6 +2,7 @@ mod blink_manager;
|
||||
pub mod display_map;
|
||||
mod editor_settings;
|
||||
mod element;
|
||||
mod inlay_hint_cache;
|
||||
|
||||
mod git;
|
||||
mod highlight_matching_bracket;
|
||||
@ -52,11 +53,12 @@ use gpui::{
|
||||
};
|
||||
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
||||
use hover_popover::{hide_hover, HoverState};
|
||||
use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
|
||||
pub use items::MAX_TAB_TITLE_LEN;
|
||||
use itertools::Itertools;
|
||||
pub use language::{char_kind, CharKind};
|
||||
use language::{
|
||||
language_settings::{self, all_language_settings},
|
||||
language_settings::{self, all_language_settings, InlayHintSettings},
|
||||
AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape,
|
||||
Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, OffsetRangeExt,
|
||||
OffsetUtf16, Point, Selection, SelectionGoal, TransactionId,
|
||||
@ -64,11 +66,12 @@ use language::{
|
||||
use link_go_to_definition::{
|
||||
hide_link_definition, show_link_definition, LinkDefinitionKind, LinkGoToDefinitionState,
|
||||
};
|
||||
use log::error;
|
||||
use multi_buffer::ToOffsetUtf16;
|
||||
pub use multi_buffer::{
|
||||
Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset,
|
||||
ToPoint,
|
||||
};
|
||||
use multi_buffer::{MultiBufferChunks, ToOffsetUtf16};
|
||||
use ordered_float::OrderedFloat;
|
||||
use project::{FormatTrigger, Location, LocationLink, Project, ProjectPath, ProjectTransaction};
|
||||
use scroll::{
|
||||
@ -85,12 +88,13 @@ use std::{
|
||||
cmp::{self, Ordering, Reverse},
|
||||
mem,
|
||||
num::NonZeroU32,
|
||||
ops::{Deref, DerefMut, Range},
|
||||
ops::{ControlFlow, Deref, DerefMut, Range},
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
pub use sum_tree::Bias;
|
||||
use text::Rope;
|
||||
use theme::{DiagnosticStyle, Theme, ThemeSettings};
|
||||
use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
|
||||
use workspace::{ItemNavHistory, ViewId, Workspace};
|
||||
@ -180,6 +184,12 @@ pub struct GutterHover {
|
||||
pub hovered: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum InlayId {
|
||||
Suggestion(usize),
|
||||
Hint(usize),
|
||||
}
|
||||
|
||||
actions!(
|
||||
editor,
|
||||
[
|
||||
@ -535,6 +545,8 @@ pub struct Editor {
|
||||
gutter_hovered: bool,
|
||||
link_go_to_definition_state: LinkGoToDefinitionState,
|
||||
copilot_state: CopilotState,
|
||||
inlay_hint_cache: InlayHintCache,
|
||||
next_inlay_id: usize,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@ -1056,6 +1068,7 @@ pub struct CopilotState {
|
||||
cycled: bool,
|
||||
completions: Vec<copilot::Completion>,
|
||||
active_completion_index: usize,
|
||||
suggestion: Option<Inlay>,
|
||||
}
|
||||
|
||||
impl Default for CopilotState {
|
||||
@ -1067,6 +1080,7 @@ impl Default for CopilotState {
|
||||
completions: Default::default(),
|
||||
active_completion_index: 0,
|
||||
cycled: false,
|
||||
suggestion: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1181,6 +1195,14 @@ enum GotoDefinitionKind {
|
||||
Type,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
enum InlayRefreshReason {
|
||||
SettingsChange(InlayHintSettings),
|
||||
NewLinesShown,
|
||||
ExcerptEdited,
|
||||
RefreshRequested,
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
pub fn single_line(
|
||||
field_editor_style: Option<Arc<GetFieldEditorTheme>>,
|
||||
@ -1282,15 +1304,28 @@ impl Editor {
|
||||
let soft_wrap_mode_override =
|
||||
(mode == EditorMode::SingleLine).then(|| language_settings::SoftWrap::None);
|
||||
|
||||
let mut project_subscription = None;
|
||||
if mode == EditorMode::Full && buffer.read(cx).is_singleton() {
|
||||
let mut project_subscriptions = Vec::new();
|
||||
if mode == EditorMode::Full {
|
||||
if let Some(project) = project.as_ref() {
|
||||
project_subscription = Some(cx.observe(project, |_, _, cx| {
|
||||
cx.emit(Event::TitleChanged);
|
||||
}))
|
||||
if buffer.read(cx).is_singleton() {
|
||||
project_subscriptions.push(cx.observe(project, |_, _, cx| {
|
||||
cx.emit(Event::TitleChanged);
|
||||
}));
|
||||
}
|
||||
project_subscriptions.push(cx.subscribe(project, |editor, _, event, cx| {
|
||||
if let project::Event::RefreshInlays = event {
|
||||
editor.refresh_inlays(InlayRefreshReason::RefreshRequested, cx);
|
||||
};
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
let inlay_hint_settings = inlay_hint_settings(
|
||||
selections.newest_anchor().head(),
|
||||
&buffer.read(cx).snapshot(cx),
|
||||
cx,
|
||||
);
|
||||
|
||||
let mut this = Self {
|
||||
handle: cx.weak_handle(),
|
||||
buffer: buffer.clone(),
|
||||
@ -1324,6 +1359,7 @@ impl Editor {
|
||||
.add_view(|cx| context_menu::ContextMenu::new(editor_view_id, cx)),
|
||||
completion_tasks: Default::default(),
|
||||
next_completion_id: 0,
|
||||
next_inlay_id: 0,
|
||||
available_code_actions: Default::default(),
|
||||
code_actions_task: Default::default(),
|
||||
document_highlights_task: Default::default(),
|
||||
@ -1340,6 +1376,7 @@ impl Editor {
|
||||
hover_state: Default::default(),
|
||||
link_go_to_definition_state: Default::default(),
|
||||
copilot_state: Default::default(),
|
||||
inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
|
||||
gutter_hovered: false,
|
||||
_subscriptions: vec![
|
||||
cx.observe(&buffer, Self::on_buffer_changed),
|
||||
@ -1350,9 +1387,7 @@ impl Editor {
|
||||
],
|
||||
};
|
||||
|
||||
if let Some(project_subscription) = project_subscription {
|
||||
this._subscriptions.push(project_subscription);
|
||||
}
|
||||
this._subscriptions.extend(project_subscriptions);
|
||||
|
||||
this.end_selection(cx);
|
||||
this.scroll_manager.show_scrollbar(cx);
|
||||
@ -1873,7 +1908,7 @@ impl Editor {
|
||||
s.set_pending(pending, mode);
|
||||
});
|
||||
} else {
|
||||
log::error!("update_selection dispatched with no pending selection");
|
||||
error!("update_selection dispatched with no pending selection");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -2577,6 +2612,106 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn refresh_inlays(&mut self, reason: InlayRefreshReason, cx: &mut ViewContext<Self>) {
|
||||
if self.project.is_none() || self.mode != EditorMode::Full {
|
||||
return;
|
||||
}
|
||||
|
||||
let invalidate_cache = match reason {
|
||||
InlayRefreshReason::SettingsChange(new_settings) => {
|
||||
match self.inlay_hint_cache.update_settings(
|
||||
&self.buffer,
|
||||
new_settings,
|
||||
self.visible_inlay_hints(cx),
|
||||
cx,
|
||||
) {
|
||||
ControlFlow::Break(Some(InlaySplice {
|
||||
to_remove,
|
||||
to_insert,
|
||||
})) => {
|
||||
self.splice_inlay_hints(to_remove, to_insert, cx);
|
||||
return;
|
||||
}
|
||||
ControlFlow::Break(None) => return,
|
||||
ControlFlow::Continue(()) => InvalidationStrategy::RefreshRequested,
|
||||
}
|
||||
}
|
||||
InlayRefreshReason::NewLinesShown => InvalidationStrategy::None,
|
||||
InlayRefreshReason::ExcerptEdited => InvalidationStrategy::ExcerptEdited,
|
||||
InlayRefreshReason::RefreshRequested => InvalidationStrategy::RefreshRequested,
|
||||
};
|
||||
|
||||
self.inlay_hint_cache.refresh_inlay_hints(
|
||||
self.excerpt_visible_offsets(cx),
|
||||
invalidate_cache,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
fn visible_inlay_hints(&self, cx: &ViewContext<'_, '_, Editor>) -> Vec<Inlay> {
|
||||
self.display_map
|
||||
.read(cx)
|
||||
.current_inlays()
|
||||
.filter(move |inlay| {
|
||||
Some(inlay.id) != self.copilot_state.suggestion.as_ref().map(|h| h.id)
|
||||
})
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn excerpt_visible_offsets(
|
||||
&self,
|
||||
cx: &mut ViewContext<'_, '_, Editor>,
|
||||
) -> HashMap<ExcerptId, (ModelHandle<Buffer>, Range<usize>)> {
|
||||
let multi_buffer = self.buffer().read(cx);
|
||||
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
|
||||
let multi_buffer_visible_start = self
|
||||
.scroll_manager
|
||||
.anchor()
|
||||
.anchor
|
||||
.to_point(&multi_buffer_snapshot);
|
||||
let multi_buffer_visible_end = multi_buffer_snapshot.clip_point(
|
||||
multi_buffer_visible_start
|
||||
+ Point::new(self.visible_line_count().unwrap_or(0.).ceil() as u32, 0),
|
||||
Bias::Left,
|
||||
);
|
||||
let multi_buffer_visible_range = multi_buffer_visible_start..multi_buffer_visible_end;
|
||||
multi_buffer
|
||||
.range_to_buffer_ranges(multi_buffer_visible_range, cx)
|
||||
.into_iter()
|
||||
.filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
|
||||
.map(|(buffer, excerpt_visible_range, excerpt_id)| {
|
||||
(excerpt_id, (buffer, excerpt_visible_range))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn splice_inlay_hints(
|
||||
&self,
|
||||
to_remove: Vec<InlayId>,
|
||||
to_insert: Vec<(Anchor, InlayId, project::InlayHint)>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let buffer = self.buffer.read(cx).read(cx);
|
||||
let new_inlays = to_insert
|
||||
.into_iter()
|
||||
.map(|(position, id, hint)| {
|
||||
let mut text = hint.text();
|
||||
if hint.padding_right {
|
||||
text.push(' ');
|
||||
}
|
||||
if hint.padding_left {
|
||||
text.insert(0, ' ');
|
||||
}
|
||||
(id, InlayProperties { position, text })
|
||||
})
|
||||
.collect();
|
||||
drop(buffer);
|
||||
self.display_map.update(cx, |display_map, cx| {
|
||||
display_map.splice_inlays(to_remove, new_inlays, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn trigger_on_type_formatting(
|
||||
&self,
|
||||
input: String,
|
||||
@ -3227,10 +3362,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||
if let Some(suggestion) = self
|
||||
.display_map
|
||||
.update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx))
|
||||
{
|
||||
if let Some(suggestion) = self.take_active_copilot_suggestion(cx) {
|
||||
if let Some((copilot, completion)) =
|
||||
Copilot::global(cx).zip(self.copilot_state.active_completion())
|
||||
{
|
||||
@ -3249,7 +3381,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
fn discard_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
|
||||
if self.has_active_copilot_suggestion(cx) {
|
||||
if let Some(suggestion) = self.take_active_copilot_suggestion(cx) {
|
||||
if let Some(copilot) = Copilot::global(cx) {
|
||||
copilot
|
||||
.update(cx, |copilot, cx| {
|
||||
@ -3260,8 +3392,9 @@ impl Editor {
|
||||
self.report_copilot_event(None, false, cx)
|
||||
}
|
||||
|
||||
self.display_map
|
||||
.update(cx, |map, cx| map.replace_suggestion::<usize>(None, cx));
|
||||
self.display_map.update(cx, |map, cx| {
|
||||
map.splice_inlays::<&str>(vec![suggestion.id], Vec::new(), cx)
|
||||
});
|
||||
cx.notify();
|
||||
true
|
||||
} else {
|
||||
@ -3282,7 +3415,26 @@ impl Editor {
|
||||
}
|
||||
|
||||
fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
|
||||
self.display_map.read(cx).has_suggestion()
|
||||
if let Some(suggestion) = self.copilot_state.suggestion.as_ref() {
|
||||
let buffer = self.buffer.read(cx).read(cx);
|
||||
suggestion.position.is_valid(&buffer)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn take_active_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<Inlay> {
|
||||
let suggestion = self.copilot_state.suggestion.take()?;
|
||||
self.display_map.update(cx, |map, cx| {
|
||||
map.splice_inlays::<&str>(vec![suggestion.id], Default::default(), cx);
|
||||
});
|
||||
let buffer = self.buffer.read(cx).read(cx);
|
||||
|
||||
if suggestion.position.is_valid(&buffer) {
|
||||
Some(suggestion)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn update_visible_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) {
|
||||
@ -3299,14 +3451,27 @@ impl Editor {
|
||||
.copilot_state
|
||||
.text_for_active_completion(cursor, &snapshot)
|
||||
{
|
||||
let text = Rope::from(text);
|
||||
let mut to_remove = Vec::new();
|
||||
if let Some(suggestion) = self.copilot_state.suggestion.take() {
|
||||
to_remove.push(suggestion.id);
|
||||
}
|
||||
|
||||
let suggestion_inlay_id = InlayId::Suggestion(post_inc(&mut self.next_inlay_id));
|
||||
let to_insert = vec![(
|
||||
suggestion_inlay_id,
|
||||
InlayProperties {
|
||||
position: cursor,
|
||||
text: text.clone(),
|
||||
},
|
||||
)];
|
||||
self.display_map.update(cx, move |map, cx| {
|
||||
map.replace_suggestion(
|
||||
Some(Suggestion {
|
||||
position: cursor,
|
||||
text: text.trim_end().into(),
|
||||
}),
|
||||
cx,
|
||||
)
|
||||
map.splice_inlays(to_remove, to_insert, cx)
|
||||
});
|
||||
self.copilot_state.suggestion = Some(Inlay {
|
||||
id: suggestion_inlay_id,
|
||||
position: cursor,
|
||||
text,
|
||||
});
|
||||
cx.notify();
|
||||
} else {
|
||||
@ -6641,7 +6806,7 @@ impl Editor {
|
||||
if let Some((_, end_selections)) = self.selection_history.transaction_mut(tx_id) {
|
||||
*end_selections = Some(self.selections.disjoint_anchors());
|
||||
} else {
|
||||
log::error!("unexpectedly ended a transaction that wasn't started by this editor");
|
||||
error!("unexpectedly ended a transaction that wasn't started by this editor");
|
||||
}
|
||||
|
||||
cx.emit(Event::Edited);
|
||||
@ -7103,6 +7268,7 @@ impl Editor {
|
||||
self.update_visible_copilot_suggestion(cx);
|
||||
}
|
||||
cx.emit(Event::BufferEdited);
|
||||
self.refresh_inlays(InlayRefreshReason::ExcerptEdited, cx);
|
||||
}
|
||||
multi_buffer::Event::ExcerptsAdded {
|
||||
buffer,
|
||||
@ -7127,7 +7293,7 @@ impl Editor {
|
||||
self.refresh_active_diagnostics(cx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn on_display_map_changed(&mut self, _: ModelHandle<DisplayMap>, cx: &mut ViewContext<Self>) {
|
||||
@ -7136,6 +7302,14 @@ impl Editor {
|
||||
|
||||
fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.refresh_copilot_suggestions(true, cx);
|
||||
self.refresh_inlays(
|
||||
InlayRefreshReason::SettingsChange(inlay_hint_settings(
|
||||
self.selections.newest_anchor().head(),
|
||||
&self.buffer.read(cx).snapshot(cx),
|
||||
cx,
|
||||
)),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn set_searchable(&mut self, searchable: bool) {
|
||||
@ -7425,6 +7599,23 @@ impl Editor {
|
||||
let Some(lines) = serde_json::to_string_pretty(&lines).log_err() else { return; };
|
||||
cx.write_to_clipboard(ClipboardItem::new(lines));
|
||||
}
|
||||
|
||||
pub fn inlay_hint_cache(&self) -> &InlayHintCache {
|
||||
&self.inlay_hint_cache
|
||||
}
|
||||
}
|
||||
|
||||
fn inlay_hint_settings(
|
||||
location: Anchor,
|
||||
snapshot: &MultiBufferSnapshot,
|
||||
cx: &mut ViewContext<'_, '_, Editor>,
|
||||
) -> InlayHintSettings {
|
||||
let file = snapshot.file_at(location);
|
||||
let language = snapshot.language_at(location);
|
||||
let settings = all_language_settings(file, cx);
|
||||
settings
|
||||
.language(language.map(|l| l.name()).as_deref())
|
||||
.inlay_hints
|
||||
}
|
||||
|
||||
fn consume_contiguous_rows(
|
||||
|
@ -1392,7 +1392,12 @@ impl EditorElement {
|
||||
} else {
|
||||
let style = &self.style;
|
||||
let chunks = snapshot
|
||||
.chunks(rows.clone(), true, Some(style.theme.suggestion))
|
||||
.chunks(
|
||||
rows.clone(),
|
||||
true,
|
||||
Some(style.theme.hint),
|
||||
Some(style.theme.suggestion),
|
||||
)
|
||||
.map(|chunk| {
|
||||
let mut highlight_style = chunk
|
||||
.syntax_highlight_id
|
||||
@ -1921,7 +1926,7 @@ impl Element<Editor> for EditorElement {
|
||||
let em_advance = style.text.em_advance(cx.font_cache());
|
||||
let overscroll = vec2f(em_width, 0.);
|
||||
let snapshot = {
|
||||
editor.set_visible_line_count(size.y() / line_height);
|
||||
editor.set_visible_line_count(size.y() / line_height, cx);
|
||||
|
||||
let editor_width = text_width - gutter_margin - overscroll.x() - em_width;
|
||||
let wrap_width = match editor.soft_wrap_mode(cx) {
|
||||
|
2021
crates/editor/src/inlay_hint_cache.rs
Normal file
2021
crates/editor/src/inlay_hint_cache.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -49,6 +49,10 @@ impl Anchor {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bias(&self) -> Bias {
|
||||
self.text_anchor.bias
|
||||
}
|
||||
|
||||
pub fn bias_left(&self, snapshot: &MultiBufferSnapshot) -> Anchor {
|
||||
if self.text_anchor.bias != Bias::Left {
|
||||
if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
|
||||
@ -81,6 +85,19 @@ impl Anchor {
|
||||
{
|
||||
snapshot.summary_for_anchor(self)
|
||||
}
|
||||
|
||||
pub fn is_valid(&self, snapshot: &MultiBufferSnapshot) -> bool {
|
||||
if *self == Anchor::min() || *self == Anchor::max() {
|
||||
true
|
||||
} else if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
|
||||
excerpt.contains(self)
|
||||
&& (self.text_anchor == excerpt.range.context.start
|
||||
|| self.text_anchor == excerpt.range.context.end
|
||||
|| self.text_anchor.is_valid(&excerpt.buffer))
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ToOffset for Anchor {
|
||||
|
@ -13,13 +13,14 @@ use gpui::{
|
||||
};
|
||||
use language::{Bias, Point};
|
||||
use util::ResultExt;
|
||||
use workspace::WorkspaceId;
|
||||
use workspace::{item::Item, WorkspaceId};
|
||||
|
||||
use crate::{
|
||||
display_map::{DisplaySnapshot, ToDisplayPoint},
|
||||
hover_popover::hide_hover,
|
||||
persistence::DB,
|
||||
Anchor, DisplayPoint, Editor, EditorMode, Event, MultiBufferSnapshot, ToPoint,
|
||||
Anchor, DisplayPoint, Editor, EditorMode, Event, InlayRefreshReason, MultiBufferSnapshot,
|
||||
ToPoint,
|
||||
};
|
||||
|
||||
use self::{
|
||||
@ -293,8 +294,19 @@ impl Editor {
|
||||
self.scroll_manager.visible_line_count
|
||||
}
|
||||
|
||||
pub(crate) fn set_visible_line_count(&mut self, lines: f32) {
|
||||
self.scroll_manager.visible_line_count = Some(lines)
|
||||
pub(crate) fn set_visible_line_count(&mut self, lines: f32, cx: &mut ViewContext<Self>) {
|
||||
let opened_first_time = self.scroll_manager.visible_line_count.is_none();
|
||||
self.scroll_manager.visible_line_count = Some(lines);
|
||||
if opened_first_time {
|
||||
cx.spawn(|editor, mut cx| async move {
|
||||
editor
|
||||
.update(&mut cx, |editor, cx| {
|
||||
editor.refresh_inlays(InlayRefreshReason::NewLinesShown, cx)
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.detach()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext<Self>) {
|
||||
@ -320,6 +332,10 @@ impl Editor {
|
||||
workspace_id,
|
||||
cx,
|
||||
);
|
||||
|
||||
if !self.is_singleton(cx) {
|
||||
self.refresh_inlays(InlayRefreshReason::NewLinesShown, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {
|
||||
|
@ -388,6 +388,7 @@ struct FakeFsState {
|
||||
event_txs: Vec<smol::channel::Sender<Vec<fsevent::Event>>>,
|
||||
events_paused: bool,
|
||||
buffered_events: Vec<fsevent::Event>,
|
||||
metadata_call_count: usize,
|
||||
read_dir_call_count: usize,
|
||||
}
|
||||
|
||||
@ -538,6 +539,7 @@ impl FakeFs {
|
||||
buffered_events: Vec::new(),
|
||||
events_paused: false,
|
||||
read_dir_call_count: 0,
|
||||
metadata_call_count: 0,
|
||||
}),
|
||||
})
|
||||
}
|
||||
@ -774,10 +776,16 @@ impl FakeFs {
|
||||
result
|
||||
}
|
||||
|
||||
/// How many `read_dir` calls have been issued.
|
||||
pub fn read_dir_call_count(&self) -> usize {
|
||||
self.state.lock().read_dir_call_count
|
||||
}
|
||||
|
||||
/// How many `metadata` calls have been issued.
|
||||
pub fn metadata_call_count(&self) -> usize {
|
||||
self.state.lock().metadata_call_count
|
||||
}
|
||||
|
||||
async fn simulate_random_delay(&self) {
|
||||
self.executor
|
||||
.upgrade()
|
||||
@ -1098,7 +1106,8 @@ impl Fs for FakeFs {
|
||||
async fn metadata(&self, path: &Path) -> Result<Option<Metadata>> {
|
||||
self.simulate_random_delay().await;
|
||||
let path = normalize_path(path);
|
||||
let state = self.state.lock();
|
||||
let mut state = self.state.lock();
|
||||
state.metadata_call_count += 1;
|
||||
if let Some((mut entry, _)) = state.try_read_path(&path, false) {
|
||||
let is_symlink = entry.lock().is_symlink();
|
||||
if is_symlink {
|
||||
|
@ -8,8 +8,8 @@ use crate::{
|
||||
MouseButton, MouseMovedEvent, PromptLevel, WindowBounds,
|
||||
},
|
||||
scene::{
|
||||
CursorRegion, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover,
|
||||
MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut, Scene,
|
||||
CursorRegion, MouseClick, MouseClickOut, MouseDown, MouseDownOut, MouseDrag, MouseEvent,
|
||||
MouseHover, MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut, Scene,
|
||||
},
|
||||
text_layout::TextLayoutCache,
|
||||
util::post_inc,
|
||||
@ -524,6 +524,10 @@ impl<'a> WindowContext<'a> {
|
||||
region: Default::default(),
|
||||
platform_event: e.clone(),
|
||||
}));
|
||||
mouse_events.push(MouseEvent::ClickOut(MouseClickOut {
|
||||
region: Default::default(),
|
||||
platform_event: e.clone(),
|
||||
}));
|
||||
}
|
||||
|
||||
Event::MouseMoved(
|
||||
@ -712,7 +716,10 @@ impl<'a> WindowContext<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
MouseEvent::MoveOut(_) | MouseEvent::UpOut(_) | MouseEvent::DownOut(_) => {
|
||||
MouseEvent::MoveOut(_)
|
||||
| MouseEvent::UpOut(_)
|
||||
| MouseEvent::DownOut(_)
|
||||
| MouseEvent::ClickOut(_) => {
|
||||
for (mouse_region, _) in self.window.mouse_regions.iter().rev() {
|
||||
// NOT contains
|
||||
if !mouse_region
|
||||
|
@ -7,8 +7,8 @@ use crate::{
|
||||
platform::CursorStyle,
|
||||
platform::MouseButton,
|
||||
scene::{
|
||||
CursorRegion, HandlerSet, MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseHover,
|
||||
MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut,
|
||||
CursorRegion, HandlerSet, MouseClick, MouseClickOut, MouseDown, MouseDownOut, MouseDrag,
|
||||
MouseHover, MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut,
|
||||
},
|
||||
AnyElement, Element, EventContext, LayoutContext, MouseRegion, MouseState, SceneBuilder,
|
||||
SizeConstraint, View, ViewContext,
|
||||
@ -136,6 +136,15 @@ impl<Tag, V: View> MouseEventHandler<Tag, V> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click_out(
|
||||
mut self,
|
||||
button: MouseButton,
|
||||
handler: impl Fn(MouseClickOut, &mut V, &mut EventContext<V>) + 'static,
|
||||
) -> Self {
|
||||
self.handlers = self.handlers.on_click_out(button, handler);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_down_out(
|
||||
mut self,
|
||||
button: MouseButton,
|
||||
|
@ -99,6 +99,20 @@ impl Deref for MouseClick {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct MouseClickOut {
|
||||
pub region: RectF,
|
||||
pub platform_event: MouseButtonEvent,
|
||||
}
|
||||
|
||||
impl Deref for MouseClickOut {
|
||||
type Target = MouseButtonEvent;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.platform_event
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct MouseDownOut {
|
||||
pub region: RectF,
|
||||
@ -150,6 +164,7 @@ pub enum MouseEvent {
|
||||
Down(MouseDown),
|
||||
Up(MouseUp),
|
||||
Click(MouseClick),
|
||||
ClickOut(MouseClickOut),
|
||||
DownOut(MouseDownOut),
|
||||
UpOut(MouseUpOut),
|
||||
ScrollWheel(MouseScrollWheel),
|
||||
@ -165,6 +180,7 @@ impl MouseEvent {
|
||||
MouseEvent::Down(r) => r.region = region,
|
||||
MouseEvent::Up(r) => r.region = region,
|
||||
MouseEvent::Click(r) => r.region = region,
|
||||
MouseEvent::ClickOut(r) => r.region = region,
|
||||
MouseEvent::DownOut(r) => r.region = region,
|
||||
MouseEvent::UpOut(r) => r.region = region,
|
||||
MouseEvent::ScrollWheel(r) => r.region = region,
|
||||
@ -182,6 +198,7 @@ impl MouseEvent {
|
||||
MouseEvent::Down(_) => true,
|
||||
MouseEvent::Up(_) => true,
|
||||
MouseEvent::Click(_) => true,
|
||||
MouseEvent::ClickOut(_) => true,
|
||||
MouseEvent::DownOut(_) => false,
|
||||
MouseEvent::UpOut(_) => false,
|
||||
MouseEvent::ScrollWheel(_) => true,
|
||||
@ -222,6 +239,10 @@ impl MouseEvent {
|
||||
discriminant(&MouseEvent::Click(Default::default()))
|
||||
}
|
||||
|
||||
pub fn click_out_disc() -> Discriminant<MouseEvent> {
|
||||
discriminant(&MouseEvent::ClickOut(Default::default()))
|
||||
}
|
||||
|
||||
pub fn down_out_disc() -> Discriminant<MouseEvent> {
|
||||
discriminant(&MouseEvent::DownOut(Default::default()))
|
||||
}
|
||||
@ -239,6 +260,7 @@ impl MouseEvent {
|
||||
MouseEvent::Down(e) => HandlerKey::new(Self::down_disc(), Some(e.button)),
|
||||
MouseEvent::Up(e) => HandlerKey::new(Self::up_disc(), Some(e.button)),
|
||||
MouseEvent::Click(e) => HandlerKey::new(Self::click_disc(), Some(e.button)),
|
||||
MouseEvent::ClickOut(e) => HandlerKey::new(Self::click_out_disc(), Some(e.button)),
|
||||
MouseEvent::UpOut(e) => HandlerKey::new(Self::up_out_disc(), Some(e.button)),
|
||||
MouseEvent::DownOut(e) => HandlerKey::new(Self::down_out_disc(), Some(e.button)),
|
||||
MouseEvent::ScrollWheel(_) => HandlerKey::new(Self::scroll_wheel_disc(), None),
|
||||
|
@ -14,7 +14,7 @@ use super::{
|
||||
MouseClick, MouseDown, MouseDownOut, MouseDrag, MouseEvent, MouseHover, MouseMove, MouseUp,
|
||||
MouseUpOut,
|
||||
},
|
||||
MouseMoveOut, MouseScrollWheel,
|
||||
MouseClickOut, MouseMoveOut, MouseScrollWheel,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -89,6 +89,15 @@ impl MouseRegion {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
|
||||
where
|
||||
V: View,
|
||||
F: Fn(MouseClickOut, &mut V, &mut EventContext<V>) + 'static,
|
||||
{
|
||||
self.handlers = self.handlers.on_click_out(button, handler);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_down_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
|
||||
where
|
||||
V: View,
|
||||
@ -246,6 +255,10 @@ impl HandlerSet {
|
||||
HandlerKey::new(MouseEvent::click_disc(), Some(button)),
|
||||
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
|
||||
);
|
||||
set.insert(
|
||||
HandlerKey::new(MouseEvent::click_out_disc(), Some(button)),
|
||||
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
|
||||
);
|
||||
set.insert(
|
||||
HandlerKey::new(MouseEvent::down_out_disc(), Some(button)),
|
||||
SmallVec::from_buf([Rc::new(|_, _, _, _| true)]),
|
||||
@ -405,6 +418,28 @@ impl HandlerSet {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
|
||||
where
|
||||
V: View,
|
||||
F: Fn(MouseClickOut, &mut V, &mut EventContext<V>) + 'static,
|
||||
{
|
||||
self.insert(MouseEvent::click_out_disc(), Some(button),
|
||||
Rc::new(move |region_event, view, cx, view_id| {
|
||||
if let MouseEvent::ClickOut(e) = region_event {
|
||||
let view = view.downcast_mut().unwrap();
|
||||
let mut cx = ViewContext::mutable(cx, view_id);
|
||||
let mut cx = EventContext::new(&mut cx);
|
||||
handler(e, view, &mut cx);
|
||||
cx.handled
|
||||
} else {
|
||||
panic!(
|
||||
"Mouse Region Event incorrectly called with mismatched event type. Expected MouseRegionEvent::ClickOut, found {:?}",
|
||||
region_event);
|
||||
}
|
||||
}));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_down_out<V, F>(mut self, button: MouseButton, handler: F) -> Self
|
||||
where
|
||||
V: View,
|
||||
|
@ -20,7 +20,7 @@ use futures::{
|
||||
use gpui::{executor::Background, AppContext, AsyncAppContext, Task};
|
||||
use highlight_map::HighlightMap;
|
||||
use lazy_static::lazy_static;
|
||||
use lsp::CodeActionKind;
|
||||
use lsp::{CodeActionKind, LanguageServerBinary};
|
||||
use parking_lot::{Mutex, RwLock};
|
||||
use postage::watch;
|
||||
use regex::Regex;
|
||||
@ -30,7 +30,6 @@ use std::{
|
||||
any::Any,
|
||||
borrow::Cow,
|
||||
cell::RefCell,
|
||||
ffi::OsString,
|
||||
fmt::Debug,
|
||||
hash::Hash,
|
||||
mem,
|
||||
@ -86,12 +85,6 @@ pub trait ToLspPosition {
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct LanguageServerName(pub Arc<str>);
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct LanguageServerBinary {
|
||||
pub path: PathBuf,
|
||||
pub arguments: Vec<OsString>,
|
||||
}
|
||||
|
||||
/// Represents a Language Server, with certain cached sync properties.
|
||||
/// Uses [`LspAdapter`] under the hood, but calls all 'static' methods
|
||||
/// once at startup, and caches the results.
|
||||
@ -167,6 +160,17 @@ impl CachedLspAdapter {
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn can_be_reinstalled(&self) -> bool {
|
||||
self.adapter.can_be_reinstalled()
|
||||
}
|
||||
|
||||
pub async fn installation_test_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
self.adapter.installation_test_binary(container_dir).await
|
||||
}
|
||||
|
||||
pub fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
|
||||
self.adapter.code_action_kinds()
|
||||
}
|
||||
@ -249,6 +253,15 @@ pub trait LspAdapter: 'static + Send + Sync {
|
||||
delegate: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary>;
|
||||
|
||||
fn can_be_reinstalled(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
async fn installation_test_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
) -> Option<LanguageServerBinary>;
|
||||
|
||||
async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
|
||||
|
||||
async fn process_completion(&self, _: &mut lsp::CompletionItem) {}
|
||||
@ -576,7 +589,8 @@ struct LanguageRegistryState {
|
||||
|
||||
pub struct PendingLanguageServer {
|
||||
pub server_id: LanguageServerId,
|
||||
pub task: Task<Result<lsp::LanguageServer>>,
|
||||
pub task: Task<Result<Option<lsp::LanguageServer>>>,
|
||||
pub container_dir: Option<Arc<Path>>,
|
||||
}
|
||||
|
||||
impl LanguageRegistry {
|
||||
@ -848,7 +862,7 @@ impl LanguageRegistry {
|
||||
self.state.read().languages.iter().cloned().collect()
|
||||
}
|
||||
|
||||
pub fn start_language_server(
|
||||
pub fn create_pending_language_server(
|
||||
self: &Arc<Self>,
|
||||
language: Arc<Language>,
|
||||
adapter: Arc<CachedLspAdapter>,
|
||||
@ -858,7 +872,7 @@ impl LanguageRegistry {
|
||||
) -> Option<PendingLanguageServer> {
|
||||
let server_id = self.state.write().next_language_server_id();
|
||||
log::info!(
|
||||
"starting language server name:{}, path:{root_path:?}, id:{server_id}",
|
||||
"starting language server {:?}, path: {root_path:?}, id: {server_id}",
|
||||
adapter.name.0
|
||||
);
|
||||
|
||||
@ -888,66 +902,81 @@ impl LanguageRegistry {
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
Ok(server)
|
||||
|
||||
Ok(Some(server))
|
||||
});
|
||||
|
||||
return Some(PendingLanguageServer { server_id, task });
|
||||
return Some(PendingLanguageServer {
|
||||
server_id,
|
||||
task,
|
||||
container_dir: None,
|
||||
});
|
||||
}
|
||||
|
||||
let download_dir = self
|
||||
.language_server_download_dir
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow!("language server download directory has not been assigned"))
|
||||
.ok_or_else(|| anyhow!("language server download directory has not been assigned before starting server"))
|
||||
.log_err()?;
|
||||
let this = self.clone();
|
||||
let language = language.clone();
|
||||
let download_dir = download_dir.clone();
|
||||
let container_dir: Arc<Path> = Arc::from(download_dir.join(adapter.name.0.as_ref()));
|
||||
let root_path = root_path.clone();
|
||||
let adapter = adapter.clone();
|
||||
let lsp_binary_statuses = self.lsp_binary_statuses_tx.clone();
|
||||
let login_shell_env_loaded = self.login_shell_env_loaded.clone();
|
||||
|
||||
let task = cx.spawn(|mut cx| async move {
|
||||
login_shell_env_loaded.await;
|
||||
let task = {
|
||||
let container_dir = container_dir.clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
login_shell_env_loaded.await;
|
||||
|
||||
let entry = this
|
||||
.lsp_binary_paths
|
||||
.lock()
|
||||
.entry(adapter.name.clone())
|
||||
.or_insert_with(|| {
|
||||
cx.spawn(|cx| {
|
||||
get_binary(
|
||||
adapter.clone(),
|
||||
language.clone(),
|
||||
delegate.clone(),
|
||||
download_dir,
|
||||
lsp_binary_statuses,
|
||||
cx,
|
||||
)
|
||||
.map_err(Arc::new)
|
||||
let mut lock = this.lsp_binary_paths.lock();
|
||||
let entry = lock
|
||||
.entry(adapter.name.clone())
|
||||
.or_insert_with(|| {
|
||||
cx.spawn(|cx| {
|
||||
get_binary(
|
||||
adapter.clone(),
|
||||
language.clone(),
|
||||
delegate.clone(),
|
||||
container_dir,
|
||||
lsp_binary_statuses,
|
||||
cx,
|
||||
)
|
||||
.map_err(Arc::new)
|
||||
})
|
||||
.shared()
|
||||
})
|
||||
.shared()
|
||||
})
|
||||
.clone();
|
||||
let binary = entry.clone().map_err(|e| anyhow!(e)).await?;
|
||||
.clone();
|
||||
drop(lock);
|
||||
|
||||
if let Some(task) = adapter.will_start_server(&delegate, &mut cx) {
|
||||
task.await?;
|
||||
}
|
||||
let binary = match entry.clone().await.log_err() {
|
||||
Some(binary) => binary,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let server = lsp::LanguageServer::new(
|
||||
server_id,
|
||||
&binary.path,
|
||||
&binary.arguments,
|
||||
&root_path,
|
||||
adapter.code_action_kinds(),
|
||||
cx,
|
||||
)?;
|
||||
if let Some(task) = adapter.will_start_server(&delegate, &mut cx) {
|
||||
if task.await.log_err().is_none() {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(server)
|
||||
});
|
||||
Ok(Some(lsp::LanguageServer::new(
|
||||
server_id,
|
||||
binary,
|
||||
&root_path,
|
||||
adapter.code_action_kinds(),
|
||||
cx,
|
||||
)?))
|
||||
})
|
||||
};
|
||||
|
||||
Some(PendingLanguageServer { server_id, task })
|
||||
Some(PendingLanguageServer {
|
||||
server_id,
|
||||
task,
|
||||
container_dir: Some(container_dir),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn language_server_binary_statuses(
|
||||
@ -955,6 +984,30 @@ impl LanguageRegistry {
|
||||
) -> async_broadcast::Receiver<(Arc<Language>, LanguageServerBinaryStatus)> {
|
||||
self.lsp_binary_statuses_rx.clone()
|
||||
}
|
||||
|
||||
pub fn delete_server_container(
|
||||
&self,
|
||||
adapter: Arc<CachedLspAdapter>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<()> {
|
||||
log::info!("deleting server container");
|
||||
|
||||
let mut lock = self.lsp_binary_paths.lock();
|
||||
lock.remove(&adapter.name);
|
||||
|
||||
let download_dir = self
|
||||
.language_server_download_dir
|
||||
.clone()
|
||||
.expect("language server download directory has not been assigned before deleting server container");
|
||||
|
||||
cx.spawn(|_| async move {
|
||||
let container_dir = download_dir.join(adapter.name.0.as_ref());
|
||||
smol::fs::remove_dir_all(container_dir)
|
||||
.await
|
||||
.context("server container removal")
|
||||
.log_err();
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl LanguageRegistryState {
|
||||
@ -1005,11 +1058,10 @@ async fn get_binary(
|
||||
adapter: Arc<CachedLspAdapter>,
|
||||
language: Arc<Language>,
|
||||
delegate: Arc<dyn LspAdapterDelegate>,
|
||||
download_dir: Arc<Path>,
|
||||
container_dir: Arc<Path>,
|
||||
statuses: async_broadcast::Sender<(Arc<Language>, LanguageServerBinaryStatus)>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<LanguageServerBinary> {
|
||||
let container_dir = download_dir.join(adapter.name.0.as_ref());
|
||||
if !container_dir.exists() {
|
||||
smol::fs::create_dir_all(&container_dir)
|
||||
.await
|
||||
@ -1030,14 +1082,14 @@ async fn get_binary(
|
||||
.await;
|
||||
|
||||
if let Err(error) = binary.as_ref() {
|
||||
if let Some(cached) = adapter
|
||||
.cached_server_binary(container_dir, delegate.as_ref())
|
||||
if let Some(binary) = adapter
|
||||
.cached_server_binary(container_dir.to_path_buf(), delegate.as_ref())
|
||||
.await
|
||||
{
|
||||
statuses
|
||||
.broadcast((language.clone(), LanguageServerBinaryStatus::Cached))
|
||||
.await?;
|
||||
return Ok(cached);
|
||||
return Ok(binary);
|
||||
} else {
|
||||
statuses
|
||||
.broadcast((
|
||||
@ -1049,6 +1101,7 @@ async fn get_binary(
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
binary
|
||||
}
|
||||
|
||||
@ -1066,16 +1119,19 @@ async fn fetch_latest_binary(
|
||||
LanguageServerBinaryStatus::CheckingForUpdate,
|
||||
))
|
||||
.await?;
|
||||
|
||||
let version_info = adapter.fetch_latest_server_version(delegate).await?;
|
||||
lsp_binary_statuses_tx
|
||||
.broadcast((language.clone(), LanguageServerBinaryStatus::Downloading))
|
||||
.await?;
|
||||
|
||||
let binary = adapter
|
||||
.fetch_server_binary(version_info, container_dir.to_path_buf(), delegate)
|
||||
.await?;
|
||||
lsp_binary_statuses_tx
|
||||
.broadcast((language.clone(), LanguageServerBinaryStatus::Downloaded))
|
||||
.await?;
|
||||
|
||||
Ok(binary)
|
||||
}
|
||||
|
||||
@ -1617,6 +1673,10 @@ impl LspAdapter for Arc<FakeLspAdapter> {
|
||||
unreachable!();
|
||||
}
|
||||
|
||||
async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
|
||||
unreachable!();
|
||||
}
|
||||
|
||||
async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {}
|
||||
|
||||
async fn disk_based_diagnostic_sources(&self) -> Vec<String> {
|
||||
|
@ -1,6 +1,6 @@
|
||||
use crate::{File, Language};
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use collections::{HashMap, HashSet};
|
||||
use globset::GlobMatcher;
|
||||
use gpui::AppContext;
|
||||
use schemars::{
|
||||
@ -52,6 +52,7 @@ pub struct LanguageSettings {
|
||||
pub show_copilot_suggestions: bool,
|
||||
pub show_whitespaces: ShowWhitespaceSetting,
|
||||
pub extend_comment_on_newline: bool,
|
||||
pub inlay_hints: InlayHintSettings,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@ -98,6 +99,8 @@ pub struct LanguageSettingsContent {
|
||||
pub show_whitespaces: Option<ShowWhitespaceSetting>,
|
||||
#[serde(default)]
|
||||
pub extend_comment_on_newline: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub inlay_hints: Option<InlayHintSettings>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
@ -150,6 +153,38 @@ pub enum Formatter {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
pub struct InlayHintSettings {
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub show_type_hints: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub show_parameter_hints: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub show_other_hints: bool,
|
||||
}
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
impl InlayHintSettings {
|
||||
pub fn enabled_inlay_hint_kinds(&self) -> HashSet<Option<InlayHintKind>> {
|
||||
let mut kinds = HashSet::default();
|
||||
if self.show_type_hints {
|
||||
kinds.insert(Some(InlayHintKind::Type));
|
||||
}
|
||||
if self.show_parameter_hints {
|
||||
kinds.insert(Some(InlayHintKind::Parameter));
|
||||
}
|
||||
if self.show_other_hints {
|
||||
kinds.insert(None);
|
||||
}
|
||||
kinds
|
||||
}
|
||||
}
|
||||
|
||||
impl AllLanguageSettings {
|
||||
pub fn language<'a>(&'a self, language_name: Option<&str>) -> &'a LanguageSettings {
|
||||
if let Some(name) = language_name {
|
||||
@ -184,6 +219,29 @@ impl AllLanguageSettings {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum InlayHintKind {
|
||||
Type,
|
||||
Parameter,
|
||||
}
|
||||
|
||||
impl InlayHintKind {
|
||||
pub fn from_name(name: &str) -> Option<Self> {
|
||||
match name {
|
||||
"type" => Some(InlayHintKind::Type),
|
||||
"parameter" => Some(InlayHintKind::Parameter),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
InlayHintKind::Type => "type",
|
||||
InlayHintKind::Parameter => "parameter",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl settings::Setting for AllLanguageSettings {
|
||||
const KEY: Option<&'static str> = None;
|
||||
|
||||
@ -347,6 +405,7 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
|
||||
&mut settings.extend_comment_on_newline,
|
||||
src.extend_comment_on_newline,
|
||||
);
|
||||
merge(&mut settings.inlay_hints, src.inlay_hints);
|
||||
fn merge<T>(target: &mut T, value: Option<T>) {
|
||||
if let Some(value) = value {
|
||||
*target = value;
|
||||
|
@ -16,6 +16,7 @@ use smol::{
|
||||
process::{self, Child},
|
||||
};
|
||||
use std::{
|
||||
ffi::OsString,
|
||||
fmt,
|
||||
future::Future,
|
||||
io::Write,
|
||||
@ -36,6 +37,12 @@ type NotificationHandler = Box<dyn Send + FnMut(Option<usize>, &str, AsyncAppCon
|
||||
type ResponseHandler = Box<dyn Send + FnOnce(Result<String, Error>)>;
|
||||
type IoHandler = Box<dyn Send + FnMut(bool, &str)>;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct LanguageServerBinary {
|
||||
pub path: PathBuf,
|
||||
pub arguments: Vec<OsString>,
|
||||
}
|
||||
|
||||
pub struct LanguageServer {
|
||||
server_id: LanguageServerId,
|
||||
next_id: AtomicUsize,
|
||||
@ -51,7 +58,7 @@ pub struct LanguageServer {
|
||||
io_tasks: Mutex<Option<(Task<Option<()>>, Task<Option<()>>)>>,
|
||||
output_done_rx: Mutex<Option<barrier::Receiver>>,
|
||||
root_path: PathBuf,
|
||||
_server: Option<Child>,
|
||||
_server: Option<Mutex<Child>>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
@ -119,10 +126,9 @@ struct Error {
|
||||
}
|
||||
|
||||
impl LanguageServer {
|
||||
pub fn new<T: AsRef<std::ffi::OsStr>>(
|
||||
pub fn new(
|
||||
server_id: LanguageServerId,
|
||||
binary_path: &Path,
|
||||
arguments: &[T],
|
||||
binary: LanguageServerBinary,
|
||||
root_path: &Path,
|
||||
code_action_kinds: Option<Vec<CodeActionKind>>,
|
||||
cx: AsyncAppContext,
|
||||
@ -133,9 +139,9 @@ impl LanguageServer {
|
||||
root_path.parent().unwrap_or_else(|| Path::new("/"))
|
||||
};
|
||||
|
||||
let mut server = process::Command::new(binary_path)
|
||||
let mut server = process::Command::new(&binary.path)
|
||||
.current_dir(working_dir)
|
||||
.args(arguments)
|
||||
.args(binary.arguments)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::inherit())
|
||||
@ -167,9 +173,10 @@ impl LanguageServer {
|
||||
},
|
||||
);
|
||||
|
||||
if let Some(name) = binary_path.file_name() {
|
||||
if let Some(name) = binary.path.file_name() {
|
||||
server.name = name.to_string_lossy().to_string();
|
||||
}
|
||||
|
||||
Ok(server)
|
||||
}
|
||||
|
||||
@ -231,7 +238,7 @@ impl LanguageServer {
|
||||
io_tasks: Mutex::new(Some((input_task, output_task))),
|
||||
output_done_rx: Mutex::new(Some(output_done_rx)),
|
||||
root_path: root_path.to_path_buf(),
|
||||
_server: server,
|
||||
_server: server.map(|server| Mutex::new(server)),
|
||||
}
|
||||
}
|
||||
|
||||
@ -381,6 +388,9 @@ impl LanguageServer {
|
||||
resolve_support: None,
|
||||
..WorkspaceSymbolClientCapabilities::default()
|
||||
}),
|
||||
inlay_hint: Some(InlayHintWorkspaceClientCapabilities {
|
||||
refresh_support: Some(true),
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
text_document: Some(TextDocumentClientCapabilities {
|
||||
@ -422,6 +432,10 @@ impl LanguageServer {
|
||||
content_format: Some(vec![MarkupKind::Markdown]),
|
||||
..Default::default()
|
||||
}),
|
||||
inlay_hint: Some(InlayHintClientCapabilities {
|
||||
resolve_support: None,
|
||||
dynamic_registration: Some(false),
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
experimental: Some(json!({
|
||||
@ -600,6 +614,7 @@ impl LanguageServer {
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
Err(error) => {
|
||||
log::error!(
|
||||
"error deserializing {} request: {:?}, message: {:?}",
|
||||
@ -701,7 +716,7 @@ impl LanguageServer {
|
||||
.context("failed to deserialize response"),
|
||||
Err(error) => Err(anyhow!("{}", error.message)),
|
||||
};
|
||||
let _ = tx.send(response);
|
||||
_ = tx.send(response);
|
||||
})
|
||||
.detach();
|
||||
}),
|
||||
|
@ -20,3 +20,4 @@ serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
smol.workspace = true
|
||||
log.workspace = true
|
||||
|
@ -1,21 +1,24 @@
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use futures::lock::Mutex;
|
||||
use futures::{future::Shared, FutureExt};
|
||||
use gpui::{executor::Background, Task};
|
||||
use parking_lot::Mutex;
|
||||
use serde::Deserialize;
|
||||
use smol::{fs, io::BufReader, process::Command};
|
||||
use std::process::Output;
|
||||
use std::{
|
||||
env::consts,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
sync::{Arc, OnceLock},
|
||||
};
|
||||
use util::http::HttpClient;
|
||||
use util::{http::HttpClient, ResultExt};
|
||||
|
||||
const VERSION: &str = "v18.15.0";
|
||||
|
||||
#[derive(Deserialize)]
|
||||
static RUNTIME_INSTANCE: OnceLock<Arc<NodeRuntime>> = OnceLock::new();
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct NpmInfo {
|
||||
#[serde(default)]
|
||||
@ -23,7 +26,7 @@ pub struct NpmInfo {
|
||||
versions: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
pub struct NpmInfoDistTags {
|
||||
latest: Option<String>,
|
||||
}
|
||||
@ -35,12 +38,16 @@ pub struct NodeRuntime {
|
||||
}
|
||||
|
||||
impl NodeRuntime {
|
||||
pub fn new(http: Arc<dyn HttpClient>, background: Arc<Background>) -> Arc<NodeRuntime> {
|
||||
Arc::new(NodeRuntime {
|
||||
http,
|
||||
background,
|
||||
installation_path: Mutex::new(None),
|
||||
})
|
||||
pub fn instance(http: Arc<dyn HttpClient>, background: Arc<Background>) -> Arc<NodeRuntime> {
|
||||
RUNTIME_INSTANCE
|
||||
.get_or_init(|| {
|
||||
Arc::new(NodeRuntime {
|
||||
http,
|
||||
background,
|
||||
installation_path: Mutex::new(None),
|
||||
})
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub async fn binary_path(&self) -> Result<PathBuf> {
|
||||
@ -50,55 +57,74 @@ impl NodeRuntime {
|
||||
|
||||
pub async fn run_npm_subcommand(
|
||||
&self,
|
||||
directory: &Path,
|
||||
directory: Option<&Path>,
|
||||
subcommand: &str,
|
||||
args: &[&str],
|
||||
) -> Result<()> {
|
||||
) -> Result<Output> {
|
||||
let attempt = |installation_path: PathBuf| async move {
|
||||
let node_binary = installation_path.join("bin/node");
|
||||
let npm_file = installation_path.join("bin/npm");
|
||||
|
||||
if smol::fs::metadata(&node_binary).await.is_err() {
|
||||
return Err(anyhow!("missing node binary file"));
|
||||
}
|
||||
|
||||
if smol::fs::metadata(&npm_file).await.is_err() {
|
||||
return Err(anyhow!("missing npm file"));
|
||||
}
|
||||
|
||||
let mut command = Command::new(node_binary);
|
||||
command.arg(npm_file).arg(subcommand).args(args);
|
||||
|
||||
if let Some(directory) = directory {
|
||||
command.current_dir(directory);
|
||||
}
|
||||
|
||||
command.output().await.map_err(|e| anyhow!("{e}"))
|
||||
};
|
||||
|
||||
let installation_path = self.install_if_needed().await?;
|
||||
let node_binary = installation_path.join("bin/node");
|
||||
let npm_file = installation_path.join("bin/npm");
|
||||
|
||||
let output = Command::new(node_binary)
|
||||
.arg(npm_file)
|
||||
.arg(subcommand)
|
||||
.args(args)
|
||||
.current_dir(directory)
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"failed to execute npm {subcommand} subcommand:\nstdout: {:?}\nstderr: {:?}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
let mut output = attempt(installation_path).await;
|
||||
if output.is_err() {
|
||||
let installation_path = self.reinstall().await?;
|
||||
output = attempt(installation_path).await;
|
||||
if output.is_err() {
|
||||
return Err(anyhow!(
|
||||
"failed to launch npm subcommand {subcommand} subcommand"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
if let Ok(output) = &output {
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"failed to execute npm {subcommand} subcommand:\nstdout: {:?}\nstderr: {:?}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
output.map_err(|e| anyhow!("{e}"))
|
||||
}
|
||||
|
||||
pub async fn npm_package_latest_version(&self, name: &str) -> Result<String> {
|
||||
let installation_path = self.install_if_needed().await?;
|
||||
let node_binary = installation_path.join("bin/node");
|
||||
let npm_file = installation_path.join("bin/npm");
|
||||
|
||||
let output = Command::new(node_binary)
|
||||
.arg(npm_file)
|
||||
.args(["-fetch-retry-mintimeout", "2000"])
|
||||
.args(["-fetch-retry-maxtimeout", "5000"])
|
||||
.args(["-fetch-timeout", "5000"])
|
||||
.args(["info", name, "--json"])
|
||||
.output()
|
||||
.await
|
||||
.context("failed to run npm info")?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"failed to execute npm info:\nstdout: {:?}\nstderr: {:?}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
}
|
||||
let output = self
|
||||
.run_npm_subcommand(
|
||||
None,
|
||||
"info",
|
||||
&[
|
||||
name,
|
||||
"--json",
|
||||
"-fetch-retry-mintimeout",
|
||||
"2000",
|
||||
"-fetch-retry-maxtimeout",
|
||||
"5000",
|
||||
"-fetch-timeout",
|
||||
"5000",
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?;
|
||||
info.dist_tags
|
||||
@ -112,41 +138,54 @@ impl NodeRuntime {
|
||||
directory: &Path,
|
||||
packages: impl IntoIterator<Item = (&str, &str)>,
|
||||
) -> Result<()> {
|
||||
let installation_path = self.install_if_needed().await?;
|
||||
let node_binary = installation_path.join("bin/node");
|
||||
let npm_file = installation_path.join("bin/npm");
|
||||
let packages: Vec<_> = packages
|
||||
.into_iter()
|
||||
.map(|(name, version)| format!("{name}@{version}"))
|
||||
.collect();
|
||||
|
||||
let output = Command::new(node_binary)
|
||||
.arg(npm_file)
|
||||
.args(["-fetch-retry-mintimeout", "2000"])
|
||||
.args(["-fetch-retry-maxtimeout", "5000"])
|
||||
.args(["-fetch-timeout", "5000"])
|
||||
.arg("install")
|
||||
.arg("--prefix")
|
||||
.arg(directory)
|
||||
.args(
|
||||
packages
|
||||
.into_iter()
|
||||
.map(|(name, version)| format!("{name}@{version}")),
|
||||
)
|
||||
.output()
|
||||
.await
|
||||
.context("failed to run npm install")?;
|
||||
let mut arguments: Vec<_> = packages.iter().map(|p| p.as_str()).collect();
|
||||
arguments.extend_from_slice(&[
|
||||
"-fetch-retry-mintimeout",
|
||||
"2000",
|
||||
"-fetch-retry-maxtimeout",
|
||||
"5000",
|
||||
"-fetch-timeout",
|
||||
"5000",
|
||||
]);
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow!(
|
||||
"failed to execute npm install:\nstdout: {:?}\nstderr: {:?}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
}
|
||||
self.run_npm_subcommand(Some(directory), "install", &arguments)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn reinstall(&self) -> Result<PathBuf> {
|
||||
log::info!("beginnning to reinstall Node runtime");
|
||||
let mut installation_path = self.installation_path.lock().await;
|
||||
|
||||
if let Some(task) = installation_path.as_ref().cloned() {
|
||||
if let Ok(installation_path) = task.await {
|
||||
smol::fs::remove_dir_all(&installation_path)
|
||||
.await
|
||||
.context("node dir removal")
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
let http = self.http.clone();
|
||||
let task = self
|
||||
.background
|
||||
.spawn(async move { Self::install(http).await.map_err(Arc::new) })
|
||||
.shared();
|
||||
|
||||
*installation_path = Some(task.clone());
|
||||
task.await.map_err(|e| anyhow!("{}", e))
|
||||
}
|
||||
|
||||
async fn install_if_needed(&self) -> Result<PathBuf> {
|
||||
let task = self
|
||||
.installation_path
|
||||
.lock()
|
||||
.await
|
||||
.get_or_insert_with(|| {
|
||||
let http = self.http.clone();
|
||||
self.background
|
||||
@ -155,13 +194,11 @@ impl NodeRuntime {
|
||||
})
|
||||
.clone();
|
||||
|
||||
match task.await {
|
||||
Ok(path) => Ok(path),
|
||||
Err(error) => Err(anyhow!("{}", error)),
|
||||
}
|
||||
task.await.map_err(|e| anyhow!("{}", e))
|
||||
}
|
||||
|
||||
async fn install(http: Arc<dyn HttpClient>) -> Result<PathBuf> {
|
||||
log::info!("installing Node runtime");
|
||||
let arch = match consts::ARCH {
|
||||
"x86_64" => "x64",
|
||||
"aarch64" => "arm64",
|
||||
|
@ -1,14 +1,15 @@
|
||||
use crate::{
|
||||
DocumentHighlight, Hover, HoverBlock, HoverBlockKind, Location, LocationLink, Project,
|
||||
ProjectTransaction,
|
||||
DocumentHighlight, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel,
|
||||
InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink,
|
||||
MarkupContent, Project, ProjectTransaction,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use async_trait::async_trait;
|
||||
use client::proto::{self, PeerId};
|
||||
use fs::LineEnding;
|
||||
use gpui::{AppContext, AsyncAppContext, ModelHandle};
|
||||
use language::{
|
||||
language_settings::language_settings,
|
||||
language_settings::{language_settings, InlayHintKind},
|
||||
point_from_lsp, point_to_lsp,
|
||||
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
|
||||
range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CharKind, CodeAction,
|
||||
@ -126,6 +127,10 @@ pub(crate) struct OnTypeFormatting {
|
||||
pub push_to_history: bool,
|
||||
}
|
||||
|
||||
pub(crate) struct InlayHints {
|
||||
pub range: Range<Anchor>,
|
||||
}
|
||||
|
||||
pub(crate) struct FormattingOptions {
|
||||
tab_size: u32,
|
||||
}
|
||||
@ -1780,3 +1785,327 @@ impl LspCommand for OnTypeFormatting {
|
||||
message.buffer_id
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspCommand for InlayHints {
|
||||
type Response = Vec<InlayHint>;
|
||||
type LspRequest = lsp::InlayHintRequest;
|
||||
type ProtoRequest = proto::InlayHints;
|
||||
|
||||
fn check_capabilities(&self, server_capabilities: &lsp::ServerCapabilities) -> bool {
|
||||
let Some(inlay_hint_provider) = &server_capabilities.inlay_hint_provider else { return false };
|
||||
match inlay_hint_provider {
|
||||
lsp::OneOf::Left(enabled) => *enabled,
|
||||
lsp::OneOf::Right(inlay_hint_capabilities) => match inlay_hint_capabilities {
|
||||
lsp::InlayHintServerCapabilities::Options(_) => true,
|
||||
lsp::InlayHintServerCapabilities::RegistrationOptions(_) => false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn to_lsp(
|
||||
&self,
|
||||
path: &Path,
|
||||
buffer: &Buffer,
|
||||
_: &Arc<LanguageServer>,
|
||||
_: &AppContext,
|
||||
) -> lsp::InlayHintParams {
|
||||
lsp::InlayHintParams {
|
||||
text_document: lsp::TextDocumentIdentifier {
|
||||
uri: lsp::Url::from_file_path(path).unwrap(),
|
||||
},
|
||||
range: range_to_lsp(self.range.to_point_utf16(buffer)),
|
||||
work_done_progress_params: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn response_from_lsp(
|
||||
self,
|
||||
message: Option<Vec<lsp::InlayHint>>,
|
||||
_: ModelHandle<Project>,
|
||||
buffer: ModelHandle<Buffer>,
|
||||
_: LanguageServerId,
|
||||
cx: AsyncAppContext,
|
||||
) -> Result<Vec<InlayHint>> {
|
||||
cx.read(|cx| {
|
||||
let origin_buffer = buffer.read(cx);
|
||||
Ok(message
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|lsp_hint| {
|
||||
let kind = lsp_hint.kind.and_then(|kind| match kind {
|
||||
lsp::InlayHintKind::TYPE => Some(InlayHintKind::Type),
|
||||
lsp::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter),
|
||||
_ => None,
|
||||
});
|
||||
let position = origin_buffer
|
||||
.clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left);
|
||||
InlayHint {
|
||||
buffer_id: origin_buffer.remote_id(),
|
||||
position: if kind == Some(InlayHintKind::Parameter) {
|
||||
origin_buffer.anchor_before(position)
|
||||
} else {
|
||||
origin_buffer.anchor_after(position)
|
||||
},
|
||||
padding_left: lsp_hint.padding_left.unwrap_or(false),
|
||||
padding_right: lsp_hint.padding_right.unwrap_or(false),
|
||||
label: match lsp_hint.label {
|
||||
lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s),
|
||||
lsp::InlayHintLabel::LabelParts(lsp_parts) => {
|
||||
InlayHintLabel::LabelParts(
|
||||
lsp_parts
|
||||
.into_iter()
|
||||
.map(|label_part| InlayHintLabelPart {
|
||||
value: label_part.value,
|
||||
tooltip: label_part.tooltip.map(
|
||||
|tooltip| {
|
||||
match tooltip {
|
||||
lsp::InlayHintLabelPartTooltip::String(s) => {
|
||||
InlayHintLabelPartTooltip::String(s)
|
||||
}
|
||||
lsp::InlayHintLabelPartTooltip::MarkupContent(
|
||||
markup_content,
|
||||
) => InlayHintLabelPartTooltip::MarkupContent(
|
||||
MarkupContent {
|
||||
kind: format!("{:?}", markup_content.kind),
|
||||
value: markup_content.value,
|
||||
},
|
||||
),
|
||||
}
|
||||
},
|
||||
),
|
||||
location: label_part.location.map(|lsp_location| {
|
||||
let target_start = origin_buffer.clip_point_utf16(
|
||||
point_from_lsp(lsp_location.range.start),
|
||||
Bias::Left,
|
||||
);
|
||||
let target_end = origin_buffer.clip_point_utf16(
|
||||
point_from_lsp(lsp_location.range.end),
|
||||
Bias::Left,
|
||||
);
|
||||
Location {
|
||||
buffer: buffer.clone(),
|
||||
range: origin_buffer.anchor_after(target_start)
|
||||
..origin_buffer.anchor_before(target_end),
|
||||
}
|
||||
}),
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
},
|
||||
kind,
|
||||
tooltip: lsp_hint.tooltip.map(|tooltip| match tooltip {
|
||||
lsp::InlayHintTooltip::String(s) => InlayHintTooltip::String(s),
|
||||
lsp::InlayHintTooltip::MarkupContent(markup_content) => {
|
||||
InlayHintTooltip::MarkupContent(MarkupContent {
|
||||
kind: format!("{:?}", markup_content.kind),
|
||||
value: markup_content.value,
|
||||
})
|
||||
}
|
||||
}),
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
})
|
||||
}
|
||||
|
||||
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::InlayHints {
|
||||
proto::InlayHints {
|
||||
project_id,
|
||||
buffer_id: buffer.remote_id(),
|
||||
start: Some(language::proto::serialize_anchor(&self.range.start)),
|
||||
end: Some(language::proto::serialize_anchor(&self.range.end)),
|
||||
version: serialize_version(&buffer.version()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn from_proto(
|
||||
message: proto::InlayHints,
|
||||
_: ModelHandle<Project>,
|
||||
buffer: ModelHandle<Buffer>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<Self> {
|
||||
let start = message
|
||||
.start
|
||||
.and_then(language::proto::deserialize_anchor)
|
||||
.context("invalid start")?;
|
||||
let end = message
|
||||
.end
|
||||
.and_then(language::proto::deserialize_anchor)
|
||||
.context("invalid end")?;
|
||||
buffer
|
||||
.update(&mut cx, |buffer, _| {
|
||||
buffer.wait_for_version(deserialize_version(&message.version))
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(Self { range: start..end })
|
||||
}
|
||||
|
||||
fn response_to_proto(
|
||||
response: Vec<InlayHint>,
|
||||
_: &mut Project,
|
||||
_: PeerId,
|
||||
buffer_version: &clock::Global,
|
||||
cx: &mut AppContext,
|
||||
) -> proto::InlayHintsResponse {
|
||||
proto::InlayHintsResponse {
|
||||
hints: response
|
||||
.into_iter()
|
||||
.map(|response_hint| proto::InlayHint {
|
||||
position: Some(language::proto::serialize_anchor(&response_hint.position)),
|
||||
padding_left: response_hint.padding_left,
|
||||
padding_right: response_hint.padding_right,
|
||||
label: Some(proto::InlayHintLabel {
|
||||
label: Some(match response_hint.label {
|
||||
InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s),
|
||||
InlayHintLabel::LabelParts(label_parts) => {
|
||||
proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts {
|
||||
parts: label_parts.into_iter().map(|label_part| proto::InlayHintLabelPart {
|
||||
value: label_part.value,
|
||||
tooltip: label_part.tooltip.map(|tooltip| {
|
||||
let proto_tooltip = match tooltip {
|
||||
InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s),
|
||||
InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent {
|
||||
kind: markup_content.kind,
|
||||
value: markup_content.value,
|
||||
}),
|
||||
};
|
||||
proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)}
|
||||
}),
|
||||
location: label_part.location.map(|location| proto::Location {
|
||||
start: Some(serialize_anchor(&location.range.start)),
|
||||
end: Some(serialize_anchor(&location.range.end)),
|
||||
buffer_id: location.buffer.read(cx).remote_id(),
|
||||
}),
|
||||
}).collect()
|
||||
})
|
||||
}
|
||||
}),
|
||||
}),
|
||||
kind: response_hint.kind.map(|kind| kind.name().to_string()),
|
||||
tooltip: response_hint.tooltip.map(|response_tooltip| {
|
||||
let proto_tooltip = match response_tooltip {
|
||||
InlayHintTooltip::String(s) => {
|
||||
proto::inlay_hint_tooltip::Content::Value(s)
|
||||
}
|
||||
InlayHintTooltip::MarkupContent(markup_content) => {
|
||||
proto::inlay_hint_tooltip::Content::MarkupContent(
|
||||
proto::MarkupContent {
|
||||
kind: markup_content.kind,
|
||||
value: markup_content.value,
|
||||
},
|
||||
)
|
||||
}
|
||||
};
|
||||
proto::InlayHintTooltip {
|
||||
content: Some(proto_tooltip),
|
||||
}
|
||||
}),
|
||||
})
|
||||
.collect(),
|
||||
version: serialize_version(buffer_version),
|
||||
}
|
||||
}
|
||||
|
||||
async fn response_from_proto(
|
||||
self,
|
||||
message: proto::InlayHintsResponse,
|
||||
project: ModelHandle<Project>,
|
||||
buffer: ModelHandle<Buffer>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<Vec<InlayHint>> {
|
||||
buffer
|
||||
.update(&mut cx, |buffer, _| {
|
||||
buffer.wait_for_version(deserialize_version(&message.version))
|
||||
})
|
||||
.await?;
|
||||
|
||||
let mut hints = Vec::new();
|
||||
for message_hint in message.hints {
|
||||
let buffer_id = message_hint
|
||||
.position
|
||||
.as_ref()
|
||||
.and_then(|location| location.buffer_id)
|
||||
.context("missing buffer id")?;
|
||||
let hint = InlayHint {
|
||||
buffer_id,
|
||||
position: message_hint
|
||||
.position
|
||||
.and_then(language::proto::deserialize_anchor)
|
||||
.context("invalid position")?,
|
||||
label: match message_hint
|
||||
.label
|
||||
.and_then(|label| label.label)
|
||||
.context("missing label")?
|
||||
{
|
||||
proto::inlay_hint_label::Label::Value(s) => InlayHintLabel::String(s),
|
||||
proto::inlay_hint_label::Label::LabelParts(parts) => {
|
||||
let mut label_parts = Vec::new();
|
||||
for part in parts.parts {
|
||||
label_parts.push(InlayHintLabelPart {
|
||||
value: part.value,
|
||||
tooltip: part.tooltip.map(|tooltip| match tooltip.content {
|
||||
Some(proto::inlay_hint_label_part_tooltip::Content::Value(s)) => InlayHintLabelPartTooltip::String(s),
|
||||
Some(proto::inlay_hint_label_part_tooltip::Content::MarkupContent(markup_content)) => InlayHintLabelPartTooltip::MarkupContent(MarkupContent {
|
||||
kind: markup_content.kind,
|
||||
value: markup_content.value,
|
||||
}),
|
||||
None => InlayHintLabelPartTooltip::String(String::new()),
|
||||
}),
|
||||
location: match part.location {
|
||||
Some(location) => {
|
||||
let target_buffer = project
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.wait_for_remote_buffer(location.buffer_id, cx)
|
||||
})
|
||||
.await?;
|
||||
Some(Location {
|
||||
range: location
|
||||
.start
|
||||
.and_then(language::proto::deserialize_anchor)
|
||||
.context("invalid start")?
|
||||
..location
|
||||
.end
|
||||
.and_then(language::proto::deserialize_anchor)
|
||||
.context("invalid end")?,
|
||||
buffer: target_buffer,
|
||||
})},
|
||||
None => None,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
InlayHintLabel::LabelParts(label_parts)
|
||||
}
|
||||
},
|
||||
padding_left: message_hint.padding_left,
|
||||
padding_right: message_hint.padding_right,
|
||||
kind: message_hint
|
||||
.kind
|
||||
.as_deref()
|
||||
.and_then(InlayHintKind::from_name),
|
||||
tooltip: message_hint.tooltip.and_then(|tooltip| {
|
||||
Some(match tooltip.content? {
|
||||
proto::inlay_hint_tooltip::Content::Value(s) => InlayHintTooltip::String(s),
|
||||
proto::inlay_hint_tooltip::Content::MarkupContent(markup_content) => {
|
||||
InlayHintTooltip::MarkupContent(MarkupContent {
|
||||
kind: markup_content.kind,
|
||||
value: markup_content.value,
|
||||
})
|
||||
}
|
||||
})
|
||||
}),
|
||||
};
|
||||
|
||||
hints.push(hint);
|
||||
}
|
||||
|
||||
Ok(hints)
|
||||
}
|
||||
|
||||
fn buffer_id_from_proto(message: &proto::InlayHints) -> u64 {
|
||||
message.buffer_id
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -596,6 +596,8 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
|
||||
);
|
||||
});
|
||||
|
||||
let prev_read_dir_count = fs.read_dir_call_count();
|
||||
|
||||
// Keep track of the FS events reported to the language server.
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
let file_changes = Arc::new(Mutex::new(Vec::new()));
|
||||
@ -607,6 +609,12 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
|
||||
register_options: serde_json::to_value(
|
||||
lsp::DidChangeWatchedFilesRegistrationOptions {
|
||||
watchers: vec![
|
||||
lsp::FileSystemWatcher {
|
||||
glob_pattern: lsp::GlobPattern::String(
|
||||
"/the-root/Cargo.toml".to_string(),
|
||||
),
|
||||
kind: None,
|
||||
},
|
||||
lsp::FileSystemWatcher {
|
||||
glob_pattern: lsp::GlobPattern::String(
|
||||
"/the-root/src/*.{rs,c}".to_string(),
|
||||
@ -638,6 +646,7 @@ async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppCon
|
||||
|
||||
cx.foreground().run_until_parked();
|
||||
assert_eq!(mem::take(&mut *file_changes.lock()), &[]);
|
||||
assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 4);
|
||||
|
||||
// Now the language server has asked us to watch an ignored directory path,
|
||||
// so we recursively load it.
|
||||
|
@ -3071,17 +3071,20 @@ impl BackgroundScanner {
|
||||
|
||||
path_prefix = self.path_prefixes_to_scan_rx.recv().fuse() => {
|
||||
let Ok(path_prefix) = path_prefix else { break };
|
||||
log::trace!("adding path prefix {:?}", path_prefix);
|
||||
|
||||
self.forcibly_load_paths(&[path_prefix.clone()]).await;
|
||||
let did_scan = self.forcibly_load_paths(&[path_prefix.clone()]).await;
|
||||
if did_scan {
|
||||
let abs_path =
|
||||
{
|
||||
let mut state = self.state.lock();
|
||||
state.path_prefixes_to_scan.insert(path_prefix.clone());
|
||||
state.snapshot.abs_path.join(&path_prefix)
|
||||
};
|
||||
|
||||
let abs_path =
|
||||
{
|
||||
let mut state = self.state.lock();
|
||||
state.path_prefixes_to_scan.insert(path_prefix.clone());
|
||||
state.snapshot.abs_path.join(path_prefix)
|
||||
};
|
||||
if let Some(abs_path) = self.fs.canonicalize(&abs_path).await.log_err() {
|
||||
self.process_events(vec![abs_path]).await;
|
||||
if let Some(abs_path) = self.fs.canonicalize(&abs_path).await.log_err() {
|
||||
self.process_events(vec![abs_path]).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3097,10 +3100,13 @@ impl BackgroundScanner {
|
||||
}
|
||||
}
|
||||
|
||||
async fn process_scan_request(&self, request: ScanRequest, scanning: bool) -> bool {
|
||||
async fn process_scan_request(&self, mut request: ScanRequest, scanning: bool) -> bool {
|
||||
log::debug!("rescanning paths {:?}", request.relative_paths);
|
||||
|
||||
let root_path = self.forcibly_load_paths(&request.relative_paths).await;
|
||||
request.relative_paths.sort_unstable();
|
||||
self.forcibly_load_paths(&request.relative_paths).await;
|
||||
|
||||
let root_path = self.state.lock().snapshot.abs_path.clone();
|
||||
let root_canonical_path = match self.fs.canonicalize(&root_path).await {
|
||||
Ok(path) => path,
|
||||
Err(err) => {
|
||||
@ -3108,10 +3114,9 @@ impl BackgroundScanner {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let abs_paths = request
|
||||
.relative_paths
|
||||
.into_iter()
|
||||
.iter()
|
||||
.map(|path| {
|
||||
if path.file_name().is_some() {
|
||||
root_canonical_path.join(path)
|
||||
@ -3120,12 +3125,19 @@ impl BackgroundScanner {
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
self.reload_entries_for_paths(root_path, root_canonical_path, abs_paths, None)
|
||||
.await;
|
||||
|
||||
self.reload_entries_for_paths(
|
||||
root_path,
|
||||
root_canonical_path,
|
||||
&request.relative_paths,
|
||||
abs_paths,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
self.send_status_update(scanning, Some(request.done))
|
||||
}
|
||||
|
||||
async fn process_events(&mut self, abs_paths: Vec<PathBuf>) {
|
||||
async fn process_events(&mut self, mut abs_paths: Vec<PathBuf>) {
|
||||
log::debug!("received fs events {:?}", abs_paths);
|
||||
|
||||
let root_path = self.state.lock().snapshot.abs_path.clone();
|
||||
@ -3137,25 +3149,61 @@ impl BackgroundScanner {
|
||||
}
|
||||
};
|
||||
|
||||
let (scan_job_tx, scan_job_rx) = channel::unbounded();
|
||||
let paths = self
|
||||
.reload_entries_for_paths(
|
||||
let mut relative_paths = Vec::with_capacity(abs_paths.len());
|
||||
let mut unloaded_relative_paths = Vec::new();
|
||||
abs_paths.sort_unstable();
|
||||
abs_paths.dedup_by(|a, b| a.starts_with(&b));
|
||||
abs_paths.retain(|abs_path| {
|
||||
let snapshot = &self.state.lock().snapshot;
|
||||
{
|
||||
let relative_path: Arc<Path> =
|
||||
if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) {
|
||||
path.into()
|
||||
} else {
|
||||
log::error!(
|
||||
"ignoring event {abs_path:?} outside of root path {root_canonical_path:?}",
|
||||
);
|
||||
return false;
|
||||
};
|
||||
|
||||
let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| {
|
||||
snapshot
|
||||
.entry_for_path(parent)
|
||||
.map_or(false, |entry| entry.kind == EntryKind::Dir)
|
||||
});
|
||||
if !parent_dir_is_loaded {
|
||||
log::debug!("ignoring event {relative_path:?} within unloaded directory");
|
||||
unloaded_relative_paths.push(relative_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
relative_paths.push(relative_path);
|
||||
true
|
||||
}
|
||||
});
|
||||
|
||||
if !relative_paths.is_empty() {
|
||||
let (scan_job_tx, scan_job_rx) = channel::unbounded();
|
||||
self.reload_entries_for_paths(
|
||||
root_path,
|
||||
root_canonical_path,
|
||||
&relative_paths,
|
||||
abs_paths,
|
||||
Some(scan_job_tx.clone()),
|
||||
)
|
||||
.await;
|
||||
drop(scan_job_tx);
|
||||
self.scan_dirs(false, scan_job_rx).await;
|
||||
drop(scan_job_tx);
|
||||
self.scan_dirs(false, scan_job_rx).await;
|
||||
|
||||
let (scan_job_tx, scan_job_rx) = channel::unbounded();
|
||||
self.update_ignore_statuses(scan_job_tx).await;
|
||||
self.scan_dirs(false, scan_job_rx).await;
|
||||
let (scan_job_tx, scan_job_rx) = channel::unbounded();
|
||||
self.update_ignore_statuses(scan_job_tx).await;
|
||||
self.scan_dirs(false, scan_job_rx).await;
|
||||
}
|
||||
|
||||
{
|
||||
let mut state = self.state.lock();
|
||||
state.reload_repositories(&paths, self.fs.as_ref());
|
||||
relative_paths.extend(unloaded_relative_paths);
|
||||
state.reload_repositories(&relative_paths, self.fs.as_ref());
|
||||
state.snapshot.completed_scan_id = state.snapshot.scan_id;
|
||||
for (_, entry_id) in mem::take(&mut state.removed_entry_ids) {
|
||||
state.scanned_dirs.remove(&entry_id);
|
||||
@ -3165,12 +3213,11 @@ impl BackgroundScanner {
|
||||
self.send_status_update(false, None);
|
||||
}
|
||||
|
||||
async fn forcibly_load_paths(&self, paths: &[Arc<Path>]) -> Arc<Path> {
|
||||
let root_path;
|
||||
async fn forcibly_load_paths(&self, paths: &[Arc<Path>]) -> bool {
|
||||
let (scan_job_tx, mut scan_job_rx) = channel::unbounded();
|
||||
{
|
||||
let mut state = self.state.lock();
|
||||
root_path = state.snapshot.abs_path.clone();
|
||||
let root_path = state.snapshot.abs_path.clone();
|
||||
for path in paths {
|
||||
for ancestor in path.ancestors() {
|
||||
if let Some(entry) = state.snapshot.entry_for_path(ancestor) {
|
||||
@ -3201,8 +3248,8 @@ impl BackgroundScanner {
|
||||
while let Some(job) = scan_job_rx.next().await {
|
||||
self.scan_dir(&job).await.log_err();
|
||||
}
|
||||
self.state.lock().paths_to_scan.clear();
|
||||
root_path
|
||||
|
||||
mem::take(&mut self.state.lock().paths_to_scan).len() > 0
|
||||
}
|
||||
|
||||
async fn scan_dirs(
|
||||
@ -3475,7 +3522,7 @@ impl BackgroundScanner {
|
||||
.expect("channel is unbounded");
|
||||
}
|
||||
} else {
|
||||
log::debug!("defer scanning directory {:?} {:?}", entry.path, entry.kind);
|
||||
log::debug!("defer scanning directory {:?}", entry.path);
|
||||
entry.kind = EntryKind::UnloadedDir;
|
||||
}
|
||||
}
|
||||
@ -3490,26 +3537,10 @@ impl BackgroundScanner {
|
||||
&self,
|
||||
root_abs_path: Arc<Path>,
|
||||
root_canonical_path: PathBuf,
|
||||
mut abs_paths: Vec<PathBuf>,
|
||||
relative_paths: &[Arc<Path>],
|
||||
abs_paths: Vec<PathBuf>,
|
||||
scan_queue_tx: Option<Sender<ScanJob>>,
|
||||
) -> Vec<Arc<Path>> {
|
||||
let mut event_paths = Vec::<Arc<Path>>::with_capacity(abs_paths.len());
|
||||
abs_paths.sort_unstable();
|
||||
abs_paths.dedup_by(|a, b| a.starts_with(&b));
|
||||
abs_paths.retain(|abs_path| {
|
||||
if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) {
|
||||
event_paths.push(path.into());
|
||||
true
|
||||
} else {
|
||||
log::error!(
|
||||
"unexpected event {:?} for root path {:?}",
|
||||
abs_path,
|
||||
root_canonical_path
|
||||
);
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
) {
|
||||
let metadata = futures::future::join_all(
|
||||
abs_paths
|
||||
.iter()
|
||||
@ -3538,30 +3569,15 @@ impl BackgroundScanner {
|
||||
// Remove any entries for paths that no longer exist or are being recursively
|
||||
// refreshed. Do this before adding any new entries, so that renames can be
|
||||
// detected regardless of the order of the paths.
|
||||
for (path, metadata) in event_paths.iter().zip(metadata.iter()) {
|
||||
for (path, metadata) in relative_paths.iter().zip(metadata.iter()) {
|
||||
if matches!(metadata, Ok(None)) || doing_recursive_update {
|
||||
log::trace!("remove path {:?}", path);
|
||||
state.remove_path(path);
|
||||
}
|
||||
}
|
||||
|
||||
for (path, metadata) in event_paths.iter().zip(metadata.iter()) {
|
||||
if let (Some(parent), true) = (path.parent(), doing_recursive_update) {
|
||||
if state
|
||||
.snapshot
|
||||
.entry_for_path(parent)
|
||||
.map_or(true, |entry| entry.kind != EntryKind::Dir)
|
||||
{
|
||||
log::debug!(
|
||||
"ignoring event {path:?} within unloaded directory {:?}",
|
||||
parent
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for (path, metadata) in relative_paths.iter().zip(metadata.iter()) {
|
||||
let abs_path: Arc<Path> = root_abs_path.join(&path).into();
|
||||
|
||||
match metadata {
|
||||
Ok(Some((metadata, canonical_path))) => {
|
||||
let ignore_stack = state
|
||||
@ -3624,12 +3640,10 @@ impl BackgroundScanner {
|
||||
|
||||
util::extend_sorted(
|
||||
&mut state.changed_paths,
|
||||
event_paths.iter().cloned(),
|
||||
relative_paths.iter().cloned(),
|
||||
usize::MAX,
|
||||
Ord::cmp,
|
||||
);
|
||||
|
||||
event_paths
|
||||
}
|
||||
|
||||
fn remove_repo_path(&self, path: &Path, snapshot: &mut LocalSnapshot) -> Option<()> {
|
||||
@ -3760,25 +3774,22 @@ impl BackgroundScanner {
|
||||
|
||||
// Scan any directories that were previously ignored and weren't
|
||||
// previously scanned.
|
||||
if was_ignored
|
||||
&& !entry.is_ignored
|
||||
&& !entry.is_external
|
||||
&& entry.kind == EntryKind::UnloadedDir
|
||||
{
|
||||
job.scan_queue
|
||||
.try_send(ScanJob {
|
||||
abs_path: abs_path.clone(),
|
||||
path: entry.path.clone(),
|
||||
ignore_stack: child_ignore_stack.clone(),
|
||||
scan_queue: job.scan_queue.clone(),
|
||||
ancestor_inodes: self
|
||||
.state
|
||||
.lock()
|
||||
.snapshot
|
||||
.ancestor_inodes_for_path(&entry.path),
|
||||
is_external: false,
|
||||
})
|
||||
.unwrap();
|
||||
if was_ignored && !entry.is_ignored && entry.kind.is_unloaded() {
|
||||
let state = self.state.lock();
|
||||
if state.should_scan_directory(&entry) {
|
||||
job.scan_queue
|
||||
.try_send(ScanJob {
|
||||
abs_path: abs_path.clone(),
|
||||
path: entry.path.clone(),
|
||||
ignore_stack: child_ignore_stack.clone(),
|
||||
scan_queue: job.scan_queue.clone(),
|
||||
ancestor_inodes: state
|
||||
.snapshot
|
||||
.ancestor_inodes_for_path(&entry.path),
|
||||
is_external: false,
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
job.ignore_queue
|
||||
|
@ -454,6 +454,10 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
|
||||
"b1.js": "b1",
|
||||
"b2.js": "b2",
|
||||
},
|
||||
"c": {
|
||||
"c1.js": "c1",
|
||||
"c2.js": "c2",
|
||||
}
|
||||
},
|
||||
},
|
||||
"two": {
|
||||
@ -521,6 +525,7 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
|
||||
(Path::new("one/node_modules/b"), true),
|
||||
(Path::new("one/node_modules/b/b1.js"), true),
|
||||
(Path::new("one/node_modules/b/b2.js"), true),
|
||||
(Path::new("one/node_modules/c"), true),
|
||||
(Path::new("two"), false),
|
||||
(Path::new("two/x.js"), false),
|
||||
(Path::new("two/y.js"), false),
|
||||
@ -564,6 +569,7 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
|
||||
(Path::new("one/node_modules/b"), true),
|
||||
(Path::new("one/node_modules/b/b1.js"), true),
|
||||
(Path::new("one/node_modules/b/b2.js"), true),
|
||||
(Path::new("one/node_modules/c"), true),
|
||||
(Path::new("two"), false),
|
||||
(Path::new("two/x.js"), false),
|
||||
(Path::new("two/y.js"), false),
|
||||
@ -578,6 +584,17 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
|
||||
// Only the newly-expanded directory is scanned.
|
||||
assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 1);
|
||||
});
|
||||
|
||||
// No work happens when files and directories change within an unloaded directory.
|
||||
let prev_fs_call_count = fs.read_dir_call_count() + fs.metadata_call_count();
|
||||
fs.create_dir("/root/one/node_modules/c/lib".as_ref())
|
||||
.await
|
||||
.unwrap();
|
||||
cx.foreground().run_until_parked();
|
||||
assert_eq!(
|
||||
fs.read_dir_call_count() + fs.metadata_call_count() - prev_fs_call_count,
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
@ -384,6 +384,12 @@ impl<'a> From<&'a str> for Rope {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Rope {
|
||||
fn from(text: String) -> Self {
|
||||
Rope::from(text.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Rope {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for chunk in self.chunks() {
|
||||
|
@ -136,6 +136,10 @@ message Envelope {
|
||||
OnTypeFormattingResponse on_type_formatting_response = 112;
|
||||
|
||||
UpdateWorktreeSettings update_worktree_settings = 113;
|
||||
|
||||
InlayHints inlay_hints = 116;
|
||||
InlayHintsResponse inlay_hints_response = 117;
|
||||
RefreshInlayHints refresh_inlay_hints = 118;
|
||||
}
|
||||
}
|
||||
|
||||
@ -705,6 +709,68 @@ message OnTypeFormattingResponse {
|
||||
Transaction transaction = 1;
|
||||
}
|
||||
|
||||
message InlayHints {
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
Anchor start = 3;
|
||||
Anchor end = 4;
|
||||
repeated VectorClockEntry version = 5;
|
||||
}
|
||||
|
||||
message InlayHintsResponse {
|
||||
repeated InlayHint hints = 1;
|
||||
repeated VectorClockEntry version = 2;
|
||||
}
|
||||
|
||||
message InlayHint {
|
||||
Anchor position = 1;
|
||||
InlayHintLabel label = 2;
|
||||
optional string kind = 3;
|
||||
bool padding_left = 4;
|
||||
bool padding_right = 5;
|
||||
InlayHintTooltip tooltip = 6;
|
||||
}
|
||||
|
||||
message InlayHintLabel {
|
||||
oneof label {
|
||||
string value = 1;
|
||||
InlayHintLabelParts label_parts = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message InlayHintLabelParts {
|
||||
repeated InlayHintLabelPart parts = 1;
|
||||
}
|
||||
|
||||
message InlayHintLabelPart {
|
||||
string value = 1;
|
||||
InlayHintLabelPartTooltip tooltip = 2;
|
||||
Location location = 3;
|
||||
}
|
||||
|
||||
message InlayHintTooltip {
|
||||
oneof content {
|
||||
string value = 1;
|
||||
MarkupContent markup_content = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message InlayHintLabelPartTooltip {
|
||||
oneof content {
|
||||
string value = 1;
|
||||
MarkupContent markup_content = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message RefreshInlayHints {
|
||||
uint64 project_id = 1;
|
||||
}
|
||||
|
||||
message MarkupContent {
|
||||
string kind = 1;
|
||||
string value = 2;
|
||||
}
|
||||
|
||||
message PerformRenameResponse {
|
||||
ProjectTransaction transaction = 2;
|
||||
}
|
||||
|
@ -198,6 +198,9 @@ messages!(
|
||||
(PerformRenameResponse, Background),
|
||||
(OnTypeFormatting, Background),
|
||||
(OnTypeFormattingResponse, Background),
|
||||
(InlayHints, Background),
|
||||
(InlayHintsResponse, Background),
|
||||
(RefreshInlayHints, Foreground),
|
||||
(Ping, Foreground),
|
||||
(PrepareRename, Background),
|
||||
(PrepareRenameResponse, Background),
|
||||
@ -286,6 +289,8 @@ request_messages!(
|
||||
(PerformRename, PerformRenameResponse),
|
||||
(PrepareRename, PrepareRenameResponse),
|
||||
(OnTypeFormatting, OnTypeFormattingResponse),
|
||||
(InlayHints, InlayHintsResponse),
|
||||
(RefreshInlayHints, Ack),
|
||||
(ReloadBuffers, ReloadBuffersResponse),
|
||||
(RequestContact, Ack),
|
||||
(RemoveContact, Ack),
|
||||
@ -332,6 +337,8 @@ entity_messages!(
|
||||
OpenBufferForSymbol,
|
||||
PerformRename,
|
||||
OnTypeFormatting,
|
||||
InlayHints,
|
||||
RefreshInlayHints,
|
||||
PrepareRename,
|
||||
ReloadBuffers,
|
||||
RemoveProjectCollaborator,
|
||||
|
@ -97,6 +97,42 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_item(&self) -> Option<&'a T> {
|
||||
self.assert_did_seek();
|
||||
if let Some(entry) = self.stack.last() {
|
||||
if entry.index == entry.tree.0.items().len() - 1 {
|
||||
if let Some(next_leaf) = self.next_leaf() {
|
||||
Some(next_leaf.0.items().first().unwrap())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
match *entry.tree.0 {
|
||||
Node::Leaf { ref items, .. } => Some(&items[entry.index + 1]),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
} else if self.at_end {
|
||||
None
|
||||
} else {
|
||||
self.tree.first()
|
||||
}
|
||||
}
|
||||
|
||||
fn next_leaf(&self) -> Option<&'a SumTree<T>> {
|
||||
for entry in self.stack.iter().rev().skip(1) {
|
||||
if entry.index < entry.tree.0.child_trees().len() - 1 {
|
||||
match *entry.tree.0 {
|
||||
Node::Internal {
|
||||
ref child_trees, ..
|
||||
} => return Some(child_trees[entry.index + 1].leftmost_leaf()),
|
||||
Node::Leaf { .. } => unreachable!(),
|
||||
};
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn prev_item(&self) -> Option<&'a T> {
|
||||
self.assert_did_seek();
|
||||
if let Some(entry) = self.stack.last() {
|
||||
|
@ -95,31 +95,18 @@ impl<D> fmt::Debug for End<D> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
|
||||
#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug, Hash, Default)]
|
||||
pub enum Bias {
|
||||
#[default]
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
impl Default for Bias {
|
||||
fn default() -> Self {
|
||||
Bias::Left
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for Bias {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Bias {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
match (self, other) {
|
||||
(Self::Left, Self::Left) => Ordering::Equal,
|
||||
(Self::Left, Self::Right) => Ordering::Less,
|
||||
(Self::Right, Self::Right) => Ordering::Equal,
|
||||
(Self::Right, Self::Left) => Ordering::Greater,
|
||||
impl Bias {
|
||||
pub fn invert(self) -> Self {
|
||||
match self {
|
||||
Self::Left => Self::Right,
|
||||
Self::Right => Self::Left,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -838,6 +825,14 @@ mod tests {
|
||||
assert_eq!(cursor.item(), None);
|
||||
}
|
||||
|
||||
if before_start {
|
||||
assert_eq!(cursor.next_item(), reference_items.get(0));
|
||||
} else if pos + 1 < reference_items.len() {
|
||||
assert_eq!(cursor.next_item().unwrap(), &reference_items[pos + 1]);
|
||||
} else {
|
||||
assert_eq!(cursor.next_item(), None);
|
||||
}
|
||||
|
||||
if i < 5 {
|
||||
cursor.next(&());
|
||||
if pos < reference_items.len() {
|
||||
@ -883,14 +878,17 @@ mod tests {
|
||||
);
|
||||
assert_eq!(cursor.item(), None);
|
||||
assert_eq!(cursor.prev_item(), None);
|
||||
assert_eq!(cursor.next_item(), None);
|
||||
assert_eq!(cursor.start().sum, 0);
|
||||
cursor.prev(&());
|
||||
assert_eq!(cursor.item(), None);
|
||||
assert_eq!(cursor.prev_item(), None);
|
||||
assert_eq!(cursor.next_item(), None);
|
||||
assert_eq!(cursor.start().sum, 0);
|
||||
cursor.next(&());
|
||||
assert_eq!(cursor.item(), None);
|
||||
assert_eq!(cursor.prev_item(), None);
|
||||
assert_eq!(cursor.next_item(), None);
|
||||
assert_eq!(cursor.start().sum, 0);
|
||||
|
||||
// Single-element tree
|
||||
@ -903,22 +901,26 @@ mod tests {
|
||||
);
|
||||
assert_eq!(cursor.item(), Some(&1));
|
||||
assert_eq!(cursor.prev_item(), None);
|
||||
assert_eq!(cursor.next_item(), None);
|
||||
assert_eq!(cursor.start().sum, 0);
|
||||
|
||||
cursor.next(&());
|
||||
assert_eq!(cursor.item(), None);
|
||||
assert_eq!(cursor.prev_item(), Some(&1));
|
||||
assert_eq!(cursor.next_item(), None);
|
||||
assert_eq!(cursor.start().sum, 1);
|
||||
|
||||
cursor.prev(&());
|
||||
assert_eq!(cursor.item(), Some(&1));
|
||||
assert_eq!(cursor.prev_item(), None);
|
||||
assert_eq!(cursor.next_item(), None);
|
||||
assert_eq!(cursor.start().sum, 0);
|
||||
|
||||
let mut cursor = tree.cursor::<IntegersSummary>();
|
||||
assert_eq!(cursor.slice(&Count(1), Bias::Right, &()).items(&()), [1]);
|
||||
assert_eq!(cursor.item(), None);
|
||||
assert_eq!(cursor.prev_item(), Some(&1));
|
||||
assert_eq!(cursor.next_item(), None);
|
||||
assert_eq!(cursor.start().sum, 1);
|
||||
|
||||
cursor.seek(&Count(0), Bias::Right, &());
|
||||
@ -930,6 +932,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(cursor.item(), None);
|
||||
assert_eq!(cursor.prev_item(), Some(&1));
|
||||
assert_eq!(cursor.next_item(), None);
|
||||
assert_eq!(cursor.start().sum, 1);
|
||||
|
||||
// Multiple-element tree
|
||||
@ -940,67 +943,80 @@ mod tests {
|
||||
assert_eq!(cursor.slice(&Count(2), Bias::Right, &()).items(&()), [1, 2]);
|
||||
assert_eq!(cursor.item(), Some(&3));
|
||||
assert_eq!(cursor.prev_item(), Some(&2));
|
||||
assert_eq!(cursor.next_item(), Some(&4));
|
||||
assert_eq!(cursor.start().sum, 3);
|
||||
|
||||
cursor.next(&());
|
||||
assert_eq!(cursor.item(), Some(&4));
|
||||
assert_eq!(cursor.prev_item(), Some(&3));
|
||||
assert_eq!(cursor.next_item(), Some(&5));
|
||||
assert_eq!(cursor.start().sum, 6);
|
||||
|
||||
cursor.next(&());
|
||||
assert_eq!(cursor.item(), Some(&5));
|
||||
assert_eq!(cursor.prev_item(), Some(&4));
|
||||
assert_eq!(cursor.next_item(), Some(&6));
|
||||
assert_eq!(cursor.start().sum, 10);
|
||||
|
||||
cursor.next(&());
|
||||
assert_eq!(cursor.item(), Some(&6));
|
||||
assert_eq!(cursor.prev_item(), Some(&5));
|
||||
assert_eq!(cursor.next_item(), None);
|
||||
assert_eq!(cursor.start().sum, 15);
|
||||
|
||||
cursor.next(&());
|
||||
cursor.next(&());
|
||||
assert_eq!(cursor.item(), None);
|
||||
assert_eq!(cursor.prev_item(), Some(&6));
|
||||
assert_eq!(cursor.next_item(), None);
|
||||
assert_eq!(cursor.start().sum, 21);
|
||||
|
||||
cursor.prev(&());
|
||||
assert_eq!(cursor.item(), Some(&6));
|
||||
assert_eq!(cursor.prev_item(), Some(&5));
|
||||
assert_eq!(cursor.next_item(), None);
|
||||
assert_eq!(cursor.start().sum, 15);
|
||||
|
||||
cursor.prev(&());
|
||||
assert_eq!(cursor.item(), Some(&5));
|
||||
assert_eq!(cursor.prev_item(), Some(&4));
|
||||
assert_eq!(cursor.next_item(), Some(&6));
|
||||
assert_eq!(cursor.start().sum, 10);
|
||||
|
||||
cursor.prev(&());
|
||||
assert_eq!(cursor.item(), Some(&4));
|
||||
assert_eq!(cursor.prev_item(), Some(&3));
|
||||
assert_eq!(cursor.next_item(), Some(&5));
|
||||
assert_eq!(cursor.start().sum, 6);
|
||||
|
||||
cursor.prev(&());
|
||||
assert_eq!(cursor.item(), Some(&3));
|
||||
assert_eq!(cursor.prev_item(), Some(&2));
|
||||
assert_eq!(cursor.next_item(), Some(&4));
|
||||
assert_eq!(cursor.start().sum, 3);
|
||||
|
||||
cursor.prev(&());
|
||||
assert_eq!(cursor.item(), Some(&2));
|
||||
assert_eq!(cursor.prev_item(), Some(&1));
|
||||
assert_eq!(cursor.next_item(), Some(&3));
|
||||
assert_eq!(cursor.start().sum, 1);
|
||||
|
||||
cursor.prev(&());
|
||||
assert_eq!(cursor.item(), Some(&1));
|
||||
assert_eq!(cursor.prev_item(), None);
|
||||
assert_eq!(cursor.next_item(), Some(&2));
|
||||
assert_eq!(cursor.start().sum, 0);
|
||||
|
||||
cursor.prev(&());
|
||||
assert_eq!(cursor.item(), None);
|
||||
assert_eq!(cursor.prev_item(), None);
|
||||
assert_eq!(cursor.next_item(), Some(&1));
|
||||
assert_eq!(cursor.start().sum, 0);
|
||||
|
||||
cursor.next(&());
|
||||
assert_eq!(cursor.item(), Some(&1));
|
||||
assert_eq!(cursor.prev_item(), None);
|
||||
assert_eq!(cursor.next_item(), Some(&2));
|
||||
assert_eq!(cursor.start().sum, 0);
|
||||
|
||||
let mut cursor = tree.cursor::<IntegersSummary>();
|
||||
@ -1012,6 +1028,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(cursor.item(), None);
|
||||
assert_eq!(cursor.prev_item(), Some(&6));
|
||||
assert_eq!(cursor.next_item(), None);
|
||||
assert_eq!(cursor.start().sum, 21);
|
||||
|
||||
cursor.seek(&Count(3), Bias::Right, &());
|
||||
@ -1023,6 +1040,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(cursor.item(), None);
|
||||
assert_eq!(cursor.prev_item(), Some(&6));
|
||||
assert_eq!(cursor.next_item(), None);
|
||||
assert_eq!(cursor.start().sum, 21);
|
||||
|
||||
// Seeking can bias left or right
|
||||
|
@ -395,16 +395,17 @@ impl TerminalElement {
|
||||
// Terminal Emulator controlled behavior:
|
||||
region = region
|
||||
// Start selections
|
||||
.on_down(
|
||||
MouseButton::Left,
|
||||
TerminalElement::generic_button_handler(
|
||||
connection,
|
||||
origin,
|
||||
move |terminal, origin, e, _cx| {
|
||||
terminal.mouse_down(&e, origin);
|
||||
},
|
||||
),
|
||||
)
|
||||
.on_down(MouseButton::Left, move |event, v: &mut TerminalView, cx| {
|
||||
cx.focus_parent();
|
||||
v.context_menu.update(cx, |menu, _cx| menu.delay_cancel());
|
||||
if let Some(conn_handle) = connection.upgrade(cx) {
|
||||
conn_handle.update(cx, |terminal, cx| {
|
||||
terminal.mouse_down(&event, origin);
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
})
|
||||
// Update drag selections
|
||||
.on_drag(MouseButton::Left, move |event, _: &mut TerminalView, cx| {
|
||||
if cx.is_self_focused() {
|
||||
|
@ -87,6 +87,7 @@ impl TerminalPanel {
|
||||
}
|
||||
})
|
||||
},
|
||||
|_, _| {},
|
||||
None,
|
||||
))
|
||||
.with_child(Pane::render_tab_bar_button(
|
||||
@ -100,6 +101,7 @@ impl TerminalPanel {
|
||||
Some(("Toggle Zoom".into(), Some(Box::new(workspace::ToggleZoom)))),
|
||||
cx,
|
||||
move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
|
||||
|_, _| {},
|
||||
None,
|
||||
))
|
||||
.into_any()
|
||||
|
@ -689,6 +689,7 @@ pub struct Editor {
|
||||
pub line_number_active: Color,
|
||||
pub guest_selections: Vec<SelectionStyle>,
|
||||
pub syntax: Arc<SyntaxTheme>,
|
||||
pub hint: HighlightStyle,
|
||||
pub suggestion: HighlightStyle,
|
||||
pub diagnostic_path_header: DiagnosticPathHeader,
|
||||
pub diagnostic_header: DiagnosticHeader,
|
||||
|
@ -118,14 +118,15 @@ pub fn merge_non_null_json_value_into(source: serde_json::Value, target: &mut se
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ResultExt {
|
||||
pub trait ResultExt<E> {
|
||||
type Ok;
|
||||
|
||||
fn log_err(self) -> Option<Self::Ok>;
|
||||
fn warn_on_err(self) -> Option<Self::Ok>;
|
||||
fn inspect_error(self, func: impl FnOnce(&E)) -> Self;
|
||||
}
|
||||
|
||||
impl<T, E> ResultExt for Result<T, E>
|
||||
impl<T, E> ResultExt<E> for Result<T, E>
|
||||
where
|
||||
E: std::fmt::Debug,
|
||||
{
|
||||
@ -152,6 +153,15 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// https://doc.rust-lang.org/std/result/enum.Result.html#method.inspect_err
|
||||
fn inspect_error(self, func: impl FnOnce(&E)) -> Self {
|
||||
if let Err(err) = &self {
|
||||
func(err);
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub trait TryFutureExt {
|
||||
|
@ -273,6 +273,11 @@ impl Pane {
|
||||
Some(("New...".into(), None)),
|
||||
cx,
|
||||
|pane, cx| pane.deploy_new_menu(cx),
|
||||
|pane, cx| {
|
||||
pane.tab_bar_context_menu
|
||||
.handle
|
||||
.update(cx, |menu, _| menu.delay_cancel())
|
||||
},
|
||||
pane.tab_bar_context_menu
|
||||
.handle_if_kind(TabBarContextMenuKind::New),
|
||||
))
|
||||
@ -283,6 +288,11 @@ impl Pane {
|
||||
Some(("Split Pane".into(), None)),
|
||||
cx,
|
||||
|pane, cx| pane.deploy_split_menu(cx),
|
||||
|pane, cx| {
|
||||
pane.tab_bar_context_menu
|
||||
.handle
|
||||
.update(cx, |menu, _| menu.delay_cancel())
|
||||
},
|
||||
pane.tab_bar_context_menu
|
||||
.handle_if_kind(TabBarContextMenuKind::Split),
|
||||
))
|
||||
@ -304,6 +314,7 @@ impl Pane {
|
||||
Some((tooltip_label, Some(Box::new(ToggleZoom)))),
|
||||
cx,
|
||||
move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
|
||||
move |_, _| {},
|
||||
None,
|
||||
)
|
||||
})
|
||||
@ -988,7 +999,7 @@ impl Pane {
|
||||
|
||||
fn deploy_split_menu(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
|
||||
menu.show(
|
||||
menu.toggle(
|
||||
Default::default(),
|
||||
AnchorCorner::TopRight,
|
||||
vec![
|
||||
@ -1006,7 +1017,7 @@ impl Pane {
|
||||
|
||||
fn deploy_new_menu(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.tab_bar_context_menu.handle.update(cx, |menu, cx| {
|
||||
menu.show(
|
||||
menu.toggle(
|
||||
Default::default(),
|
||||
AnchorCorner::TopRight,
|
||||
vec![
|
||||
@ -1416,13 +1427,17 @@ impl Pane {
|
||||
.into_any()
|
||||
}
|
||||
|
||||
pub fn render_tab_bar_button<F: 'static + Fn(&mut Pane, &mut EventContext<Pane>)>(
|
||||
pub fn render_tab_bar_button<
|
||||
F1: 'static + Fn(&mut Pane, &mut EventContext<Pane>),
|
||||
F2: 'static + Fn(&mut Pane, &mut EventContext<Pane>),
|
||||
>(
|
||||
index: usize,
|
||||
icon: &'static str,
|
||||
is_active: bool,
|
||||
tooltip: Option<(String, Option<Box<dyn Action>>)>,
|
||||
cx: &mut ViewContext<Pane>,
|
||||
on_click: F,
|
||||
on_click: F1,
|
||||
on_down: F2,
|
||||
context_menu: Option<ViewHandle<ContextMenu>>,
|
||||
) -> AnyElement<Pane> {
|
||||
enum TabBarButton {}
|
||||
@ -1440,6 +1455,7 @@ impl Pane {
|
||||
.with_height(style.button_width)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_down(MouseButton::Left, move |_, pane, cx| on_down(pane, cx))
|
||||
.on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx))
|
||||
.into_any();
|
||||
if let Some((tooltip, action)) = tooltip {
|
||||
|
@ -2,6 +2,7 @@ use anyhow::{anyhow, Context, Result};
|
||||
use async_trait::async_trait;
|
||||
use futures::StreamExt;
|
||||
pub use language::*;
|
||||
use lsp::LanguageServerBinary;
|
||||
use smol::fs::{self, File};
|
||||
use std::{any::Any, path::PathBuf, sync::Arc};
|
||||
use util::{
|
||||
@ -86,31 +87,19 @@ impl super::LspAdapter for CLspAdapter {
|
||||
container_dir: PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
(|| async move {
|
||||
let mut last_clangd_dir = None;
|
||||
let mut entries = fs::read_dir(&container_dir).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let entry = entry?;
|
||||
if entry.file_type().await?.is_dir() {
|
||||
last_clangd_dir = Some(entry.path());
|
||||
}
|
||||
}
|
||||
let clangd_dir = last_clangd_dir.ok_or_else(|| anyhow!("no cached binary"))?;
|
||||
let clangd_bin = clangd_dir.join("bin/clangd");
|
||||
if clangd_bin.exists() {
|
||||
Ok(LanguageServerBinary {
|
||||
path: clangd_bin,
|
||||
arguments: vec![],
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"missing clangd binary in directory {:?}",
|
||||
clangd_dir
|
||||
))
|
||||
}
|
||||
})()
|
||||
.await
|
||||
.log_err()
|
||||
get_cached_server_binary(container_dir).await
|
||||
}
|
||||
|
||||
async fn installation_test_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
get_cached_server_binary(container_dir)
|
||||
.await
|
||||
.map(|mut binary| {
|
||||
binary.arguments = vec!["--help".into()];
|
||||
binary
|
||||
})
|
||||
}
|
||||
|
||||
async fn label_for_completion(
|
||||
@ -250,6 +239,34 @@ impl super::LspAdapter for CLspAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
|
||||
(|| async move {
|
||||
let mut last_clangd_dir = None;
|
||||
let mut entries = fs::read_dir(&container_dir).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let entry = entry?;
|
||||
if entry.file_type().await?.is_dir() {
|
||||
last_clangd_dir = Some(entry.path());
|
||||
}
|
||||
}
|
||||
let clangd_dir = last_clangd_dir.ok_or_else(|| anyhow!("no cached binary"))?;
|
||||
let clangd_bin = clangd_dir.join("bin/clangd");
|
||||
if clangd_bin.exists() {
|
||||
Ok(LanguageServerBinary {
|
||||
path: clangd_bin,
|
||||
arguments: vec![],
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"missing clangd binary in directory {:?}",
|
||||
clangd_dir
|
||||
))
|
||||
}
|
||||
})()
|
||||
.await
|
||||
.log_err()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::TestAppContext;
|
||||
|
@ -3,7 +3,7 @@ use async_trait::async_trait;
|
||||
use futures::StreamExt;
|
||||
use gpui::{AsyncAppContext, Task};
|
||||
pub use language::*;
|
||||
use lsp::{CompletionItemKind, SymbolKind};
|
||||
use lsp::{CompletionItemKind, LanguageServerBinary, SymbolKind};
|
||||
use smol::fs::{self, File};
|
||||
use std::{
|
||||
any::Any,
|
||||
@ -140,20 +140,14 @@ impl LspAdapter for ElixirLspAdapter {
|
||||
container_dir: PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
(|| async move {
|
||||
let mut last = None;
|
||||
let mut entries = fs::read_dir(&container_dir).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
last = Some(entry?.path());
|
||||
}
|
||||
last.map(|path| LanguageServerBinary {
|
||||
path,
|
||||
arguments: vec![],
|
||||
})
|
||||
.ok_or_else(|| anyhow!("no cached binary"))
|
||||
})()
|
||||
.await
|
||||
.log_err()
|
||||
get_cached_server_binary(container_dir).await
|
||||
}
|
||||
|
||||
async fn installation_test_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
get_cached_server_binary(container_dir).await
|
||||
}
|
||||
|
||||
async fn label_for_completion(
|
||||
@ -239,3 +233,20 @@ impl LspAdapter for ElixirLspAdapter {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
|
||||
(|| async move {
|
||||
let mut last = None;
|
||||
let mut entries = fs::read_dir(&container_dir).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
last = Some(entry?.path());
|
||||
}
|
||||
last.map(|path| LanguageServerBinary {
|
||||
path,
|
||||
arguments: vec![],
|
||||
})
|
||||
.ok_or_else(|| anyhow!("no cached binary"))
|
||||
})()
|
||||
.await
|
||||
.log_err()
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ use futures::StreamExt;
|
||||
use gpui::{AsyncAppContext, Task};
|
||||
pub use language::*;
|
||||
use lazy_static::lazy_static;
|
||||
use lsp::LanguageServerBinary;
|
||||
use regex::Regex;
|
||||
use smol::{fs, process};
|
||||
use std::{
|
||||
@ -148,32 +149,19 @@ impl super::LspAdapter for GoLspAdapter {
|
||||
container_dir: PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
(|| async move {
|
||||
let mut last_binary_path = None;
|
||||
let mut entries = fs::read_dir(&container_dir).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let entry = entry?;
|
||||
if entry.file_type().await?.is_file()
|
||||
&& entry
|
||||
.file_name()
|
||||
.to_str()
|
||||
.map_or(false, |name| name.starts_with("gopls_"))
|
||||
{
|
||||
last_binary_path = Some(entry.path());
|
||||
}
|
||||
}
|
||||
get_cached_server_binary(container_dir).await
|
||||
}
|
||||
|
||||
if let Some(path) = last_binary_path {
|
||||
Ok(LanguageServerBinary {
|
||||
path,
|
||||
arguments: server_binary_arguments(),
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!("no cached binary"))
|
||||
}
|
||||
})()
|
||||
.await
|
||||
.log_err()
|
||||
async fn installation_test_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
get_cached_server_binary(container_dir)
|
||||
.await
|
||||
.map(|mut binary| {
|
||||
binary.arguments = vec!["--help".into()];
|
||||
binary
|
||||
})
|
||||
}
|
||||
|
||||
async fn label_for_completion(
|
||||
@ -336,6 +324,35 @@ impl super::LspAdapter for GoLspAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
|
||||
(|| async move {
|
||||
let mut last_binary_path = None;
|
||||
let mut entries = fs::read_dir(&container_dir).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let entry = entry?;
|
||||
if entry.file_type().await?.is_file()
|
||||
&& entry
|
||||
.file_name()
|
||||
.to_str()
|
||||
.map_or(false, |name| name.starts_with("gopls_"))
|
||||
{
|
||||
last_binary_path = Some(entry.path());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(path) = last_binary_path {
|
||||
Ok(LanguageServerBinary {
|
||||
path,
|
||||
arguments: server_binary_arguments(),
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!("no cached binary"))
|
||||
}
|
||||
})()
|
||||
.await
|
||||
.log_err()
|
||||
}
|
||||
|
||||
fn adjust_runs(
|
||||
delta: usize,
|
||||
mut runs: Vec<(Range<usize>, HighlightId)>,
|
||||
|
@ -1,7 +1,8 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use futures::StreamExt;
|
||||
use language::{LanguageServerBinary, LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use lsp::LanguageServerBinary;
|
||||
use node_runtime::NodeRuntime;
|
||||
use serde_json::json;
|
||||
use smol::fs;
|
||||
@ -13,6 +14,9 @@ use std::{
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
const SERVER_PATH: &'static str =
|
||||
"node_modules/vscode-langservers-extracted/bin/vscode-html-language-server";
|
||||
|
||||
fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
|
||||
vec![server_path.into(), "--stdio".into()]
|
||||
}
|
||||
@ -22,9 +26,6 @@ pub struct HtmlLspAdapter {
|
||||
}
|
||||
|
||||
impl HtmlLspAdapter {
|
||||
const SERVER_PATH: &'static str =
|
||||
"node_modules/vscode-langservers-extracted/bin/vscode-html-language-server";
|
||||
|
||||
pub fn new(node: Arc<NodeRuntime>) -> Self {
|
||||
HtmlLspAdapter { node }
|
||||
}
|
||||
@ -54,7 +55,7 @@ impl LspAdapter for HtmlLspAdapter {
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Result<LanguageServerBinary> {
|
||||
let version = version.downcast::<String>().unwrap();
|
||||
let server_path = container_dir.join(Self::SERVER_PATH);
|
||||
let server_path = container_dir.join(SERVER_PATH);
|
||||
|
||||
if fs::metadata(&server_path).await.is_err() {
|
||||
self.node
|
||||
@ -76,31 +77,14 @@ impl LspAdapter for HtmlLspAdapter {
|
||||
container_dir: PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
(|| async move {
|
||||
let mut last_version_dir = None;
|
||||
let mut entries = fs::read_dir(&container_dir).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let entry = entry?;
|
||||
if entry.file_type().await?.is_dir() {
|
||||
last_version_dir = Some(entry.path());
|
||||
}
|
||||
}
|
||||
let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
|
||||
let server_path = last_version_dir.join(Self::SERVER_PATH);
|
||||
if server_path.exists() {
|
||||
Ok(LanguageServerBinary {
|
||||
path: self.node.binary_path().await?,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"missing executable in directory {:?}",
|
||||
last_version_dir
|
||||
))
|
||||
}
|
||||
})()
|
||||
.await
|
||||
.log_err()
|
||||
get_cached_server_binary(container_dir, &self.node).await
|
||||
}
|
||||
|
||||
async fn installation_test_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
get_cached_server_binary(container_dir, &self.node).await
|
||||
}
|
||||
|
||||
async fn initialization_options(&self) -> Option<serde_json::Value> {
|
||||
@ -109,3 +93,34 @@ impl LspAdapter for HtmlLspAdapter {
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_server_binary(
|
||||
container_dir: PathBuf,
|
||||
node: &NodeRuntime,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
(|| async move {
|
||||
let mut last_version_dir = None;
|
||||
let mut entries = fs::read_dir(&container_dir).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let entry = entry?;
|
||||
if entry.file_type().await?.is_dir() {
|
||||
last_version_dir = Some(entry.path());
|
||||
}
|
||||
}
|
||||
let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
|
||||
let server_path = last_version_dir.join(SERVER_PATH);
|
||||
if server_path.exists() {
|
||||
Ok(LanguageServerBinary {
|
||||
path: node.binary_path().await?,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"missing executable in directory {:?}",
|
||||
last_version_dir
|
||||
))
|
||||
}
|
||||
})()
|
||||
.await
|
||||
.log_err()
|
||||
}
|
||||
|
@ -3,9 +3,8 @@ use async_trait::async_trait;
|
||||
use collections::HashMap;
|
||||
use futures::{future::BoxFuture, FutureExt, StreamExt};
|
||||
use gpui::AppContext;
|
||||
use language::{
|
||||
LanguageRegistry, LanguageServerBinary, LanguageServerName, LspAdapter, LspAdapterDelegate,
|
||||
};
|
||||
use language::{LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use lsp::LanguageServerBinary;
|
||||
use node_runtime::NodeRuntime;
|
||||
use serde_json::json;
|
||||
use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore};
|
||||
@ -84,32 +83,14 @@ impl LspAdapter for JsonLspAdapter {
|
||||
container_dir: PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
(|| async move {
|
||||
let mut last_version_dir = None;
|
||||
let mut entries = fs::read_dir(&container_dir).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let entry = entry?;
|
||||
if entry.file_type().await?.is_dir() {
|
||||
last_version_dir = Some(entry.path());
|
||||
}
|
||||
}
|
||||
get_cached_server_binary(container_dir, &self.node).await
|
||||
}
|
||||
|
||||
let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
|
||||
let server_path = last_version_dir.join(SERVER_PATH);
|
||||
if server_path.exists() {
|
||||
Ok(LanguageServerBinary {
|
||||
path: self.node.binary_path().await?,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"missing executable in directory {:?}",
|
||||
last_version_dir
|
||||
))
|
||||
}
|
||||
})()
|
||||
.await
|
||||
.log_err()
|
||||
async fn installation_test_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
get_cached_server_binary(container_dir, &self.node).await
|
||||
}
|
||||
|
||||
async fn initialization_options(&self) -> Option<serde_json::Value> {
|
||||
@ -162,6 +143,38 @@ impl LspAdapter for JsonLspAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_server_binary(
|
||||
container_dir: PathBuf,
|
||||
node: &NodeRuntime,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
(|| async move {
|
||||
let mut last_version_dir = None;
|
||||
let mut entries = fs::read_dir(&container_dir).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let entry = entry?;
|
||||
if entry.file_type().await?.is_dir() {
|
||||
last_version_dir = Some(entry.path());
|
||||
}
|
||||
}
|
||||
|
||||
let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
|
||||
let server_path = last_version_dir.join(SERVER_PATH);
|
||||
if server_path.exists() {
|
||||
Ok(LanguageServerBinary {
|
||||
path: node.binary_path().await?,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"missing executable in directory {:?}",
|
||||
last_version_dir
|
||||
))
|
||||
}
|
||||
})()
|
||||
.await
|
||||
.log_err()
|
||||
}
|
||||
|
||||
fn schema_file_match(path: &Path) -> &Path {
|
||||
path.strip_prefix(path.parent().unwrap().parent().unwrap())
|
||||
.unwrap()
|
||||
|
@ -3,7 +3,8 @@ use async_trait::async_trait;
|
||||
use collections::HashMap;
|
||||
use futures::lock::Mutex;
|
||||
use gpui::executor::Background;
|
||||
use language::{LanguageServerBinary, LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use lsp::LanguageServerBinary;
|
||||
use plugin_runtime::{Plugin, PluginBinary, PluginBuilder, WasiFn};
|
||||
use std::{any::Any, path::PathBuf, sync::Arc};
|
||||
use util::ResultExt;
|
||||
@ -129,6 +130,14 @@ impl LspAdapter for PluginLspAdapter {
|
||||
.await
|
||||
}
|
||||
|
||||
fn can_be_reinstalled(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
|
||||
None
|
||||
}
|
||||
|
||||
async fn initialization_options(&self) -> Option<serde_json::Value> {
|
||||
let string: String = self
|
||||
.runtime
|
||||
|
@ -3,7 +3,8 @@ use async_compression::futures::bufread::GzipDecoder;
|
||||
use async_tar::Archive;
|
||||
use async_trait::async_trait;
|
||||
use futures::{io::BufReader, StreamExt};
|
||||
use language::{LanguageServerBinary, LanguageServerName, LspAdapterDelegate};
|
||||
use language::{LanguageServerName, LspAdapterDelegate};
|
||||
use lsp::LanguageServerBinary;
|
||||
use smol::fs;
|
||||
use std::{any::Any, env::consts, ffi::OsString, path::PathBuf};
|
||||
use util::{
|
||||
@ -91,31 +92,47 @@ impl super::LspAdapter for LuaLspAdapter {
|
||||
container_dir: PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
async_iife!({
|
||||
let mut last_binary_path = None;
|
||||
let mut entries = fs::read_dir(&container_dir).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let entry = entry?;
|
||||
if entry.file_type().await?.is_file()
|
||||
&& entry
|
||||
.file_name()
|
||||
.to_str()
|
||||
.map_or(false, |name| name == "lua-language-server")
|
||||
{
|
||||
last_binary_path = Some(entry.path());
|
||||
}
|
||||
}
|
||||
get_cached_server_binary(container_dir).await
|
||||
}
|
||||
|
||||
if let Some(path) = last_binary_path {
|
||||
Ok(LanguageServerBinary {
|
||||
path,
|
||||
arguments: server_binary_arguments(),
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!("no cached binary"))
|
||||
}
|
||||
})
|
||||
.await
|
||||
.log_err()
|
||||
async fn installation_test_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
get_cached_server_binary(container_dir)
|
||||
.await
|
||||
.map(|mut binary| {
|
||||
binary.arguments = vec!["--version".into()];
|
||||
binary
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
|
||||
async_iife!({
|
||||
let mut last_binary_path = None;
|
||||
let mut entries = fs::read_dir(&container_dir).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let entry = entry?;
|
||||
if entry.file_type().await?.is_file()
|
||||
&& entry
|
||||
.file_name()
|
||||
.to_str()
|
||||
.map_or(false, |name| name == "lua-language-server")
|
||||
{
|
||||
last_binary_path = Some(entry.path());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(path) = last_binary_path {
|
||||
Ok(LanguageServerBinary {
|
||||
path,
|
||||
arguments: server_binary_arguments(),
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!("no cached binary"))
|
||||
}
|
||||
})
|
||||
.await
|
||||
.log_err()
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use futures::StreamExt;
|
||||
use language::{LanguageServerBinary, LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use lsp::LanguageServerBinary;
|
||||
use node_runtime::NodeRuntime;
|
||||
use smol::fs;
|
||||
use std::{
|
||||
@ -12,6 +13,8 @@ use std::{
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
const SERVER_PATH: &'static str = "node_modules/pyright/langserver.index.js";
|
||||
|
||||
fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
|
||||
vec![server_path.into(), "--stdio".into()]
|
||||
}
|
||||
@ -21,8 +24,6 @@ pub struct PythonLspAdapter {
|
||||
}
|
||||
|
||||
impl PythonLspAdapter {
|
||||
const SERVER_PATH: &'static str = "node_modules/pyright/langserver.index.js";
|
||||
|
||||
pub fn new(node: Arc<NodeRuntime>) -> Self {
|
||||
PythonLspAdapter { node }
|
||||
}
|
||||
@ -48,7 +49,7 @@ impl LspAdapter for PythonLspAdapter {
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Result<LanguageServerBinary> {
|
||||
let version = version.downcast::<String>().unwrap();
|
||||
let server_path = container_dir.join(Self::SERVER_PATH);
|
||||
let server_path = container_dir.join(SERVER_PATH);
|
||||
|
||||
if fs::metadata(&server_path).await.is_err() {
|
||||
self.node
|
||||
@ -67,31 +68,14 @@ impl LspAdapter for PythonLspAdapter {
|
||||
container_dir: PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
(|| async move {
|
||||
let mut last_version_dir = None;
|
||||
let mut entries = fs::read_dir(&container_dir).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let entry = entry?;
|
||||
if entry.file_type().await?.is_dir() {
|
||||
last_version_dir = Some(entry.path());
|
||||
}
|
||||
}
|
||||
let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
|
||||
let server_path = last_version_dir.join(Self::SERVER_PATH);
|
||||
if server_path.exists() {
|
||||
Ok(LanguageServerBinary {
|
||||
path: self.node.binary_path().await?,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"missing executable in directory {:?}",
|
||||
last_version_dir
|
||||
))
|
||||
}
|
||||
})()
|
||||
.await
|
||||
.log_err()
|
||||
get_cached_server_binary(container_dir, &self.node).await
|
||||
}
|
||||
|
||||
async fn installation_test_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
get_cached_server_binary(container_dir, &self.node).await
|
||||
}
|
||||
|
||||
async fn process_completion(&self, item: &mut lsp::CompletionItem) {
|
||||
@ -170,6 +154,37 @@ impl LspAdapter for PythonLspAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_server_binary(
|
||||
container_dir: PathBuf,
|
||||
node: &NodeRuntime,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
(|| async move {
|
||||
let mut last_version_dir = None;
|
||||
let mut entries = fs::read_dir(&container_dir).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let entry = entry?;
|
||||
if entry.file_type().await?.is_dir() {
|
||||
last_version_dir = Some(entry.path());
|
||||
}
|
||||
}
|
||||
let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
|
||||
let server_path = last_version_dir.join(SERVER_PATH);
|
||||
if server_path.exists() {
|
||||
Ok(LanguageServerBinary {
|
||||
path: node.binary_path().await?,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"missing executable in directory {:?}",
|
||||
last_version_dir
|
||||
))
|
||||
}
|
||||
})()
|
||||
.await
|
||||
.log_err()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::{ModelContext, TestAppContext};
|
||||
|
@ -1,6 +1,7 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_trait::async_trait;
|
||||
use language::{LanguageServerBinary, LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use lsp::LanguageServerBinary;
|
||||
use std::{any::Any, path::PathBuf, sync::Arc};
|
||||
|
||||
pub struct RubyLanguageServer;
|
||||
@ -38,6 +39,14 @@ impl LspAdapter for RubyLanguageServer {
|
||||
})
|
||||
}
|
||||
|
||||
fn can_be_reinstalled(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
|
||||
None
|
||||
}
|
||||
|
||||
async fn label_for_completion(
|
||||
&self,
|
||||
item: &lsp::CompletionItem,
|
||||
|
@ -4,6 +4,7 @@ use async_trait::async_trait;
|
||||
use futures::{io::BufReader, StreamExt};
|
||||
pub use language::*;
|
||||
use lazy_static::lazy_static;
|
||||
use lsp::LanguageServerBinary;
|
||||
use regex::Regex;
|
||||
use smol::fs::{self, File};
|
||||
use std::{any::Any, borrow::Cow, env::consts, path::PathBuf, str, sync::Arc};
|
||||
@ -78,20 +79,19 @@ impl LspAdapter for RustLspAdapter {
|
||||
container_dir: PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
(|| async move {
|
||||
let mut last = None;
|
||||
let mut entries = fs::read_dir(&container_dir).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
last = Some(entry?.path());
|
||||
}
|
||||
get_cached_server_binary(container_dir).await
|
||||
}
|
||||
|
||||
anyhow::Ok(LanguageServerBinary {
|
||||
path: last.ok_or_else(|| anyhow!("no cached binary"))?,
|
||||
arguments: Default::default(),
|
||||
async fn installation_test_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
get_cached_server_binary(container_dir)
|
||||
.await
|
||||
.map(|mut binary| {
|
||||
binary.arguments = vec!["--help".into()];
|
||||
binary
|
||||
})
|
||||
})()
|
||||
.await
|
||||
.log_err()
|
||||
}
|
||||
|
||||
async fn disk_based_diagnostic_sources(&self) -> Vec<String> {
|
||||
@ -258,6 +258,22 @@ impl LspAdapter for RustLspAdapter {
|
||||
})
|
||||
}
|
||||
}
|
||||
async fn get_cached_server_binary(container_dir: PathBuf) -> Option<LanguageServerBinary> {
|
||||
(|| async move {
|
||||
let mut last = None;
|
||||
let mut entries = fs::read_dir(&container_dir).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
last = Some(entry?.path());
|
||||
}
|
||||
|
||||
anyhow::Ok(LanguageServerBinary {
|
||||
path: last.ok_or_else(|| anyhow!("no cached binary"))?,
|
||||
arguments: Default::default(),
|
||||
})
|
||||
})()
|
||||
.await
|
||||
.log_err()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
@ -4,8 +4,8 @@ use async_tar::Archive;
|
||||
use async_trait::async_trait;
|
||||
use futures::{future::BoxFuture, FutureExt};
|
||||
use gpui::AppContext;
|
||||
use language::{LanguageServerBinary, LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use lsp::CodeActionKind;
|
||||
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use lsp::{CodeActionKind, LanguageServerBinary};
|
||||
use node_runtime::NodeRuntime;
|
||||
use serde_json::{json, Value};
|
||||
use smol::{fs, io::BufReader, stream::StreamExt};
|
||||
@ -104,28 +104,14 @@ impl LspAdapter for TypeScriptLspAdapter {
|
||||
container_dir: PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
(|| async move {
|
||||
let old_server_path = container_dir.join(Self::OLD_SERVER_PATH);
|
||||
let new_server_path = container_dir.join(Self::NEW_SERVER_PATH);
|
||||
if new_server_path.exists() {
|
||||
Ok(LanguageServerBinary {
|
||||
path: self.node.binary_path().await?,
|
||||
arguments: typescript_server_binary_arguments(&new_server_path),
|
||||
})
|
||||
} else if old_server_path.exists() {
|
||||
Ok(LanguageServerBinary {
|
||||
path: self.node.binary_path().await?,
|
||||
arguments: typescript_server_binary_arguments(&old_server_path),
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"missing executable in directory {:?}",
|
||||
container_dir
|
||||
))
|
||||
}
|
||||
})()
|
||||
.await
|
||||
.log_err()
|
||||
get_cached_ts_server_binary(container_dir, &self.node).await
|
||||
}
|
||||
|
||||
async fn installation_test_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
get_cached_ts_server_binary(container_dir, &self.node).await
|
||||
}
|
||||
|
||||
fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
|
||||
@ -173,6 +159,34 @@ impl LspAdapter for TypeScriptLspAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_ts_server_binary(
|
||||
container_dir: PathBuf,
|
||||
node: &NodeRuntime,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
(|| async move {
|
||||
let old_server_path = container_dir.join(TypeScriptLspAdapter::OLD_SERVER_PATH);
|
||||
let new_server_path = container_dir.join(TypeScriptLspAdapter::NEW_SERVER_PATH);
|
||||
if new_server_path.exists() {
|
||||
Ok(LanguageServerBinary {
|
||||
path: node.binary_path().await?,
|
||||
arguments: typescript_server_binary_arguments(&new_server_path),
|
||||
})
|
||||
} else if old_server_path.exists() {
|
||||
Ok(LanguageServerBinary {
|
||||
path: node.binary_path().await?,
|
||||
arguments: typescript_server_binary_arguments(&old_server_path),
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"missing executable in directory {:?}",
|
||||
container_dir
|
||||
))
|
||||
}
|
||||
})()
|
||||
.await
|
||||
.log_err()
|
||||
}
|
||||
|
||||
pub struct EsLintLspAdapter {
|
||||
node: Arc<NodeRuntime>,
|
||||
}
|
||||
@ -249,11 +263,11 @@ impl LspAdapter for EsLintLspAdapter {
|
||||
fs::rename(first.path(), &repo_root).await?;
|
||||
|
||||
self.node
|
||||
.run_npm_subcommand(&repo_root, "install", &[])
|
||||
.run_npm_subcommand(Some(&repo_root), "install", &[])
|
||||
.await?;
|
||||
|
||||
self.node
|
||||
.run_npm_subcommand(&repo_root, "run-script", &["compile"])
|
||||
.run_npm_subcommand(Some(&repo_root), "run-script", &["compile"])
|
||||
.await?;
|
||||
}
|
||||
|
||||
@ -268,21 +282,14 @@ impl LspAdapter for EsLintLspAdapter {
|
||||
container_dir: PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
(|| async move {
|
||||
// This is unfortunate but we don't know what the version is to build a path directly
|
||||
let mut dir = fs::read_dir(&container_dir).await?;
|
||||
let first = dir.next().await.ok_or(anyhow!("missing first file"))??;
|
||||
if !first.file_type().await?.is_dir() {
|
||||
return Err(anyhow!("First entry is not a directory"));
|
||||
}
|
||||
get_cached_eslint_server_binary(container_dir, &self.node).await
|
||||
}
|
||||
|
||||
Ok(LanguageServerBinary {
|
||||
path: first.path().join(Self::SERVER_PATH),
|
||||
arguments: Default::default(),
|
||||
})
|
||||
})()
|
||||
.await
|
||||
.log_err()
|
||||
async fn installation_test_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
get_cached_eslint_server_binary(container_dir, &self.node).await
|
||||
}
|
||||
|
||||
async fn label_for_completion(
|
||||
@ -298,6 +305,28 @@ impl LspAdapter for EsLintLspAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_eslint_server_binary(
|
||||
container_dir: PathBuf,
|
||||
node: &NodeRuntime,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
(|| async move {
|
||||
// This is unfortunate but we don't know what the version is to build a path directly
|
||||
let mut dir = fs::read_dir(&container_dir).await?;
|
||||
let first = dir.next().await.ok_or(anyhow!("missing first file"))??;
|
||||
if !first.file_type().await?.is_dir() {
|
||||
return Err(anyhow!("First entry is not a directory"));
|
||||
}
|
||||
let server_path = first.path().join(EsLintLspAdapter::SERVER_PATH);
|
||||
|
||||
Ok(LanguageServerBinary {
|
||||
path: node.binary_path().await?,
|
||||
arguments: eslint_server_binary_arguments(&server_path),
|
||||
})
|
||||
})()
|
||||
.await
|
||||
.log_err()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use gpui::TestAppContext;
|
||||
|
@ -3,9 +3,9 @@ use async_trait::async_trait;
|
||||
use futures::{future::BoxFuture, FutureExt, StreamExt};
|
||||
use gpui::AppContext;
|
||||
use language::{
|
||||
language_settings::all_language_settings, LanguageServerBinary, LanguageServerName, LspAdapter,
|
||||
LspAdapterDelegate,
|
||||
language_settings::all_language_settings, LanguageServerName, LspAdapter, LspAdapterDelegate,
|
||||
};
|
||||
use lsp::LanguageServerBinary;
|
||||
use node_runtime::NodeRuntime;
|
||||
use serde_json::Value;
|
||||
use smol::fs;
|
||||
@ -18,6 +18,8 @@ use std::{
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
const SERVER_PATH: &'static str = "node_modules/yaml-language-server/bin/yaml-language-server";
|
||||
|
||||
fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
|
||||
vec![server_path.into(), "--stdio".into()]
|
||||
}
|
||||
@ -27,8 +29,6 @@ pub struct YamlLspAdapter {
|
||||
}
|
||||
|
||||
impl YamlLspAdapter {
|
||||
const SERVER_PATH: &'static str = "node_modules/yaml-language-server/bin/yaml-language-server";
|
||||
|
||||
pub fn new(node: Arc<NodeRuntime>) -> Self {
|
||||
YamlLspAdapter { node }
|
||||
}
|
||||
@ -58,7 +58,7 @@ impl LspAdapter for YamlLspAdapter {
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Result<LanguageServerBinary> {
|
||||
let version = version.downcast::<String>().unwrap();
|
||||
let server_path = container_dir.join(Self::SERVER_PATH);
|
||||
let server_path = container_dir.join(SERVER_PATH);
|
||||
|
||||
if fs::metadata(&server_path).await.is_err() {
|
||||
self.node
|
||||
@ -77,33 +77,15 @@ impl LspAdapter for YamlLspAdapter {
|
||||
container_dir: PathBuf,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
(|| async move {
|
||||
let mut last_version_dir = None;
|
||||
let mut entries = fs::read_dir(&container_dir).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let entry = entry?;
|
||||
if entry.file_type().await?.is_dir() {
|
||||
last_version_dir = Some(entry.path());
|
||||
}
|
||||
}
|
||||
let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
|
||||
let server_path = last_version_dir.join(Self::SERVER_PATH);
|
||||
if server_path.exists() {
|
||||
Ok(LanguageServerBinary {
|
||||
path: self.node.binary_path().await?,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"missing executable in directory {:?}",
|
||||
last_version_dir
|
||||
))
|
||||
}
|
||||
})()
|
||||
.await
|
||||
.log_err()
|
||||
get_cached_server_binary(container_dir, &self.node).await
|
||||
}
|
||||
|
||||
async fn installation_test_binary(
|
||||
&self,
|
||||
container_dir: PathBuf,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
get_cached_server_binary(container_dir, &self.node).await
|
||||
}
|
||||
fn workspace_configuration(&self, cx: &mut AppContext) -> Option<BoxFuture<'static, Value>> {
|
||||
let tab_size = all_language_settings(None, cx)
|
||||
.language(Some("YAML"))
|
||||
@ -121,3 +103,34 @@ impl LspAdapter for YamlLspAdapter {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_server_binary(
|
||||
container_dir: PathBuf,
|
||||
node: &NodeRuntime,
|
||||
) -> Option<LanguageServerBinary> {
|
||||
(|| async move {
|
||||
let mut last_version_dir = None;
|
||||
let mut entries = fs::read_dir(&container_dir).await?;
|
||||
while let Some(entry) = entries.next().await {
|
||||
let entry = entry?;
|
||||
if entry.file_type().await?.is_dir() {
|
||||
last_version_dir = Some(entry.path());
|
||||
}
|
||||
}
|
||||
let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
|
||||
let server_path = last_version_dir.join(SERVER_PATH);
|
||||
if server_path.exists() {
|
||||
Ok(LanguageServerBinary {
|
||||
path: node.binary_path().await?,
|
||||
arguments: server_binary_arguments(&server_path),
|
||||
})
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"missing executable in directory {:?}",
|
||||
last_version_dir
|
||||
))
|
||||
}
|
||||
})()
|
||||
.await
|
||||
.log_err()
|
||||
}
|
||||
|
@ -131,7 +131,7 @@ fn main() {
|
||||
languages.set_executor(cx.background().clone());
|
||||
languages.set_language_server_download_dir(paths::LANGUAGES_DIR.clone());
|
||||
let languages = Arc::new(languages);
|
||||
let node_runtime = NodeRuntime::new(http.clone(), cx.background().to_owned());
|
||||
let node_runtime = NodeRuntime::instance(http.clone(), cx.background().to_owned());
|
||||
|
||||
languages::init(languages.clone(), node_runtime.clone());
|
||||
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx));
|
||||
|
@ -2144,7 +2144,7 @@ mod tests {
|
||||
languages.set_executor(cx.background().clone());
|
||||
let languages = Arc::new(languages);
|
||||
let http = FakeHttpClient::with_404_response();
|
||||
let node_runtime = NodeRuntime::new(http, cx.background().to_owned());
|
||||
let node_runtime = NodeRuntime::instance(http, cx.background().to_owned());
|
||||
languages::init(languages.clone(), node_runtime);
|
||||
for name in languages.language_names() {
|
||||
languages.language_for_name(&name);
|
||||
|
33
styles/.eslintrc.js
Normal file
33
styles/.eslintrc.js
Normal file
@ -0,0 +1,33 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:import/typescript",
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
},
|
||||
plugins: ["@typescript-eslint", "import"],
|
||||
globals: {
|
||||
module: true,
|
||||
},
|
||||
settings: {
|
||||
"import/parsers": {
|
||||
"@typescript-eslint/parser": [".ts"],
|
||||
},
|
||||
"import/resolver": {
|
||||
typescript: true,
|
||||
node: true,
|
||||
},
|
||||
"import/extensions": [".ts"],
|
||||
},
|
||||
rules: {
|
||||
"linebreak-style": ["error", "unix"],
|
||||
semi: ["error", "never"],
|
||||
},
|
||||
}
|
6
styles/.prettierrc
Normal file
6
styles/.prettierrc
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"semi": false,
|
||||
"printWidth": 80,
|
||||
"htmlWhitespaceSensitivity": "strict",
|
||||
"tabWidth": 4
|
||||
}
|
2682
styles/package-lock.json
generated
2682
styles/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,24 +1,22 @@
|
||||
{
|
||||
"name": "styles",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"description": "Typescript app that builds Zed's themes",
|
||||
"main": "./src/build_themes.ts",
|
||||
"scripts": {
|
||||
"build": "ts-node ./src/buildThemes.ts",
|
||||
"build-licenses": "ts-node ./src/buildLicenses.ts",
|
||||
"build-tokens": "ts-node ./src/buildTokens.ts",
|
||||
"build-types": "ts-node ./src/buildTypes.ts",
|
||||
"build": "ts-node ./src/build_themes.ts",
|
||||
"build-licenses": "ts-node ./src/build_licenses.ts",
|
||||
"build-tokens": "ts-node ./src/build_tokens.ts",
|
||||
"build-types": "ts-node ./src/build_types.ts",
|
||||
"test": "vitest"
|
||||
},
|
||||
"author": "",
|
||||
"author": "Zed Industries (https://github.com/zed-industries/)",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@tokens-studio/types": "^0.2.3",
|
||||
"@types/chroma-js": "^2.4.0",
|
||||
"@types/node": "^18.14.1",
|
||||
"ayu": "^8.0.1",
|
||||
"bezier-easing": "^2.1.0",
|
||||
"case-anything": "^2.1.10",
|
||||
"chroma-js": "^2.4.2",
|
||||
"deepmerge": "^4.3.0",
|
||||
"json-schema-to-typescript": "^13.0.2",
|
||||
@ -26,15 +24,13 @@
|
||||
"ts-deepmerge": "^6.0.3",
|
||||
"ts-node": "^10.9.1",
|
||||
"utility-types": "^3.10.0",
|
||||
"vitest": "^0.32.0"
|
||||
},
|
||||
"prettier": {
|
||||
"semi": false,
|
||||
"printWidth": 80,
|
||||
"htmlWhitespaceSensitivity": "strict",
|
||||
"tabWidth": 4
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/coverage-v8": "^0.32.0"
|
||||
"vitest": "^0.32.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.60.1",
|
||||
"@typescript-eslint/parser": "^5.60.1",
|
||||
"@vitest/coverage-v8": "^0.32.0",
|
||||
"eslint": "^8.43.0",
|
||||
"eslint-import-resolver-typescript": "^3.5.5",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"typescript": "^5.1.5"
|
||||
}
|
||||
}
|
||||
|
@ -1,50 +0,0 @@
|
||||
import * as fs from "fs"
|
||||
import toml from "toml"
|
||||
import { themes } from "./themes"
|
||||
import { ThemeConfig } from "./common"
|
||||
|
||||
const ACCEPTED_LICENSES_FILE = `${__dirname}/../../script/licenses/zed-licenses.toml`
|
||||
|
||||
// Use the cargo-about configuration file as the source of truth for supported licenses.
|
||||
function parseAcceptedToml(file: string): string[] {
|
||||
let buffer = fs.readFileSync(file).toString()
|
||||
|
||||
let obj = toml.parse(buffer)
|
||||
|
||||
if (!Array.isArray(obj.accepted)) {
|
||||
throw Error("Accepted license source is malformed")
|
||||
}
|
||||
|
||||
return obj.accepted
|
||||
}
|
||||
|
||||
function checkLicenses(themes: ThemeConfig[]) {
|
||||
for (const theme of themes) {
|
||||
if (!theme.licenseFile) {
|
||||
throw Error(`Theme ${theme.name} should have a LICENSE file`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function generateLicenseFile(themes: ThemeConfig[]) {
|
||||
checkLicenses(themes)
|
||||
for (const theme of themes) {
|
||||
const licenseText = fs.readFileSync(theme.licenseFile).toString()
|
||||
writeLicense(theme.name, licenseText, theme.licenseUrl)
|
||||
}
|
||||
}
|
||||
|
||||
function writeLicense(
|
||||
themeName: string,
|
||||
licenseText: string,
|
||||
licenseUrl?: string
|
||||
) {
|
||||
process.stdout.write(
|
||||
licenseUrl
|
||||
? `## [${themeName}](${licenseUrl})\n\n${licenseText}\n********************************************************************************\n\n`
|
||||
: `## ${themeName}\n\n${licenseText}\n********************************************************************************\n\n`
|
||||
)
|
||||
}
|
||||
|
||||
const acceptedLicenses = parseAcceptedToml(ACCEPTED_LICENSES_FILE)
|
||||
generateLicenseFile(themes)
|
@ -1,43 +0,0 @@
|
||||
import * as fs from "fs"
|
||||
import { tmpdir } from "os"
|
||||
import * as path from "path"
|
||||
import app from "./styleTree/app"
|
||||
import { ColorScheme, createColorScheme } from "./theme/colorScheme"
|
||||
import snakeCase from "./utils/snakeCase"
|
||||
import { themes } from "./themes"
|
||||
|
||||
const assetsDirectory = `${__dirname}/../../assets`
|
||||
const tempDirectory = fs.mkdtempSync(path.join(tmpdir(), "build-themes"))
|
||||
|
||||
// Clear existing themes
|
||||
function clearThemes(themeDirectory: string) {
|
||||
if (!fs.existsSync(themeDirectory)) {
|
||||
fs.mkdirSync(themeDirectory, { recursive: true })
|
||||
} else {
|
||||
for (const file of fs.readdirSync(themeDirectory)) {
|
||||
if (file.endsWith(".json")) {
|
||||
fs.unlinkSync(path.join(themeDirectory, file))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function writeThemes(colorSchemes: ColorScheme[], outputDirectory: string) {
|
||||
clearThemes(outputDirectory)
|
||||
for (let colorScheme of colorSchemes) {
|
||||
let styleTree = snakeCase(app(colorScheme))
|
||||
let styleTreeJSON = JSON.stringify(styleTree, null, 2)
|
||||
let tempPath = path.join(tempDirectory, `${colorScheme.name}.json`)
|
||||
let outPath = path.join(outputDirectory, `${colorScheme.name}.json`)
|
||||
fs.writeFileSync(tempPath, styleTreeJSON)
|
||||
fs.renameSync(tempPath, outPath)
|
||||
console.log(`- ${outPath} created`)
|
||||
}
|
||||
}
|
||||
|
||||
const colorSchemes: ColorScheme[] = themes.map((theme) =>
|
||||
createColorScheme(theme)
|
||||
)
|
||||
|
||||
// Write new themes to theme directory
|
||||
writeThemes(colorSchemes, `${assetsDirectory}/themes`)
|
@ -1,87 +0,0 @@
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import { ColorScheme, createColorScheme } from "./common"
|
||||
import { themes } from "./themes"
|
||||
import { slugify } from "./utils/slugify"
|
||||
import { colorSchemeTokens } from "./theme/tokens/colorScheme"
|
||||
|
||||
const TOKENS_DIRECTORY = path.join(__dirname, "..", "target", "tokens")
|
||||
const TOKENS_FILE = path.join(TOKENS_DIRECTORY, "$themes.json")
|
||||
const METADATA_FILE = path.join(TOKENS_DIRECTORY, "$metadata.json")
|
||||
|
||||
function clearTokens(tokensDirectory: string) {
|
||||
if (!fs.existsSync(tokensDirectory)) {
|
||||
fs.mkdirSync(tokensDirectory, { recursive: true })
|
||||
} else {
|
||||
for (const file of fs.readdirSync(tokensDirectory)) {
|
||||
if (file.endsWith(".json")) {
|
||||
fs.unlinkSync(path.join(tokensDirectory, file))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type TokenSet = {
|
||||
id: string
|
||||
name: string
|
||||
selectedTokenSets: { [key: string]: "enabled" }
|
||||
}
|
||||
|
||||
function buildTokenSetOrder(colorSchemes: ColorScheme[]): {
|
||||
tokenSetOrder: string[]
|
||||
} {
|
||||
const tokenSetOrder: string[] = colorSchemes.map((scheme) =>
|
||||
scheme.name.toLowerCase().replace(/\s+/g, "_")
|
||||
)
|
||||
return { tokenSetOrder }
|
||||
}
|
||||
|
||||
function buildThemesIndex(colorSchemes: ColorScheme[]): TokenSet[] {
|
||||
const themesIndex: TokenSet[] = colorSchemes.map((scheme, index) => {
|
||||
const id = `${scheme.isLight ? "light" : "dark"}_${scheme.name
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, "_")}_${index}`
|
||||
const selectedTokenSets: { [key: string]: "enabled" } = {}
|
||||
const tokenSet = scheme.name.toLowerCase().replace(/\s+/g, "_")
|
||||
selectedTokenSets[tokenSet] = "enabled"
|
||||
|
||||
return {
|
||||
id,
|
||||
name: `${scheme.name} - ${scheme.isLight ? "Light" : "Dark"}`,
|
||||
selectedTokenSets,
|
||||
}
|
||||
})
|
||||
|
||||
return themesIndex
|
||||
}
|
||||
|
||||
function writeTokens(colorSchemes: ColorScheme[], tokensDirectory: string) {
|
||||
clearTokens(tokensDirectory)
|
||||
|
||||
for (const colorScheme of colorSchemes) {
|
||||
const fileName = slugify(colorScheme.name) + ".json"
|
||||
const tokens = colorSchemeTokens(colorScheme)
|
||||
const tokensJSON = JSON.stringify(tokens, null, 2)
|
||||
const outPath = path.join(tokensDirectory, fileName)
|
||||
fs.writeFileSync(outPath, tokensJSON, { mode: 0o644 })
|
||||
console.log(`- ${outPath} created`)
|
||||
}
|
||||
|
||||
const themeIndexData = buildThemesIndex(colorSchemes)
|
||||
|
||||
const themesJSON = JSON.stringify(themeIndexData, null, 2)
|
||||
fs.writeFileSync(TOKENS_FILE, themesJSON, { mode: 0o644 })
|
||||
console.log(`- ${TOKENS_FILE} created`)
|
||||
|
||||
const tokenSetOrderData = buildTokenSetOrder(colorSchemes)
|
||||
|
||||
const metadataJSON = JSON.stringify(tokenSetOrderData, null, 2)
|
||||
fs.writeFileSync(METADATA_FILE, metadataJSON, { mode: 0o644 })
|
||||
console.log(`- ${METADATA_FILE} created`)
|
||||
}
|
||||
|
||||
const colorSchemes: ColorScheme[] = themes.map((theme) =>
|
||||
createColorScheme(theme)
|
||||
)
|
||||
|
||||
writeTokens(colorSchemes, TOKENS_DIRECTORY)
|
@ -1,64 +0,0 @@
|
||||
import * as fs from "fs/promises"
|
||||
import * as fsSync from "fs"
|
||||
import * as path from "path"
|
||||
import { compile } from "json-schema-to-typescript"
|
||||
|
||||
const BANNER = `/*
|
||||
* This file is autogenerated
|
||||
*/\n\n`
|
||||
const dirname = __dirname
|
||||
|
||||
async function main() {
|
||||
let schemasPath = path.join(dirname, "../../", "crates/theme/schemas")
|
||||
let schemaFiles = (await fs.readdir(schemasPath)).filter((x) =>
|
||||
x.endsWith(".json")
|
||||
)
|
||||
|
||||
let compiledTypes = new Set()
|
||||
|
||||
for (let filename of schemaFiles) {
|
||||
let filePath = path.join(schemasPath, filename)
|
||||
const fileContents = await fs.readFile(filePath)
|
||||
let schema = JSON.parse(fileContents.toString())
|
||||
let compiled = await compile(schema, schema.title, {
|
||||
bannerComment: "",
|
||||
})
|
||||
let eachType = compiled.split("export")
|
||||
for (let type of eachType) {
|
||||
if (!type) {
|
||||
continue
|
||||
}
|
||||
compiledTypes.add("export " + type.trim())
|
||||
}
|
||||
}
|
||||
|
||||
let output = BANNER + Array.from(compiledTypes).join("\n\n")
|
||||
let outputPath = path.join(dirname, "../../styles/src/types/zed.ts")
|
||||
|
||||
try {
|
||||
let existing = await fs.readFile(outputPath)
|
||||
if (existing.toString() == output) {
|
||||
// Skip writing if it hasn't changed
|
||||
console.log("Schemas are up to date")
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
// It's fine if there's no output from a previous run.
|
||||
// @ts-ignore
|
||||
if (e.code !== "ENOENT") {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
const typesDic = path.dirname(outputPath)
|
||||
if (!fsSync.existsSync(typesDic)) {
|
||||
await fs.mkdir(typesDic)
|
||||
}
|
||||
await fs.writeFile(outputPath, output)
|
||||
console.log(`Wrote Typescript types to ${outputPath}`)
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e)
|
||||
process.exit(1)
|
||||
})
|
50
styles/src/build_licenses.ts
Normal file
50
styles/src/build_licenses.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import * as fs from "fs"
|
||||
import toml from "toml"
|
||||
import { themes } from "./themes"
|
||||
import { ThemeConfig } from "./common"
|
||||
|
||||
const ACCEPTED_LICENSES_FILE = `${__dirname}/../../script/licenses/zed-licenses.toml`
|
||||
|
||||
// Use the cargo-about configuration file as the source of truth for supported licenses.
|
||||
function parse_accepted_toml(file: string): string[] {
|
||||
const buffer = fs.readFileSync(file).toString()
|
||||
|
||||
const obj = toml.parse(buffer)
|
||||
|
||||
if (!Array.isArray(obj.accepted)) {
|
||||
throw Error("Accepted license source is malformed")
|
||||
}
|
||||
|
||||
return obj.accepted
|
||||
}
|
||||
|
||||
function check_licenses(themes: ThemeConfig[]) {
|
||||
for (const theme of themes) {
|
||||
if (!theme.license_file) {
|
||||
throw Error(`Theme ${theme.name} should have a LICENSE file`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function generate_license_file(themes: ThemeConfig[]) {
|
||||
check_licenses(themes)
|
||||
for (const theme of themes) {
|
||||
const license_text = fs.readFileSync(theme.license_file).toString()
|
||||
write_license(theme.name, license_text, theme.license_url)
|
||||
}
|
||||
}
|
||||
|
||||
function write_license(
|
||||
theme_name: string,
|
||||
license_text: string,
|
||||
license_url?: string
|
||||
) {
|
||||
process.stdout.write(
|
||||
license_url
|
||||
? `## [${theme_name}](${license_url})\n\n${license_text}\n********************************************************************************\n\n`
|
||||
: `## ${theme_name}\n\n${license_text}\n********************************************************************************\n\n`
|
||||
)
|
||||
}
|
||||
|
||||
const accepted_licenses = parse_accepted_toml(ACCEPTED_LICENSES_FILE)
|
||||
generate_license_file(themes)
|
43
styles/src/build_themes.ts
Normal file
43
styles/src/build_themes.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import * as fs from "fs"
|
||||
import { tmpdir } from "os"
|
||||
import * as path from "path"
|
||||
import app from "./style_tree/app"
|
||||
import { ColorScheme, create_color_scheme } from "./theme/color_scheme"
|
||||
import { themes } from "./themes"
|
||||
|
||||
const assets_directory = `${__dirname}/../../assets`
|
||||
const temp_directory = fs.mkdtempSync(path.join(tmpdir(), "build-themes"))
|
||||
|
||||
function clear_themes(theme_directory: string) {
|
||||
if (!fs.existsSync(theme_directory)) {
|
||||
fs.mkdirSync(theme_directory, { recursive: true })
|
||||
} else {
|
||||
for (const file of fs.readdirSync(theme_directory)) {
|
||||
if (file.endsWith(".json")) {
|
||||
fs.unlinkSync(path.join(theme_directory, file))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function write_themes(themes: ColorScheme[], output_directory: string) {
|
||||
clear_themes(output_directory)
|
||||
for (const color_scheme of themes) {
|
||||
const style_tree = app(color_scheme)
|
||||
const style_tree_json = JSON.stringify(style_tree, null, 2)
|
||||
const temp_path = path.join(temp_directory, `${color_scheme.name}.json`)
|
||||
const out_path = path.join(
|
||||
output_directory,
|
||||
`${color_scheme.name}.json`
|
||||
)
|
||||
fs.writeFileSync(temp_path, style_tree_json)
|
||||
fs.renameSync(temp_path, out_path)
|
||||
console.log(`- ${out_path} created`)
|
||||
}
|
||||
}
|
||||
|
||||
const all_themes: ColorScheme[] = themes.map((theme) =>
|
||||
create_color_scheme(theme)
|
||||
)
|
||||
|
||||
write_themes(all_themes, `${assets_directory}/themes`)
|
87
styles/src/build_tokens.ts
Normal file
87
styles/src/build_tokens.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import * as fs from "fs"
|
||||
import * as path from "path"
|
||||
import { ColorScheme, create_color_scheme } from "./common"
|
||||
import { themes } from "./themes"
|
||||
import { slugify } from "./utils/slugify"
|
||||
import { theme_tokens } from "./theme/tokens/color_scheme"
|
||||
|
||||
const TOKENS_DIRECTORY = path.join(__dirname, "..", "target", "tokens")
|
||||
const TOKENS_FILE = path.join(TOKENS_DIRECTORY, "$themes.json")
|
||||
const METADATA_FILE = path.join(TOKENS_DIRECTORY, "$metadata.json")
|
||||
|
||||
function clear_tokens(tokens_directory: string) {
|
||||
if (!fs.existsSync(tokens_directory)) {
|
||||
fs.mkdirSync(tokens_directory, { recursive: true })
|
||||
} else {
|
||||
for (const file of fs.readdirSync(tokens_directory)) {
|
||||
if (file.endsWith(".json")) {
|
||||
fs.unlinkSync(path.join(tokens_directory, file))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type TokenSet = {
|
||||
id: string
|
||||
name: string
|
||||
selected_token_sets: { [key: string]: "enabled" }
|
||||
}
|
||||
|
||||
function build_token_set_order(theme: ColorScheme[]): {
|
||||
token_set_order: string[]
|
||||
} {
|
||||
const token_set_order: string[] = theme.map((scheme) =>
|
||||
scheme.name.toLowerCase().replace(/\s+/g, "_")
|
||||
)
|
||||
return { token_set_order }
|
||||
}
|
||||
|
||||
function build_themes_index(theme: ColorScheme[]): TokenSet[] {
|
||||
const themes_index: TokenSet[] = theme.map((scheme, index) => {
|
||||
const id = `${scheme.is_light ? "light" : "dark"}_${scheme.name
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, "_")}_${index}`
|
||||
const selected_token_sets: { [key: string]: "enabled" } = {}
|
||||
const token_set = scheme.name.toLowerCase().replace(/\s+/g, "_")
|
||||
selected_token_sets[token_set] = "enabled"
|
||||
|
||||
return {
|
||||
id,
|
||||
name: `${scheme.name} - ${scheme.is_light ? "Light" : "Dark"}`,
|
||||
selected_token_sets,
|
||||
}
|
||||
})
|
||||
|
||||
return themes_index
|
||||
}
|
||||
|
||||
function write_tokens(themes: ColorScheme[], tokens_directory: string) {
|
||||
clear_tokens(tokens_directory)
|
||||
|
||||
for (const theme of themes) {
|
||||
const file_name = slugify(theme.name) + ".json"
|
||||
const tokens = theme_tokens(theme)
|
||||
const tokens_json = JSON.stringify(tokens, null, 2)
|
||||
const out_path = path.join(tokens_directory, file_name)
|
||||
fs.writeFileSync(out_path, tokens_json, { mode: 0o644 })
|
||||
console.log(`- ${out_path} created`)
|
||||
}
|
||||
|
||||
const theme_index_data = build_themes_index(themes)
|
||||
|
||||
const themes_json = JSON.stringify(theme_index_data, null, 2)
|
||||
fs.writeFileSync(TOKENS_FILE, themes_json, { mode: 0o644 })
|
||||
console.log(`- ${TOKENS_FILE} created`)
|
||||
|
||||
const token_set_order_data = build_token_set_order(themes)
|
||||
|
||||
const metadata_json = JSON.stringify(token_set_order_data, null, 2)
|
||||
fs.writeFileSync(METADATA_FILE, metadata_json, { mode: 0o644 })
|
||||
console.log(`- ${METADATA_FILE} created`)
|
||||
}
|
||||
|
||||
const all_themes: ColorScheme[] = themes.map((theme) =>
|
||||
create_color_scheme(theme)
|
||||
)
|
||||
|
||||
write_tokens(all_themes, TOKENS_DIRECTORY)
|
62
styles/src/build_types.ts
Normal file
62
styles/src/build_types.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import * as fs from "fs/promises"
|
||||
import * as fsSync from "fs"
|
||||
import * as path from "path"
|
||||
import { compile } from "json-schema-to-typescript"
|
||||
|
||||
const BANNER = `/*
|
||||
* This file is autogenerated
|
||||
*/\n\n`
|
||||
const dirname = __dirname
|
||||
|
||||
async function main() {
|
||||
const schemas_path = path.join(dirname, "../../", "crates/theme/schemas")
|
||||
const schema_files = (await fs.readdir(schemas_path)).filter((x) =>
|
||||
x.endsWith(".json")
|
||||
)
|
||||
|
||||
const compiled_types = new Set()
|
||||
|
||||
for (const filename of schema_files) {
|
||||
const file_path = path.join(schemas_path, filename)
|
||||
const file_contents = await fs.readFile(file_path)
|
||||
const schema = JSON.parse(file_contents.toString())
|
||||
const compiled = await compile(schema, schema.title, {
|
||||
bannerComment: "",
|
||||
})
|
||||
const each_type = compiled.split("export")
|
||||
for (const type of each_type) {
|
||||
if (!type) {
|
||||
continue
|
||||
}
|
||||
compiled_types.add("export " + type.trim())
|
||||
}
|
||||
}
|
||||
|
||||
const output = BANNER + Array.from(compiled_types).join("\n\n")
|
||||
const output_path = path.join(dirname, "../../styles/src/types/zed.ts")
|
||||
|
||||
try {
|
||||
const existing = await fs.readFile(output_path)
|
||||
if (existing.toString() == output) {
|
||||
// Skip writing if it hasn't changed
|
||||
console.log("Schemas are up to date")
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.code !== "ENOENT") {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
const types_dic = path.dirname(output_path)
|
||||
if (!fsSync.existsSync(types_dic)) {
|
||||
await fs.mkdir(types_dic)
|
||||
}
|
||||
await fs.writeFile(output_path, output)
|
||||
console.log(`Wrote Typescript types to ${output_path}`)
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e)
|
||||
process.exit(1)
|
||||
})
|
@ -2,42 +2,24 @@ import chroma from "chroma-js"
|
||||
export * from "./theme"
|
||||
export { chroma }
|
||||
|
||||
export const fontFamilies = {
|
||||
export const font_families = {
|
||||
sans: "Zed Sans",
|
||||
mono: "Zed Mono",
|
||||
}
|
||||
|
||||
export const fontSizes = {
|
||||
"3xs": 8,
|
||||
export const font_sizes = {
|
||||
"2xs": 10,
|
||||
xs: 12,
|
||||
sm: 14,
|
||||
md: 16,
|
||||
lg: 18,
|
||||
xl: 20,
|
||||
}
|
||||
|
||||
export type FontWeight =
|
||||
| "thin"
|
||||
| "extra_light"
|
||||
| "light"
|
||||
| "normal"
|
||||
| "medium"
|
||||
| "semibold"
|
||||
| "bold"
|
||||
| "extra_bold"
|
||||
| "black"
|
||||
export type FontWeight = "normal" | "bold"
|
||||
|
||||
export const fontWeights: { [key: string]: FontWeight } = {
|
||||
thin: "thin",
|
||||
extra_light: "extra_light",
|
||||
light: "light",
|
||||
export const font_weights: { [key: string]: FontWeight } = {
|
||||
normal: "normal",
|
||||
medium: "medium",
|
||||
semibold: "semibold",
|
||||
bold: "bold",
|
||||
extra_bold: "extra_bold",
|
||||
black: "black",
|
||||
}
|
||||
|
||||
export const sizes = {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ColorScheme } from "../common"
|
||||
import { interactive, toggleable } from "../element"
|
||||
import { background, foreground } from "../styleTree/components"
|
||||
import { background, foreground } from "../style_tree/components"
|
||||
import { ColorScheme } from "../theme/color_scheme"
|
||||
|
||||
export type Margin = {
|
||||
top: number
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { ColorScheme } from "../common"
|
||||
import { interactive, toggleable } from "../element"
|
||||
import {
|
||||
TextProperties,
|
||||
background,
|
||||
foreground,
|
||||
text,
|
||||
} from "../styleTree/components"
|
||||
} from "../style_tree/components"
|
||||
import { ColorScheme } from "../theme/color_scheme"
|
||||
import { Margin } from "./icon_button"
|
||||
|
||||
interface TextButtonOptions {
|
||||
|
@ -8,7 +8,7 @@ import { describe, it, expect } from "vitest"
|
||||
describe("interactive", () => {
|
||||
it("creates an Interactive<Element> with base properties and states", () => {
|
||||
const result = interactive({
|
||||
base: { fontSize: 10, color: "#FFFFFF" },
|
||||
base: { font_size: 10, color: "#FFFFFF" },
|
||||
state: {
|
||||
hovered: { color: "#EEEEEE" },
|
||||
clicked: { color: "#CCCCCC" },
|
||||
@ -16,25 +16,25 @@ describe("interactive", () => {
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
default: { color: "#FFFFFF", fontSize: 10 },
|
||||
hovered: { color: "#EEEEEE", fontSize: 10 },
|
||||
clicked: { color: "#CCCCCC", fontSize: 10 },
|
||||
default: { color: "#FFFFFF", font_size: 10 },
|
||||
hovered: { color: "#EEEEEE", font_size: 10 },
|
||||
clicked: { color: "#CCCCCC", font_size: 10 },
|
||||
})
|
||||
})
|
||||
|
||||
it("creates an Interactive<Element> with no base properties", () => {
|
||||
const result = interactive({
|
||||
state: {
|
||||
default: { color: "#FFFFFF", fontSize: 10 },
|
||||
default: { color: "#FFFFFF", font_size: 10 },
|
||||
hovered: { color: "#EEEEEE" },
|
||||
clicked: { color: "#CCCCCC" },
|
||||
},
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
default: { color: "#FFFFFF", fontSize: 10 },
|
||||
hovered: { color: "#EEEEEE", fontSize: 10 },
|
||||
clicked: { color: "#CCCCCC", fontSize: 10 },
|
||||
default: { color: "#FFFFFF", font_size: 10 },
|
||||
hovered: { color: "#EEEEEE", font_size: 10 },
|
||||
clicked: { color: "#CCCCCC", font_size: 10 },
|
||||
})
|
||||
})
|
||||
|
||||
@ -48,7 +48,7 @@ describe("interactive", () => {
|
||||
|
||||
it("throws error when no other state besides default is present", () => {
|
||||
const state = {
|
||||
default: { fontSize: 10 },
|
||||
default: { font_size: 10 },
|
||||
}
|
||||
|
||||
expect(() => interactive({ state })).toThrow(NOT_ENOUGH_STATES_ERROR)
|
||||
|
@ -37,61 +37,61 @@ interface InteractiveProps<T> {
|
||||
* @param state Object containing optional modified fields to be included in the resulting object for each state.
|
||||
* @returns Interactive<T> object with fields from `base` and `state`.
|
||||
*/
|
||||
export function interactive<T extends Object>({
|
||||
export function interactive<T extends object>({
|
||||
base,
|
||||
state,
|
||||
}: InteractiveProps<T>): Interactive<T> {
|
||||
if (!base && !state.default) throw new Error(NO_DEFAULT_OR_BASE_ERROR)
|
||||
|
||||
let defaultState: T
|
||||
let default_state: T
|
||||
|
||||
if (state.default && base) {
|
||||
defaultState = merge(base, state.default) as T
|
||||
default_state = merge(base, state.default) as T
|
||||
} else {
|
||||
defaultState = base ? base : (state.default as T)
|
||||
default_state = base ? base : (state.default as T)
|
||||
}
|
||||
|
||||
let interactiveObj: Interactive<T> = {
|
||||
default: defaultState,
|
||||
const interactive_obj: Interactive<T> = {
|
||||
default: default_state,
|
||||
}
|
||||
|
||||
let stateCount = 0
|
||||
let state_count = 0
|
||||
|
||||
if (state.hovered !== undefined) {
|
||||
interactiveObj.hovered = merge(
|
||||
interactiveObj.default,
|
||||
interactive_obj.hovered = merge(
|
||||
interactive_obj.default,
|
||||
state.hovered
|
||||
) as T
|
||||
stateCount++
|
||||
state_count++
|
||||
}
|
||||
|
||||
if (state.clicked !== undefined) {
|
||||
interactiveObj.clicked = merge(
|
||||
interactiveObj.default,
|
||||
interactive_obj.clicked = merge(
|
||||
interactive_obj.default,
|
||||
state.clicked
|
||||
) as T
|
||||
stateCount++
|
||||
state_count++
|
||||
}
|
||||
|
||||
if (state.selected !== undefined) {
|
||||
interactiveObj.selected = merge(
|
||||
interactiveObj.default,
|
||||
interactive_obj.selected = merge(
|
||||
interactive_obj.default,
|
||||
state.selected
|
||||
) as T
|
||||
stateCount++
|
||||
state_count++
|
||||
}
|
||||
|
||||
if (state.disabled !== undefined) {
|
||||
interactiveObj.disabled = merge(
|
||||
interactiveObj.default,
|
||||
interactive_obj.disabled = merge(
|
||||
interactive_obj.default,
|
||||
state.disabled
|
||||
) as T
|
||||
stateCount++
|
||||
state_count++
|
||||
}
|
||||
|
||||
if (stateCount < 1) {
|
||||
if (state_count < 1) {
|
||||
throw new Error(NOT_ENOUGH_STATES_ERROR)
|
||||
}
|
||||
|
||||
return interactiveObj
|
||||
return interactive_obj
|
||||
}
|
||||
|
@ -35,13 +35,13 @@ export function toggleable<T extends object>(
|
||||
if (!base && !state.inactive) throw new Error(NO_INACTIVE_OR_BASE_ERROR)
|
||||
if (!state.active) throw new Error(NO_ACTIVE_ERROR)
|
||||
|
||||
const inactiveState = base
|
||||
const inactive_state = base
|
||||
? ((state.inactive ? merge(base, state.inactive) : base) as T)
|
||||
: (state.inactive as T)
|
||||
|
||||
const toggleObj: Toggleable<T> = {
|
||||
inactive: inactiveState,
|
||||
const toggle_obj: Toggleable<T> = {
|
||||
inactive: inactive_state,
|
||||
active: merge(base ?? {}, state.active) as T,
|
||||
}
|
||||
return toggleObj
|
||||
return toggle_obj
|
||||
}
|
||||
|
@ -1,75 +0,0 @@
|
||||
import contactFinder from "./contactFinder"
|
||||
import contactsPopover from "./contactsPopover"
|
||||
import commandPalette from "./commandPalette"
|
||||
import editor from "./editor"
|
||||
import projectPanel from "./projectPanel"
|
||||
import search from "./search"
|
||||
import picker from "./picker"
|
||||
import workspace from "./workspace"
|
||||
import contextMenu from "./contextMenu"
|
||||
import sharedScreen from "./sharedScreen"
|
||||
import projectDiagnostics from "./projectDiagnostics"
|
||||
import contactNotification from "./contactNotification"
|
||||
import updateNotification from "./updateNotification"
|
||||
import simpleMessageNotification from "./simpleMessageNotification"
|
||||
import projectSharedNotification from "./projectSharedNotification"
|
||||
import tooltip from "./tooltip"
|
||||
import terminal from "./terminal"
|
||||
import contactList from "./contactList"
|
||||
import toolbarDropdownMenu from "./toolbarDropdownMenu"
|
||||
import incomingCallNotification from "./incomingCallNotification"
|
||||
import { ColorScheme } from "../theme/colorScheme"
|
||||
import feedback from "./feedback"
|
||||
import welcome from "./welcome"
|
||||
import copilot from "./copilot"
|
||||
import assistant from "./assistant"
|
||||
import { titlebar } from "./titlebar"
|
||||
|
||||
export default function app(colorScheme: ColorScheme): Object {
|
||||
return {
|
||||
meta: {
|
||||
name: colorScheme.name,
|
||||
isLight: colorScheme.isLight,
|
||||
},
|
||||
commandPalette: commandPalette(colorScheme),
|
||||
contactNotification: contactNotification(colorScheme),
|
||||
projectSharedNotification: projectSharedNotification(colorScheme),
|
||||
incomingCallNotification: incomingCallNotification(colorScheme),
|
||||
picker: picker(colorScheme),
|
||||
workspace: workspace(colorScheme),
|
||||
titlebar: titlebar(colorScheme),
|
||||
copilot: copilot(colorScheme),
|
||||
welcome: welcome(colorScheme),
|
||||
contextMenu: contextMenu(colorScheme),
|
||||
editor: editor(colorScheme),
|
||||
projectDiagnostics: projectDiagnostics(colorScheme),
|
||||
projectPanel: projectPanel(colorScheme),
|
||||
contactsPopover: contactsPopover(colorScheme),
|
||||
contactFinder: contactFinder(colorScheme),
|
||||
contactList: contactList(colorScheme),
|
||||
toolbarDropdownMenu: toolbarDropdownMenu(colorScheme),
|
||||
search: search(colorScheme),
|
||||
sharedScreen: sharedScreen(colorScheme),
|
||||
updateNotification: updateNotification(colorScheme),
|
||||
simpleMessageNotification: simpleMessageNotification(colorScheme),
|
||||
tooltip: tooltip(colorScheme),
|
||||
terminal: terminal(colorScheme),
|
||||
assistant: assistant(colorScheme),
|
||||
feedback: feedback(colorScheme),
|
||||
colorScheme: {
|
||||
...colorScheme,
|
||||
players: Object.values(colorScheme.players),
|
||||
ramps: {
|
||||
neutral: colorScheme.ramps.neutral.colors(100, "hex"),
|
||||
red: colorScheme.ramps.red.colors(100, "hex"),
|
||||
orange: colorScheme.ramps.orange.colors(100, "hex"),
|
||||
yellow: colorScheme.ramps.yellow.colors(100, "hex"),
|
||||
green: colorScheme.ramps.green.colors(100, "hex"),
|
||||
cyan: colorScheme.ramps.cyan.colors(100, "hex"),
|
||||
blue: colorScheme.ramps.blue.colors(100, "hex"),
|
||||
violet: colorScheme.ramps.violet.colors(100, "hex"),
|
||||
magenta: colorScheme.ramps.magenta.colors(100, "hex"),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
import picker from "./picker"
|
||||
import { ColorScheme } from "../theme/colorScheme"
|
||||
import { background, border, foreground, text } from "./components"
|
||||
|
||||
export default function contactFinder(colorScheme: ColorScheme): any {
|
||||
let layer = colorScheme.middle
|
||||
|
||||
const sideMargin = 6
|
||||
const contactButton = {
|
||||
background: background(layer, "variant"),
|
||||
color: foreground(layer, "variant"),
|
||||
iconWidth: 8,
|
||||
buttonWidth: 16,
|
||||
cornerRadius: 8,
|
||||
}
|
||||
|
||||
const pickerStyle = picker(colorScheme)
|
||||
const pickerInput = {
|
||||
background: background(layer, "on"),
|
||||
cornerRadius: 6,
|
||||
text: text(layer, "mono"),
|
||||
placeholderText: text(layer, "mono", "on", "disabled", { size: "xs" }),
|
||||
selection: colorScheme.players[0],
|
||||
border: border(layer),
|
||||
padding: {
|
||||
bottom: 4,
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 4,
|
||||
},
|
||||
margin: {
|
||||
left: sideMargin,
|
||||
right: sideMargin,
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
picker: {
|
||||
emptyContainer: {},
|
||||
item: {
|
||||
...pickerStyle.item,
|
||||
margin: { left: sideMargin, right: sideMargin },
|
||||
},
|
||||
noMatches: pickerStyle.noMatches,
|
||||
inputEditor: pickerInput,
|
||||
emptyInputEditor: pickerInput,
|
||||
},
|
||||
rowHeight: 28,
|
||||
contactAvatar: {
|
||||
cornerRadius: 10,
|
||||
width: 18,
|
||||
},
|
||||
contactUsername: {
|
||||
padding: {
|
||||
left: 8,
|
||||
},
|
||||
},
|
||||
contactButton: {
|
||||
...contactButton,
|
||||
hover: {
|
||||
background: background(layer, "variant", "hovered"),
|
||||
},
|
||||
},
|
||||
disabledContactButton: {
|
||||
...contactButton,
|
||||
background: background(layer, "disabled"),
|
||||
color: foreground(layer, "disabled"),
|
||||
},
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
import { ColorScheme } from "../theme/colorScheme"
|
||||
import { background, foreground, text } from "./components"
|
||||
import { interactive } from "../element"
|
||||
const avatarSize = 12
|
||||
const headerPadding = 8
|
||||
|
||||
export default function contactNotification(colorScheme: ColorScheme): Object {
|
||||
let layer = colorScheme.lowest
|
||||
return {
|
||||
headerAvatar: {
|
||||
height: avatarSize,
|
||||
width: avatarSize,
|
||||
cornerRadius: 6,
|
||||
},
|
||||
headerMessage: {
|
||||
...text(layer, "sans", { size: "xs" }),
|
||||
margin: { left: headerPadding, right: headerPadding },
|
||||
},
|
||||
headerHeight: 18,
|
||||
bodyMessage: {
|
||||
...text(layer, "sans", { size: "xs" }),
|
||||
margin: { left: avatarSize + headerPadding, top: 6, bottom: 6 },
|
||||
},
|
||||
button: interactive({
|
||||
base: {
|
||||
...text(layer, "sans", "on", { size: "xs" }),
|
||||
background: background(layer, "on"),
|
||||
padding: 4,
|
||||
cornerRadius: 6,
|
||||
margin: { left: 6 },
|
||||
},
|
||||
|
||||
state: {
|
||||
hovered: {
|
||||
background: background(layer, "on", "hovered"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
dismissButton: {
|
||||
default: {
|
||||
color: foreground(layer, "variant"),
|
||||
iconWidth: 8,
|
||||
iconHeight: 8,
|
||||
buttonWidth: 8,
|
||||
buttonHeight: 8,
|
||||
hover: {
|
||||
color: foreground(layer, "hovered"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import { ColorScheme } from "../theme/colorScheme"
|
||||
import { background, border, text } from "./components"
|
||||
|
||||
export default function contactsPopover(colorScheme: ColorScheme) {
|
||||
let layer = colorScheme.middle
|
||||
const sidePadding = 12
|
||||
return {
|
||||
background: background(layer),
|
||||
cornerRadius: 6,
|
||||
padding: { top: 6, bottom: 6 },
|
||||
shadow: colorScheme.popoverShadow,
|
||||
border: border(layer),
|
||||
width: 300,
|
||||
height: 400,
|
||||
}
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
import { ColorScheme } from "../theme/colorScheme"
|
||||
import { background, border, borderColor, text } from "./components"
|
||||
import { interactive, toggleable } from "../element"
|
||||
|
||||
export default function contextMenu(colorScheme: ColorScheme) {
|
||||
let layer = colorScheme.middle
|
||||
return {
|
||||
background: background(layer),
|
||||
cornerRadius: 10,
|
||||
padding: 4,
|
||||
shadow: colorScheme.popoverShadow,
|
||||
border: border(layer),
|
||||
keystrokeMargin: 30,
|
||||
item: toggleable({
|
||||
base: interactive({
|
||||
base: {
|
||||
iconSpacing: 8,
|
||||
iconWidth: 14,
|
||||
padding: { left: 6, right: 6, top: 2, bottom: 2 },
|
||||
cornerRadius: 6,
|
||||
label: text(layer, "sans", { size: "sm" }),
|
||||
keystroke: {
|
||||
...text(layer, "sans", "variant", {
|
||||
size: "sm",
|
||||
weight: "bold",
|
||||
}),
|
||||
padding: { left: 3, right: 3 },
|
||||
},
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
background: background(layer, "hovered"),
|
||||
label: text(layer, "sans", "hovered", { size: "sm" }),
|
||||
keystroke: {
|
||||
...text(layer, "sans", "hovered", {
|
||||
size: "sm",
|
||||
weight: "bold",
|
||||
}),
|
||||
padding: { left: 3, right: 3 },
|
||||
},
|
||||
},
|
||||
clicked: {
|
||||
background: background(layer, "pressed"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
state: {
|
||||
active: {
|
||||
default: {
|
||||
background: background(layer, "active"),
|
||||
},
|
||||
hovered: {
|
||||
background: background(layer, "hovered"),
|
||||
},
|
||||
clicked: {
|
||||
background: background(layer, "pressed"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
separator: {
|
||||
background: borderColor(layer),
|
||||
margin: { top: 2, bottom: 2 },
|
||||
},
|
||||
}
|
||||
}
|
@ -1,314 +0,0 @@
|
||||
import { withOpacity } from "../theme/color"
|
||||
import { ColorScheme, Layer, StyleSets } from "../theme/colorScheme"
|
||||
import { background, border, borderColor, foreground, text } from "./components"
|
||||
import hoverPopover from "./hoverPopover"
|
||||
|
||||
import { buildSyntax } from "../theme/syntax"
|
||||
import { interactive, toggleable } from "../element"
|
||||
|
||||
export default function editor(colorScheme: ColorScheme) {
|
||||
const { isLight } = colorScheme
|
||||
|
||||
let layer = colorScheme.highest
|
||||
|
||||
const autocompleteItem = {
|
||||
cornerRadius: 6,
|
||||
padding: {
|
||||
bottom: 2,
|
||||
left: 6,
|
||||
right: 6,
|
||||
top: 2,
|
||||
},
|
||||
}
|
||||
|
||||
function diagnostic(layer: Layer, styleSet: StyleSets) {
|
||||
return {
|
||||
textScaleFactor: 0.857,
|
||||
header: {
|
||||
border: border(layer, {
|
||||
top: true,
|
||||
}),
|
||||
},
|
||||
message: {
|
||||
text: text(layer, "sans", styleSet, "default", { size: "sm" }),
|
||||
highlightText: text(layer, "sans", styleSet, "default", {
|
||||
size: "sm",
|
||||
weight: "bold",
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const syntax = buildSyntax(colorScheme)
|
||||
|
||||
return {
|
||||
textColor: syntax.primary.color,
|
||||
background: background(layer),
|
||||
activeLineBackground: withOpacity(background(layer, "on"), 0.75),
|
||||
highlightedLineBackground: background(layer, "on"),
|
||||
// Inline autocomplete suggestions, Co-pilot suggestions, etc.
|
||||
suggestion: syntax.predictive,
|
||||
codeActions: {
|
||||
indicator: toggleable({
|
||||
base: interactive({
|
||||
base: {
|
||||
color: foreground(layer, "variant"),
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
color: foreground(layer, "variant", "hovered"),
|
||||
},
|
||||
clicked: {
|
||||
color: foreground(layer, "variant", "pressed"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
state: {
|
||||
active: {
|
||||
default: {
|
||||
color: foreground(layer, "accent"),
|
||||
},
|
||||
hovered: {
|
||||
color: foreground(layer, "accent", "hovered"),
|
||||
},
|
||||
clicked: {
|
||||
color: foreground(layer, "accent", "pressed"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
verticalScale: 0.55,
|
||||
},
|
||||
folds: {
|
||||
iconMarginScale: 2.5,
|
||||
foldedIcon: "icons/chevron_right_8.svg",
|
||||
foldableIcon: "icons/chevron_down_8.svg",
|
||||
indicator: toggleable({
|
||||
base: interactive({
|
||||
base: {
|
||||
color: foreground(layer, "variant"),
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
color: foreground(layer, "on"),
|
||||
},
|
||||
clicked: {
|
||||
color: foreground(layer, "base"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
state: {
|
||||
active: {
|
||||
default: {
|
||||
color: foreground(layer, "default"),
|
||||
},
|
||||
hovered: {
|
||||
color: foreground(layer, "variant"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
ellipses: {
|
||||
textColor: colorScheme.ramps.neutral(0.71).hex(),
|
||||
cornerRadiusFactor: 0.15,
|
||||
background: {
|
||||
// Copied from hover_popover highlight
|
||||
default: {
|
||||
color: colorScheme.ramps.neutral(0.5).alpha(0.0).hex(),
|
||||
},
|
||||
|
||||
hovered: {
|
||||
color: colorScheme.ramps.neutral(0.5).alpha(0.5).hex(),
|
||||
},
|
||||
|
||||
clicked: {
|
||||
color: colorScheme.ramps.neutral(0.5).alpha(0.7).hex(),
|
||||
},
|
||||
},
|
||||
},
|
||||
foldBackground: foreground(layer, "variant"),
|
||||
},
|
||||
diff: {
|
||||
deleted: isLight
|
||||
? colorScheme.ramps.red(0.5).hex()
|
||||
: colorScheme.ramps.red(0.4).hex(),
|
||||
modified: isLight
|
||||
? colorScheme.ramps.yellow(0.5).hex()
|
||||
: colorScheme.ramps.yellow(0.5).hex(),
|
||||
inserted: isLight
|
||||
? colorScheme.ramps.green(0.4).hex()
|
||||
: colorScheme.ramps.green(0.5).hex(),
|
||||
removedWidthEm: 0.275,
|
||||
widthEm: 0.15,
|
||||
cornerRadius: 0.05,
|
||||
},
|
||||
/** Highlights matching occurrences of what is under the cursor
|
||||
* as well as matched brackets
|
||||
*/
|
||||
documentHighlightReadBackground: withOpacity(
|
||||
foreground(layer, "accent"),
|
||||
0.1
|
||||
),
|
||||
documentHighlightWriteBackground: colorScheme.ramps
|
||||
.neutral(0.5)
|
||||
.alpha(0.4)
|
||||
.hex(), // TODO: This was blend * 2
|
||||
errorColor: background(layer, "negative"),
|
||||
gutterBackground: background(layer),
|
||||
gutterPaddingFactor: 3.5,
|
||||
lineNumber: withOpacity(foreground(layer), 0.35),
|
||||
lineNumberActive: foreground(layer),
|
||||
renameFade: 0.6,
|
||||
unnecessaryCodeFade: 0.5,
|
||||
selection: colorScheme.players[0],
|
||||
whitespace: colorScheme.ramps.neutral(0.5).hex(),
|
||||
guestSelections: [
|
||||
colorScheme.players[1],
|
||||
colorScheme.players[2],
|
||||
colorScheme.players[3],
|
||||
colorScheme.players[4],
|
||||
colorScheme.players[5],
|
||||
colorScheme.players[6],
|
||||
colorScheme.players[7],
|
||||
],
|
||||
autocomplete: {
|
||||
background: background(colorScheme.middle),
|
||||
cornerRadius: 8,
|
||||
padding: 4,
|
||||
margin: {
|
||||
left: -14,
|
||||
},
|
||||
border: border(colorScheme.middle),
|
||||
shadow: colorScheme.popoverShadow,
|
||||
matchHighlight: foreground(colorScheme.middle, "accent"),
|
||||
item: autocompleteItem,
|
||||
hoveredItem: {
|
||||
...autocompleteItem,
|
||||
matchHighlight: foreground(
|
||||
colorScheme.middle,
|
||||
"accent",
|
||||
"hovered"
|
||||
),
|
||||
background: background(colorScheme.middle, "hovered"),
|
||||
},
|
||||
selectedItem: {
|
||||
...autocompleteItem,
|
||||
matchHighlight: foreground(
|
||||
colorScheme.middle,
|
||||
"accent",
|
||||
"active"
|
||||
),
|
||||
background: background(colorScheme.middle, "active"),
|
||||
},
|
||||
},
|
||||
diagnosticHeader: {
|
||||
background: background(colorScheme.middle),
|
||||
iconWidthFactor: 1.5,
|
||||
textScaleFactor: 0.857,
|
||||
border: border(colorScheme.middle, {
|
||||
bottom: true,
|
||||
top: true,
|
||||
}),
|
||||
code: {
|
||||
...text(colorScheme.middle, "mono", { size: "sm" }),
|
||||
margin: {
|
||||
left: 10,
|
||||
},
|
||||
},
|
||||
source: {
|
||||
text: text(colorScheme.middle, "sans", {
|
||||
size: "sm",
|
||||
weight: "bold",
|
||||
}),
|
||||
},
|
||||
message: {
|
||||
highlightText: text(colorScheme.middle, "sans", {
|
||||
size: "sm",
|
||||
weight: "bold",
|
||||
}),
|
||||
text: text(colorScheme.middle, "sans", { size: "sm" }),
|
||||
},
|
||||
},
|
||||
diagnosticPathHeader: {
|
||||
background: background(colorScheme.middle),
|
||||
textScaleFactor: 0.857,
|
||||
filename: text(colorScheme.middle, "mono", { size: "sm" }),
|
||||
path: {
|
||||
...text(colorScheme.middle, "mono", { size: "sm" }),
|
||||
margin: {
|
||||
left: 12,
|
||||
},
|
||||
},
|
||||
},
|
||||
errorDiagnostic: diagnostic(colorScheme.middle, "negative"),
|
||||
warningDiagnostic: diagnostic(colorScheme.middle, "warning"),
|
||||
informationDiagnostic: diagnostic(colorScheme.middle, "accent"),
|
||||
hintDiagnostic: diagnostic(colorScheme.middle, "warning"),
|
||||
invalidErrorDiagnostic: diagnostic(colorScheme.middle, "base"),
|
||||
invalidHintDiagnostic: diagnostic(colorScheme.middle, "base"),
|
||||
invalidInformationDiagnostic: diagnostic(colorScheme.middle, "base"),
|
||||
invalidWarningDiagnostic: diagnostic(colorScheme.middle, "base"),
|
||||
hoverPopover: hoverPopover(colorScheme),
|
||||
linkDefinition: {
|
||||
color: syntax.linkUri.color,
|
||||
underline: syntax.linkUri.underline,
|
||||
},
|
||||
jumpIcon: interactive({
|
||||
base: {
|
||||
color: foreground(layer, "on"),
|
||||
iconWidth: 20,
|
||||
buttonWidth: 20,
|
||||
cornerRadius: 6,
|
||||
padding: {
|
||||
top: 6,
|
||||
bottom: 6,
|
||||
left: 6,
|
||||
right: 6,
|
||||
},
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
background: background(layer, "on", "hovered"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
scrollbar: {
|
||||
width: 12,
|
||||
minHeightFactor: 1.0,
|
||||
track: {
|
||||
border: border(layer, "variant", { left: true }),
|
||||
},
|
||||
thumb: {
|
||||
background: withOpacity(background(layer, "inverted"), 0.3),
|
||||
border: {
|
||||
width: 1,
|
||||
color: borderColor(layer, "variant"),
|
||||
top: false,
|
||||
right: true,
|
||||
left: true,
|
||||
bottom: false,
|
||||
},
|
||||
},
|
||||
git: {
|
||||
deleted: isLight
|
||||
? withOpacity(colorScheme.ramps.red(0.5).hex(), 0.8)
|
||||
: withOpacity(colorScheme.ramps.red(0.4).hex(), 0.8),
|
||||
modified: isLight
|
||||
? withOpacity(colorScheme.ramps.yellow(0.5).hex(), 0.8)
|
||||
: withOpacity(colorScheme.ramps.yellow(0.4).hex(), 0.8),
|
||||
inserted: isLight
|
||||
? withOpacity(colorScheme.ramps.green(0.5).hex(), 0.8)
|
||||
: withOpacity(colorScheme.ramps.green(0.4).hex(), 0.8),
|
||||
},
|
||||
},
|
||||
compositionMark: {
|
||||
underline: {
|
||||
thickness: 1.0,
|
||||
color: borderColor(layer),
|
||||
},
|
||||
},
|
||||
syntax,
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
import { ColorScheme } from "../theme/colorScheme"
|
||||
import { background, border, text } from "./components"
|
||||
import { interactive } from "../element"
|
||||
|
||||
export default function feedback(colorScheme: ColorScheme) {
|
||||
let layer = colorScheme.highest
|
||||
|
||||
return {
|
||||
submit_button: interactive({
|
||||
base: {
|
||||
...text(layer, "mono", "on"),
|
||||
background: background(layer, "on"),
|
||||
cornerRadius: 6,
|
||||
border: border(layer, "on"),
|
||||
margin: {
|
||||
right: 4,
|
||||
},
|
||||
padding: {
|
||||
bottom: 2,
|
||||
left: 10,
|
||||
right: 10,
|
||||
top: 2,
|
||||
},
|
||||
},
|
||||
state: {
|
||||
clicked: {
|
||||
...text(layer, "mono", "on", "pressed"),
|
||||
background: background(layer, "on", "pressed"),
|
||||
border: border(layer, "on", "pressed"),
|
||||
},
|
||||
hovered: {
|
||||
...text(layer, "mono", "on", "hovered"),
|
||||
background: background(layer, "on", "hovered"),
|
||||
border: border(layer, "on", "hovered"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
button_margin: 8,
|
||||
info_text_default: text(layer, "sans", "default", { size: "xs" }),
|
||||
link_text_default: text(layer, "sans", "default", {
|
||||
size: "xs",
|
||||
underline: true,
|
||||
}),
|
||||
link_text_hover: text(layer, "sans", "hovered", {
|
||||
size: "xs",
|
||||
underline: true,
|
||||
}),
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
import { ColorScheme } from "../theme/colorScheme"
|
||||
import { background, border, foreground, text } from "./components"
|
||||
|
||||
export default function HoverPopover(colorScheme: ColorScheme) {
|
||||
let layer = colorScheme.middle
|
||||
let baseContainer = {
|
||||
background: background(layer),
|
||||
cornerRadius: 8,
|
||||
padding: {
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 4,
|
||||
bottom: 4,
|
||||
},
|
||||
shadow: colorScheme.popoverShadow,
|
||||
border: border(layer),
|
||||
margin: {
|
||||
left: -8,
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
container: baseContainer,
|
||||
infoContainer: {
|
||||
...baseContainer,
|
||||
background: background(layer, "accent"),
|
||||
border: border(layer, "accent"),
|
||||
},
|
||||
warningContainer: {
|
||||
...baseContainer,
|
||||
background: background(layer, "warning"),
|
||||
border: border(layer, "warning"),
|
||||
},
|
||||
errorContainer: {
|
||||
...baseContainer,
|
||||
background: background(layer, "negative"),
|
||||
border: border(layer, "negative"),
|
||||
},
|
||||
blockStyle: {
|
||||
padding: { top: 4 },
|
||||
},
|
||||
prose: text(layer, "sans", { size: "sm" }),
|
||||
diagnosticSourceHighlight: { color: foreground(layer, "accent") },
|
||||
highlight: colorScheme.ramps.neutral(0.5).alpha(0.2).hex(), // TODO: blend was used here. Replace with something better
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
import { ColorScheme } from "../theme/colorScheme"
|
||||
import { background, border, text } from "./components"
|
||||
|
||||
export default function incomingCallNotification(
|
||||
colorScheme: ColorScheme
|
||||
): Object {
|
||||
let layer = colorScheme.middle
|
||||
const avatarSize = 48
|
||||
return {
|
||||
windowHeight: 74,
|
||||
windowWidth: 380,
|
||||
background: background(layer),
|
||||
callerContainer: {
|
||||
padding: 12,
|
||||
},
|
||||
callerAvatar: {
|
||||
height: avatarSize,
|
||||
width: avatarSize,
|
||||
cornerRadius: avatarSize / 2,
|
||||
},
|
||||
callerMetadata: {
|
||||
margin: { left: 10 },
|
||||
},
|
||||
callerUsername: {
|
||||
...text(layer, "sans", { size: "sm", weight: "bold" }),
|
||||
margin: { top: -3 },
|
||||
},
|
||||
callerMessage: {
|
||||
...text(layer, "sans", "variant", { size: "xs" }),
|
||||
margin: { top: -3 },
|
||||
},
|
||||
worktreeRoots: {
|
||||
...text(layer, "sans", "variant", { size: "xs", weight: "bold" }),
|
||||
margin: { top: -3 },
|
||||
},
|
||||
buttonWidth: 96,
|
||||
acceptButton: {
|
||||
background: background(layer, "accent"),
|
||||
border: border(layer, { left: true, bottom: true }),
|
||||
...text(layer, "sans", "positive", {
|
||||
size: "xs",
|
||||
weight: "extra_bold",
|
||||
}),
|
||||
},
|
||||
declineButton: {
|
||||
border: border(layer, { left: true }),
|
||||
...text(layer, "sans", "negative", {
|
||||
size: "xs",
|
||||
weight: "extra_bold",
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
import { ColorScheme } from "../theme/colorScheme"
|
||||
import { background, text } from "./components"
|
||||
|
||||
export default function projectDiagnostics(colorScheme: ColorScheme) {
|
||||
let layer = colorScheme.highest
|
||||
return {
|
||||
background: background(layer),
|
||||
tabIconSpacing: 4,
|
||||
tabIconWidth: 13,
|
||||
tabSummarySpacing: 10,
|
||||
emptyMessage: text(layer, "sans", "variant", { size: "md" }),
|
||||
}
|
||||
}
|
@ -1,188 +0,0 @@
|
||||
import { ColorScheme } from "../theme/colorScheme"
|
||||
import { withOpacity } from "../theme/color"
|
||||
import {
|
||||
Border,
|
||||
TextStyle,
|
||||
background,
|
||||
border,
|
||||
foreground,
|
||||
text,
|
||||
} from "./components"
|
||||
import { interactive, toggleable } from "../element"
|
||||
import merge from "ts-deepmerge"
|
||||
export default function projectPanel(colorScheme: ColorScheme) {
|
||||
const { isLight } = colorScheme
|
||||
|
||||
let layer = colorScheme.middle
|
||||
|
||||
type EntryStateProps = {
|
||||
background?: string
|
||||
border?: Border
|
||||
text?: TextStyle
|
||||
iconColor?: string
|
||||
}
|
||||
|
||||
type EntryState = {
|
||||
default: EntryStateProps
|
||||
hovered?: EntryStateProps
|
||||
clicked?: EntryStateProps
|
||||
}
|
||||
|
||||
const entry = (unselected?: EntryState, selected?: EntryState) => {
|
||||
const git_status = {
|
||||
git: {
|
||||
modified: isLight
|
||||
? colorScheme.ramps.yellow(0.6).hex()
|
||||
: colorScheme.ramps.yellow(0.5).hex(),
|
||||
inserted: isLight
|
||||
? colorScheme.ramps.green(0.45).hex()
|
||||
: colorScheme.ramps.green(0.5).hex(),
|
||||
conflict: isLight
|
||||
? colorScheme.ramps.red(0.6).hex()
|
||||
: colorScheme.ramps.red(0.5).hex(),
|
||||
},
|
||||
}
|
||||
|
||||
const base_properties = {
|
||||
height: 22,
|
||||
background: background(layer),
|
||||
iconColor: foreground(layer, "variant"),
|
||||
iconSize: 7,
|
||||
iconSpacing: 5,
|
||||
text: text(layer, "mono", "variant", { size: "sm" }),
|
||||
status: {
|
||||
...git_status,
|
||||
},
|
||||
}
|
||||
|
||||
const selectedStyle: EntryState | undefined = selected
|
||||
? selected
|
||||
: unselected
|
||||
|
||||
const unselected_default_style = merge(
|
||||
base_properties,
|
||||
unselected?.default ?? {},
|
||||
{}
|
||||
)
|
||||
const unselected_hovered_style = merge(
|
||||
base_properties,
|
||||
unselected?.hovered ?? {},
|
||||
{ background: background(layer, "variant", "hovered") }
|
||||
)
|
||||
const unselected_clicked_style = merge(
|
||||
base_properties,
|
||||
unselected?.clicked ?? {},
|
||||
{ background: background(layer, "variant", "pressed") }
|
||||
)
|
||||
const selected_default_style = merge(
|
||||
base_properties,
|
||||
selectedStyle?.default ?? {},
|
||||
{ background: background(layer) }
|
||||
)
|
||||
const selected_hovered_style = merge(
|
||||
base_properties,
|
||||
selectedStyle?.hovered ?? {},
|
||||
{ background: background(layer, "variant", "hovered") }
|
||||
)
|
||||
const selected_clicked_style = merge(
|
||||
base_properties,
|
||||
selectedStyle?.clicked ?? {},
|
||||
{ background: background(layer, "variant", "pressed") }
|
||||
)
|
||||
|
||||
return toggleable({
|
||||
state: {
|
||||
inactive: interactive({
|
||||
state: {
|
||||
default: unselected_default_style,
|
||||
hovered: unselected_hovered_style,
|
||||
clicked: unselected_clicked_style,
|
||||
},
|
||||
}),
|
||||
active: interactive({
|
||||
state: {
|
||||
default: selected_default_style,
|
||||
hovered: selected_hovered_style,
|
||||
clicked: selected_clicked_style,
|
||||
},
|
||||
}),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const defaultEntry = entry()
|
||||
|
||||
return {
|
||||
openProjectButton: interactive({
|
||||
base: {
|
||||
background: background(layer),
|
||||
border: border(layer, "active"),
|
||||
cornerRadius: 4,
|
||||
margin: {
|
||||
top: 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
},
|
||||
padding: {
|
||||
top: 3,
|
||||
bottom: 3,
|
||||
left: 7,
|
||||
right: 7,
|
||||
},
|
||||
...text(layer, "sans", "default", { size: "sm" }),
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
...text(layer, "sans", "default", { size: "sm" }),
|
||||
background: background(layer, "hovered"),
|
||||
border: border(layer, "active"),
|
||||
},
|
||||
clicked: {
|
||||
...text(layer, "sans", "default", { size: "sm" }),
|
||||
background: background(layer, "pressed"),
|
||||
border: border(layer, "active"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
background: background(layer),
|
||||
padding: { left: 6, right: 6, top: 0, bottom: 6 },
|
||||
indentWidth: 12,
|
||||
entry: defaultEntry,
|
||||
draggedEntry: {
|
||||
...defaultEntry.inactive.default,
|
||||
text: text(layer, "mono", "on", { size: "sm" }),
|
||||
background: withOpacity(background(layer, "on"), 0.9),
|
||||
border: border(layer),
|
||||
},
|
||||
ignoredEntry: entry(
|
||||
{
|
||||
default: {
|
||||
text: text(layer, "mono", "disabled"),
|
||||
},
|
||||
},
|
||||
{
|
||||
default: {
|
||||
iconColor: foreground(layer, "variant"),
|
||||
},
|
||||
}
|
||||
),
|
||||
cutEntry: entry(
|
||||
{
|
||||
default: {
|
||||
text: text(layer, "mono", "disabled"),
|
||||
},
|
||||
},
|
||||
{
|
||||
default: {
|
||||
background: background(layer, "active"),
|
||||
text: text(layer, "mono", "disabled", { size: "sm" }),
|
||||
},
|
||||
}
|
||||
),
|
||||
filenameEditor: {
|
||||
background: background(layer, "on"),
|
||||
text: text(layer, "mono", "on", { size: "sm" }),
|
||||
selection: colorScheme.players[0],
|
||||
},
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
import { ColorScheme } from "../theme/colorScheme"
|
||||
import { background, border, text } from "./components"
|
||||
|
||||
export default function projectSharedNotification(
|
||||
colorScheme: ColorScheme
|
||||
): Object {
|
||||
let layer = colorScheme.middle
|
||||
|
||||
const avatarSize = 48
|
||||
return {
|
||||
windowHeight: 74,
|
||||
windowWidth: 380,
|
||||
background: background(layer),
|
||||
ownerContainer: {
|
||||
padding: 12,
|
||||
},
|
||||
ownerAvatar: {
|
||||
height: avatarSize,
|
||||
width: avatarSize,
|
||||
cornerRadius: avatarSize / 2,
|
||||
},
|
||||
ownerMetadata: {
|
||||
margin: { left: 10 },
|
||||
},
|
||||
ownerUsername: {
|
||||
...text(layer, "sans", { size: "sm", weight: "bold" }),
|
||||
margin: { top: -3 },
|
||||
},
|
||||
message: {
|
||||
...text(layer, "sans", "variant", { size: "xs" }),
|
||||
margin: { top: -3 },
|
||||
},
|
||||
worktreeRoots: {
|
||||
...text(layer, "sans", "variant", { size: "xs", weight: "bold" }),
|
||||
margin: { top: -3 },
|
||||
},
|
||||
buttonWidth: 96,
|
||||
openButton: {
|
||||
background: background(layer, "accent"),
|
||||
border: border(layer, { left: true, bottom: true }),
|
||||
...text(layer, "sans", "accent", {
|
||||
size: "xs",
|
||||
weight: "extra_bold",
|
||||
}),
|
||||
},
|
||||
dismissButton: {
|
||||
border: border(layer, { left: true }),
|
||||
...text(layer, "sans", "variant", {
|
||||
size: "xs",
|
||||
weight: "extra_bold",
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
@ -1,135 +0,0 @@
|
||||
import { ColorScheme } from "../theme/colorScheme"
|
||||
import { withOpacity } from "../theme/color"
|
||||
import { background, border, foreground, text } from "./components"
|
||||
import { interactive, toggleable } from "../element"
|
||||
|
||||
export default function search(colorScheme: ColorScheme) {
|
||||
let layer = colorScheme.highest
|
||||
|
||||
// Search input
|
||||
const editor = {
|
||||
background: background(layer),
|
||||
cornerRadius: 8,
|
||||
minWidth: 200,
|
||||
maxWidth: 500,
|
||||
placeholderText: text(layer, "mono", "disabled"),
|
||||
selection: colorScheme.players[0],
|
||||
text: text(layer, "mono", "default"),
|
||||
border: border(layer),
|
||||
margin: {
|
||||
right: 12,
|
||||
},
|
||||
padding: {
|
||||
top: 3,
|
||||
bottom: 3,
|
||||
left: 12,
|
||||
right: 8,
|
||||
},
|
||||
}
|
||||
|
||||
const includeExcludeEditor = {
|
||||
...editor,
|
||||
minWidth: 100,
|
||||
maxWidth: 250,
|
||||
}
|
||||
|
||||
return {
|
||||
// TODO: Add an activeMatchBackground on the rust side to differentiate between active and inactive
|
||||
matchBackground: withOpacity(foreground(layer, "accent"), 0.4),
|
||||
optionButton: toggleable({
|
||||
base: interactive({
|
||||
base: {
|
||||
...text(layer, "mono", "on"),
|
||||
background: background(layer, "on"),
|
||||
cornerRadius: 6,
|
||||
border: border(layer, "on"),
|
||||
margin: {
|
||||
right: 4,
|
||||
},
|
||||
padding: {
|
||||
bottom: 2,
|
||||
left: 10,
|
||||
right: 10,
|
||||
top: 2,
|
||||
},
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
...text(layer, "mono", "on", "hovered"),
|
||||
background: background(layer, "on", "hovered"),
|
||||
border: border(layer, "on", "hovered"),
|
||||
},
|
||||
clicked: {
|
||||
...text(layer, "mono", "on", "pressed"),
|
||||
background: background(layer, "on", "pressed"),
|
||||
border: border(layer, "on", "pressed"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
state: {
|
||||
active: {
|
||||
default: {
|
||||
...text(layer, "mono", "accent"),
|
||||
},
|
||||
hovered: {
|
||||
...text(layer, "mono", "accent", "hovered"),
|
||||
},
|
||||
clicked: {
|
||||
...text(layer, "mono", "accent", "pressed"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
editor,
|
||||
invalidEditor: {
|
||||
...editor,
|
||||
border: border(layer, "negative"),
|
||||
},
|
||||
includeExcludeEditor,
|
||||
invalidIncludeExcludeEditor: {
|
||||
...includeExcludeEditor,
|
||||
border: border(layer, "negative"),
|
||||
},
|
||||
matchIndex: {
|
||||
...text(layer, "mono", "variant"),
|
||||
padding: {
|
||||
left: 6,
|
||||
},
|
||||
},
|
||||
optionButtonGroup: {
|
||||
padding: {
|
||||
left: 12,
|
||||
right: 12,
|
||||
},
|
||||
},
|
||||
includeExcludeInputs: {
|
||||
...text(layer, "mono", "variant"),
|
||||
padding: {
|
||||
right: 6,
|
||||
},
|
||||
},
|
||||
resultsStatus: {
|
||||
...text(layer, "mono", "on"),
|
||||
size: 18,
|
||||
},
|
||||
dismissButton: interactive({
|
||||
base: {
|
||||
color: foreground(layer, "variant"),
|
||||
iconWidth: 12,
|
||||
buttonWidth: 14,
|
||||
padding: {
|
||||
left: 10,
|
||||
right: 10,
|
||||
},
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
color: foreground(layer, "hovered"),
|
||||
},
|
||||
clicked: {
|
||||
color: foreground(layer, "pressed"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
import { ColorScheme } from "../theme/colorScheme"
|
||||
import { background } from "./components"
|
||||
|
||||
export default function sharedScreen(colorScheme: ColorScheme) {
|
||||
let layer = colorScheme.highest
|
||||
return {
|
||||
background: background(layer),
|
||||
}
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
import { ColorScheme } from "../theme/colorScheme"
|
||||
import { background, border, foreground, text } from "./components"
|
||||
import { interactive } from "../element"
|
||||
|
||||
const headerPadding = 8
|
||||
|
||||
export default function simpleMessageNotification(
|
||||
colorScheme: ColorScheme
|
||||
): Object {
|
||||
let layer = colorScheme.middle
|
||||
return {
|
||||
message: {
|
||||
...text(layer, "sans", { size: "xs" }),
|
||||
margin: { left: headerPadding, right: headerPadding },
|
||||
},
|
||||
actionMessage: interactive({
|
||||
base: {
|
||||
...text(layer, "sans", { size: "xs" }),
|
||||
border: border(layer, "active"),
|
||||
cornerRadius: 4,
|
||||
padding: {
|
||||
top: 3,
|
||||
bottom: 3,
|
||||
left: 7,
|
||||
right: 7,
|
||||
},
|
||||
|
||||
margin: { left: headerPadding, top: 6, bottom: 6 },
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
...text(layer, "sans", "default", { size: "xs" }),
|
||||
background: background(layer, "hovered"),
|
||||
border: border(layer, "active"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
dismissButton: interactive({
|
||||
base: {
|
||||
color: foreground(layer),
|
||||
iconWidth: 8,
|
||||
iconHeight: 8,
|
||||
buttonWidth: 8,
|
||||
buttonHeight: 8,
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
color: foreground(layer, "hovered"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
import { ColorScheme } from "../theme/colorScheme"
|
||||
|
||||
export default function terminal(colorScheme: ColorScheme) {
|
||||
/**
|
||||
* Colors are controlled per-cell in the terminal grid.
|
||||
* Cells can be set to any of these more 'theme-capable' colors
|
||||
* or can be set directly with RGB values.
|
||||
* Here are the common interpretations of these names:
|
||||
* https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
|
||||
*/
|
||||
return {
|
||||
black: colorScheme.ramps.neutral(0).hex(),
|
||||
red: colorScheme.ramps.red(0.5).hex(),
|
||||
green: colorScheme.ramps.green(0.5).hex(),
|
||||
yellow: colorScheme.ramps.yellow(0.5).hex(),
|
||||
blue: colorScheme.ramps.blue(0.5).hex(),
|
||||
magenta: colorScheme.ramps.magenta(0.5).hex(),
|
||||
cyan: colorScheme.ramps.cyan(0.5).hex(),
|
||||
white: colorScheme.ramps.neutral(1).hex(),
|
||||
brightBlack: colorScheme.ramps.neutral(0.4).hex(),
|
||||
brightRed: colorScheme.ramps.red(0.25).hex(),
|
||||
brightGreen: colorScheme.ramps.green(0.25).hex(),
|
||||
brightYellow: colorScheme.ramps.yellow(0.25).hex(),
|
||||
brightBlue: colorScheme.ramps.blue(0.25).hex(),
|
||||
brightMagenta: colorScheme.ramps.magenta(0.25).hex(),
|
||||
brightCyan: colorScheme.ramps.cyan(0.25).hex(),
|
||||
brightWhite: colorScheme.ramps.neutral(1).hex(),
|
||||
/**
|
||||
* Default color for characters
|
||||
*/
|
||||
foreground: colorScheme.ramps.neutral(1).hex(),
|
||||
/**
|
||||
* Default color for the rectangle background of a cell
|
||||
*/
|
||||
background: colorScheme.ramps.neutral(0).hex(),
|
||||
modalBackground: colorScheme.ramps.neutral(0.1).hex(),
|
||||
/**
|
||||
* Default color for the cursor
|
||||
*/
|
||||
cursor: colorScheme.players[0].cursor,
|
||||
dimBlack: colorScheme.ramps.neutral(1).hex(),
|
||||
dimRed: colorScheme.ramps.red(0.75).hex(),
|
||||
dimGreen: colorScheme.ramps.green(0.75).hex(),
|
||||
dimYellow: colorScheme.ramps.yellow(0.75).hex(),
|
||||
dimBlue: colorScheme.ramps.blue(0.75).hex(),
|
||||
dimMagenta: colorScheme.ramps.magenta(0.75).hex(),
|
||||
dimCyan: colorScheme.ramps.cyan(0.75).hex(),
|
||||
dimWhite: colorScheme.ramps.neutral(0.6).hex(),
|
||||
brightForeground: colorScheme.ramps.neutral(1).hex(),
|
||||
dimForeground: colorScheme.ramps.neutral(0).hex(),
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
import merge from "ts-deepmerge"
|
||||
|
||||
type ToggleState = "inactive" | "active"
|
||||
|
||||
type Toggleable<T> = Record<ToggleState, T>
|
||||
|
||||
const NO_INACTIVE_OR_BASE_ERROR =
|
||||
"A toggleable object must have an inactive state, or a base property."
|
||||
const NO_ACTIVE_ERROR = "A toggleable object must have an active state."
|
||||
|
||||
interface ToggleableProps<T> {
|
||||
base?: T
|
||||
state: Partial<Record<ToggleState, T>>
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for creating Toggleable objects.
|
||||
* @template T The type of the object being toggled.
|
||||
* @param props Object containing the base (inactive) state and state modifications to create the active state.
|
||||
* @returns A Toggleable object containing both the inactive and active states.
|
||||
* @example
|
||||
* ```
|
||||
* toggleable({
|
||||
* base: { background: "#000000", text: "#CCCCCC" },
|
||||
* state: { active: { text: "#CCCCCC" } },
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function toggleable<T extends object>(
|
||||
props: ToggleableProps<T>
|
||||
): Toggleable<T> {
|
||||
const { base, state } = props
|
||||
|
||||
if (!base && !state.inactive) throw new Error(NO_INACTIVE_OR_BASE_ERROR)
|
||||
if (!state.active) throw new Error(NO_ACTIVE_ERROR)
|
||||
|
||||
const inactiveState = base
|
||||
? ((state.inactive ? merge(base, state.inactive) : base) as T)
|
||||
: (state.inactive as T)
|
||||
|
||||
const toggleObj: Toggleable<T> = {
|
||||
inactive: inactiveState,
|
||||
active: merge(base ?? {}, state.active) as T,
|
||||
}
|
||||
|
||||
return toggleObj
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
import { ColorScheme } from "../theme/colorScheme"
|
||||
import { background, border, text } from "./components"
|
||||
import { interactive, toggleable } from "../element"
|
||||
export default function dropdownMenu(colorScheme: ColorScheme) {
|
||||
let layer = colorScheme.middle
|
||||
|
||||
return {
|
||||
rowHeight: 30,
|
||||
background: background(layer),
|
||||
border: border(layer),
|
||||
shadow: colorScheme.popoverShadow,
|
||||
header: interactive({
|
||||
base: {
|
||||
...text(layer, "sans", { size: "sm" }),
|
||||
secondaryText: text(layer, "sans", {
|
||||
size: "sm",
|
||||
color: "#aaaaaa",
|
||||
}),
|
||||
secondaryTextSpacing: 10,
|
||||
padding: { left: 8, right: 8, top: 2, bottom: 2 },
|
||||
cornerRadius: 6,
|
||||
background: background(layer, "on"),
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
background: background(layer, "hovered"),
|
||||
},
|
||||
clicked: {
|
||||
background: background(layer, "pressed"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
sectionHeader: {
|
||||
...text(layer, "sans", { size: "sm" }),
|
||||
padding: { left: 8, right: 8, top: 8, bottom: 8 },
|
||||
},
|
||||
item: toggleable({
|
||||
base: interactive({
|
||||
base: {
|
||||
...text(layer, "sans", { size: "sm" }),
|
||||
secondaryTextSpacing: 10,
|
||||
secondaryText: text(layer, "sans", { size: "sm" }),
|
||||
padding: { left: 18, right: 18, top: 2, bottom: 2 },
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
background: background(layer, "hovered"),
|
||||
...text(layer, "sans", "hovered", { size: "sm" }),
|
||||
},
|
||||
},
|
||||
}),
|
||||
state: {
|
||||
active: {
|
||||
default: {
|
||||
background: background(layer, "active"),
|
||||
},
|
||||
hovered: {
|
||||
background: background(layer, "hovered"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
import { ColorScheme } from "../theme/colorScheme"
|
||||
import { background, border, text } from "./components"
|
||||
|
||||
export default function tooltip(colorScheme: ColorScheme) {
|
||||
let layer = colorScheme.middle
|
||||
return {
|
||||
background: background(layer),
|
||||
border: border(layer),
|
||||
padding: { top: 4, bottom: 4, left: 8, right: 8 },
|
||||
margin: { top: 6, left: 6 },
|
||||
shadow: colorScheme.popoverShadow,
|
||||
cornerRadius: 6,
|
||||
text: text(layer, "sans", { size: "xs" }),
|
||||
keystroke: {
|
||||
background: background(layer, "on"),
|
||||
cornerRadius: 4,
|
||||
margin: { left: 6 },
|
||||
padding: { left: 4, right: 4 },
|
||||
...text(layer, "mono", "on", { size: "xs", weight: "bold" }),
|
||||
},
|
||||
maxTextWidth: 200,
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
import { ColorScheme } from "../theme/colorScheme"
|
||||
import { foreground, text } from "./components"
|
||||
import { interactive } from "../element"
|
||||
|
||||
const headerPadding = 8
|
||||
|
||||
export default function updateNotification(colorScheme: ColorScheme): Object {
|
||||
let layer = colorScheme.middle
|
||||
return {
|
||||
message: {
|
||||
...text(layer, "sans", { size: "xs" }),
|
||||
margin: { left: headerPadding, right: headerPadding },
|
||||
},
|
||||
actionMessage: interactive({
|
||||
base: {
|
||||
...text(layer, "sans", { size: "xs" }),
|
||||
margin: { left: headerPadding, top: 6, bottom: 6 },
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
color: foreground(layer, "hovered"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
dismissButton: interactive({
|
||||
base: {
|
||||
color: foreground(layer),
|
||||
iconWidth: 8,
|
||||
iconHeight: 8,
|
||||
buttonWidth: 8,
|
||||
buttonHeight: 8,
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
color: foreground(layer, "hovered"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
@ -1,134 +0,0 @@
|
||||
import { ColorScheme } from "../theme/colorScheme"
|
||||
import { withOpacity } from "../theme/color"
|
||||
import {
|
||||
border,
|
||||
background,
|
||||
foreground,
|
||||
text,
|
||||
TextProperties,
|
||||
svg,
|
||||
} from "./components"
|
||||
import { interactive } from "../element"
|
||||
|
||||
export default function welcome(colorScheme: ColorScheme) {
|
||||
let layer = colorScheme.highest
|
||||
|
||||
let checkboxBase = {
|
||||
cornerRadius: 4,
|
||||
padding: {
|
||||
left: 3,
|
||||
right: 3,
|
||||
top: 3,
|
||||
bottom: 3,
|
||||
},
|
||||
// shadow: colorScheme.popoverShadow,
|
||||
border: border(layer),
|
||||
margin: {
|
||||
right: 8,
|
||||
top: 5,
|
||||
bottom: 5,
|
||||
},
|
||||
}
|
||||
|
||||
let interactive_text_size: TextProperties = { size: "sm" }
|
||||
|
||||
return {
|
||||
pageWidth: 320,
|
||||
logo: svg(foreground(layer, "default"), "icons/logo_96.svg", 64, 64),
|
||||
logoSubheading: {
|
||||
...text(layer, "sans", "variant", { size: "md" }),
|
||||
margin: {
|
||||
top: 10,
|
||||
bottom: 7,
|
||||
},
|
||||
},
|
||||
buttonGroup: {
|
||||
margin: {
|
||||
top: 8,
|
||||
bottom: 16,
|
||||
},
|
||||
},
|
||||
headingGroup: {
|
||||
margin: {
|
||||
top: 8,
|
||||
bottom: 12,
|
||||
},
|
||||
},
|
||||
checkboxGroup: {
|
||||
border: border(layer, "variant"),
|
||||
background: withOpacity(background(layer, "hovered"), 0.25),
|
||||
cornerRadius: 4,
|
||||
padding: {
|
||||
left: 12,
|
||||
top: 2,
|
||||
bottom: 2,
|
||||
},
|
||||
},
|
||||
button: interactive({
|
||||
base: {
|
||||
background: background(layer),
|
||||
border: border(layer, "active"),
|
||||
cornerRadius: 4,
|
||||
margin: {
|
||||
top: 4,
|
||||
bottom: 4,
|
||||
},
|
||||
padding: {
|
||||
top: 3,
|
||||
bottom: 3,
|
||||
left: 7,
|
||||
right: 7,
|
||||
},
|
||||
...text(layer, "sans", "default", interactive_text_size),
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
...text(layer, "sans", "default", interactive_text_size),
|
||||
background: background(layer, "hovered"),
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
usageNote: {
|
||||
...text(layer, "sans", "variant", { size: "2xs" }),
|
||||
padding: {
|
||||
top: -4,
|
||||
},
|
||||
},
|
||||
checkboxContainer: {
|
||||
margin: {
|
||||
top: 4,
|
||||
},
|
||||
padding: {
|
||||
bottom: 8,
|
||||
},
|
||||
},
|
||||
checkbox: {
|
||||
label: {
|
||||
...text(layer, "sans", interactive_text_size),
|
||||
// Also supports margin, container, border, etc.
|
||||
},
|
||||
icon: svg(foreground(layer, "on"), "icons/check_12.svg", 12, 12),
|
||||
default: {
|
||||
...checkboxBase,
|
||||
background: background(layer, "default"),
|
||||
border: border(layer, "active"),
|
||||
},
|
||||
checked: {
|
||||
...checkboxBase,
|
||||
background: background(layer, "hovered"),
|
||||
border: border(layer, "active"),
|
||||
},
|
||||
hovered: {
|
||||
...checkboxBase,
|
||||
background: background(layer, "hovered"),
|
||||
border: border(layer, "active"),
|
||||
},
|
||||
hoveredAndChecked: {
|
||||
...checkboxBase,
|
||||
background: background(layer, "hovered"),
|
||||
border: border(layer, "active"),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
@ -1,200 +0,0 @@
|
||||
import { ColorScheme } from "../theme/colorScheme"
|
||||
import { withOpacity } from "../theme/color"
|
||||
import {
|
||||
background,
|
||||
border,
|
||||
borderColor,
|
||||
foreground,
|
||||
svg,
|
||||
text,
|
||||
} from "./components"
|
||||
import statusBar from "./statusBar"
|
||||
import tabBar from "./tabBar"
|
||||
import { interactive } from "../element"
|
||||
|
||||
import { titlebar } from "./titlebar"
|
||||
export default function workspace(colorScheme: ColorScheme) {
|
||||
const layer = colorScheme.lowest
|
||||
const isLight = colorScheme.isLight
|
||||
|
||||
return {
|
||||
background: background(colorScheme.lowest),
|
||||
blankPane: {
|
||||
logoContainer: {
|
||||
width: 256,
|
||||
height: 256,
|
||||
},
|
||||
logo: svg(
|
||||
withOpacity("#000000", colorScheme.isLight ? 0.6 : 0.8),
|
||||
"icons/logo_96.svg",
|
||||
256,
|
||||
256
|
||||
),
|
||||
|
||||
logoShadow: svg(
|
||||
withOpacity(
|
||||
colorScheme.isLight
|
||||
? "#FFFFFF"
|
||||
: colorScheme.lowest.base.default.background,
|
||||
colorScheme.isLight ? 1 : 0.6
|
||||
),
|
||||
"icons/logo_96.svg",
|
||||
256,
|
||||
256
|
||||
),
|
||||
keyboardHints: {
|
||||
margin: {
|
||||
top: 96,
|
||||
},
|
||||
cornerRadius: 4,
|
||||
},
|
||||
keyboardHint: interactive({
|
||||
base: {
|
||||
...text(layer, "sans", "variant", { size: "sm" }),
|
||||
padding: {
|
||||
top: 3,
|
||||
left: 8,
|
||||
right: 8,
|
||||
bottom: 3,
|
||||
},
|
||||
cornerRadius: 8,
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
...text(layer, "sans", "active", { size: "sm" }),
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
keyboardHintWidth: 320,
|
||||
},
|
||||
joiningProjectAvatar: {
|
||||
cornerRadius: 40,
|
||||
width: 80,
|
||||
},
|
||||
joiningProjectMessage: {
|
||||
padding: 12,
|
||||
...text(layer, "sans", { size: "lg" }),
|
||||
},
|
||||
externalLocationMessage: {
|
||||
background: background(colorScheme.middle, "accent"),
|
||||
border: border(colorScheme.middle, "accent"),
|
||||
cornerRadius: 6,
|
||||
padding: 12,
|
||||
margin: { bottom: 8, right: 8 },
|
||||
...text(colorScheme.middle, "sans", "accent", { size: "xs" }),
|
||||
},
|
||||
leaderBorderOpacity: 0.7,
|
||||
leaderBorderWidth: 2.0,
|
||||
tabBar: tabBar(colorScheme),
|
||||
modal: {
|
||||
margin: {
|
||||
bottom: 52,
|
||||
top: 52,
|
||||
},
|
||||
cursor: "Arrow",
|
||||
},
|
||||
zoomedBackground: {
|
||||
cursor: "Arrow",
|
||||
background: isLight
|
||||
? withOpacity(background(colorScheme.lowest), 0.8)
|
||||
: withOpacity(background(colorScheme.highest), 0.6),
|
||||
},
|
||||
zoomedPaneForeground: {
|
||||
margin: 16,
|
||||
shadow: colorScheme.modalShadow,
|
||||
border: border(colorScheme.lowest, { overlay: true }),
|
||||
},
|
||||
zoomedPanelForeground: {
|
||||
margin: 16,
|
||||
border: border(colorScheme.lowest, { overlay: true }),
|
||||
},
|
||||
dock: {
|
||||
left: {
|
||||
border: border(layer, { right: true }),
|
||||
},
|
||||
bottom: {
|
||||
border: border(layer, { top: true }),
|
||||
},
|
||||
right: {
|
||||
border: border(layer, { left: true }),
|
||||
},
|
||||
},
|
||||
paneDivider: {
|
||||
color: borderColor(layer),
|
||||
width: 1,
|
||||
},
|
||||
statusBar: statusBar(colorScheme),
|
||||
titlebar: titlebar(colorScheme),
|
||||
toolbar: {
|
||||
height: 34,
|
||||
background: background(colorScheme.highest),
|
||||
border: border(colorScheme.highest, { bottom: true }),
|
||||
itemSpacing: 8,
|
||||
navButton: interactive({
|
||||
base: {
|
||||
color: foreground(colorScheme.highest, "on"),
|
||||
iconWidth: 12,
|
||||
buttonWidth: 24,
|
||||
cornerRadius: 6,
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
color: foreground(colorScheme.highest, "on", "hovered"),
|
||||
background: background(
|
||||
colorScheme.highest,
|
||||
"on",
|
||||
"hovered"
|
||||
),
|
||||
},
|
||||
disabled: {
|
||||
color: foreground(
|
||||
colorScheme.highest,
|
||||
"on",
|
||||
"disabled"
|
||||
),
|
||||
},
|
||||
},
|
||||
}),
|
||||
padding: { left: 8, right: 8, top: 4, bottom: 4 },
|
||||
},
|
||||
breadcrumbHeight: 24,
|
||||
breadcrumbs: interactive({
|
||||
base: {
|
||||
...text(colorScheme.highest, "sans", "variant"),
|
||||
cornerRadius: 6,
|
||||
padding: {
|
||||
left: 6,
|
||||
right: 6,
|
||||
},
|
||||
},
|
||||
state: {
|
||||
hovered: {
|
||||
color: foreground(colorScheme.highest, "on", "hovered"),
|
||||
background: background(
|
||||
colorScheme.highest,
|
||||
"on",
|
||||
"hovered"
|
||||
),
|
||||
},
|
||||
},
|
||||
}),
|
||||
disconnectedOverlay: {
|
||||
...text(layer, "sans"),
|
||||
background: withOpacity(background(layer), 0.8),
|
||||
},
|
||||
notification: {
|
||||
margin: { top: 10 },
|
||||
background: background(colorScheme.middle),
|
||||
cornerRadius: 6,
|
||||
padding: 12,
|
||||
border: border(colorScheme.middle),
|
||||
shadow: colorScheme.popoverShadow,
|
||||
},
|
||||
notifications: {
|
||||
width: 400,
|
||||
margin: { right: 10, bottom: 10 },
|
||||
},
|
||||
dropTargetOverlayColor: withOpacity(foreground(layer, "variant"), 0.5),
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user