Compare commits
37 Commits
vim-mrnugg
...
x11-rework
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
feb7efff4e | ||
|
|
3956717065 | ||
|
|
d3e2327099 | ||
|
|
fc945cc351 | ||
|
|
da22e0dd0b | ||
|
|
fb3ef0d140 | ||
|
|
e71b642f44 | ||
|
|
6cedfa0ce7 | ||
|
|
209b1d1931 | ||
|
|
6986ac4c27 | ||
|
|
d50d1611b9 | ||
|
|
1260c616ba | ||
|
|
89951f7e66 | ||
|
|
cd81dad2fa | ||
|
|
3a08d7ab43 | ||
|
|
49dc63812a | ||
|
|
c0a3642f77 | ||
|
|
4d5441c09d | ||
|
|
2dc840132b | ||
|
|
5d766f61fa | ||
|
|
18b4573064 | ||
|
|
d044dc8485 | ||
|
|
f00bea5d0f | ||
|
|
b43df6048b | ||
|
|
eb914682b3 | ||
|
|
5b7e31c075 | ||
|
|
922fcaf5a6 | ||
|
|
9f88460870 | ||
|
|
e5d1cf84cf | ||
|
|
41d2c52638 | ||
|
|
d1a55d64a8 | ||
|
|
db06244972 | ||
|
|
597469bbbd | ||
|
|
e0c192d831 | ||
|
|
b2a0a7fa3c | ||
|
|
0b1a589183 | ||
|
|
7e694d1bcf |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -254,7 +254,7 @@ jobs:
|
||||
target/aarch64-apple-darwin/release/Zed-aarch64.dmg
|
||||
target/x86_64-apple-darwin/release/Zed-x86_64.dmg
|
||||
target/release/Zed.dmg
|
||||
body_file: target/release-notes.md
|
||||
body_path: target/release-notes.md
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
|
||||
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -4919,6 +4919,7 @@ dependencies = [
|
||||
"taffy",
|
||||
"thiserror",
|
||||
"time",
|
||||
"unicode-segmentation",
|
||||
"usvg",
|
||||
"util",
|
||||
"uuid",
|
||||
@@ -13549,7 +13550,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.142.0"
|
||||
version = "0.143.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
|
||||
1
assets/icons/chevron_up_down.svg
Normal file
1
assets/icons/chevron_up_down.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevrons-up-down"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg>
|
||||
|
After Width: | Height: | Size: 276 B |
1
assets/icons/font.svg
Normal file
1
assets/icons/font.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-type"><polyline points="4 7 4 4 20 4 20 7"/><line x1="9" x2="15" y1="20" y2="20"/><line x1="12" x2="12" y1="4" y2="20"/></svg>
|
||||
|
After Width: | Height: | Size: 329 B |
1
assets/icons/font_size.svg
Normal file
1
assets/icons/font_size.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-a-large-small"><path d="M21 14h-5"/><path d="M16 16v-3.5a2.5 2.5 0 0 1 5 0V16"/><path d="M4.5 13h6"/><path d="m3 16 4.5-9 4.5 9"/></svg>
|
||||
|
After Width: | Height: | Size: 339 B |
1
assets/icons/font_weight.svg
Normal file
1
assets/icons/font_weight.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bold"><path d="M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8"/></svg>
|
||||
|
After Width: | Height: | Size: 296 B |
6
assets/icons/line_height.svg
Normal file
6
assets/icons/line_height.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 13.6667H12" stroke="#B3B3B3" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4 2.33333H12" stroke="#B3B3B3" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M5 11L8 5L11 11" stroke="#B3B3B3" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6 9H10" stroke="#B3B3B3" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 539 B |
1
assets/icons/visible.svg
Normal file
1
assets/icons/visible.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-eye"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
|
After Width: | Height: | Size: 301 B |
@@ -70,6 +70,14 @@
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
"bindings": {
|
||||
"a": "project_panel::NewFile",
|
||||
"shift-a": "project_panel::NewDirectory",
|
||||
"f2": "project_panel::Rename",
|
||||
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"shift-d": "project_panel::Duplicate",
|
||||
"cmd-x": "project_panel::Cut",
|
||||
"cmd-c": "project_panel::Copy",
|
||||
"cmd-v": "project_panel::Paste",
|
||||
"ctrl-[": "project_panel::CollapseSelectedEntry",
|
||||
"ctrl-b": "project_panel::CollapseSelectedEntry",
|
||||
"alt-b": "project_panel::CollapseSelectedEntry",
|
||||
|
||||
@@ -587,8 +587,9 @@
|
||||
"alt-ctrl-shift-c": "project_panel::CopyRelativePath",
|
||||
"f2": "project_panel::Rename",
|
||||
"enter": "project_panel::Rename",
|
||||
"backspace": "project_panel::Trash",
|
||||
"delete": "project_panel::Trash",
|
||||
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"shift-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"delete": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"alt-ctrl-r": "project_panel::RevealInFinder",
|
||||
|
||||
@@ -605,6 +605,7 @@
|
||||
"left": "project_panel::CollapseSelectedEntry",
|
||||
"right": "project_panel::ExpandSelectedEntry",
|
||||
"cmd-n": "project_panel::NewFile",
|
||||
"cmd-d": "project_panel::Duplicate",
|
||||
"alt-cmd-n": "project_panel::NewDirectory",
|
||||
"cmd-x": "project_panel::Cut",
|
||||
"cmd-c": "project_panel::Copy",
|
||||
@@ -614,8 +615,9 @@
|
||||
"enter": "project_panel::Rename",
|
||||
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"delete": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"cmd-backspace": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }],
|
||||
"cmd-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"alt-cmd-r": "project_panel::RevealInFinder",
|
||||
"alt-shift-f": "project_panel::NewSearchInDirectory",
|
||||
"shift-down": "menu::SelectNext",
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
"bindings": {
|
||||
"cmd-shift-o": "file_finder::Toggle",
|
||||
"cmd-shift-a": "command_palette::Toggle",
|
||||
"shift shift": "command_palette::Toggle",
|
||||
"cmd-alt-o": "project_symbols::Toggle",
|
||||
"cmd-1": "workspace::ToggleLeftDock",
|
||||
"cmd-6": "diagnostics::Deploy"
|
||||
@@ -94,6 +95,10 @@
|
||||
"context": "ProjectPanel",
|
||||
"bindings": {
|
||||
"enter": "project_panel::Open",
|
||||
"cmd-backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"backspace": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"delete": ["project_panel::Trash", { "skip_prompt": false }],
|
||||
"shift-delete": ["project_panel::Delete", { "skip_prompt": false }],
|
||||
"shift-f6": "project_panel::Rename"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,15 @@
|
||||
},
|
||||
{
|
||||
"context": "ProjectPanel",
|
||||
"bindings": {}
|
||||
"bindings": {
|
||||
"cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }],
|
||||
"cmd-d": "project_panel::Duplicate",
|
||||
"cmd-n": "project_panel::NewFolder",
|
||||
"return": "project_panel::Rename",
|
||||
"cmd-c": "project_panel::Copy",
|
||||
"cmd-v": "project_panel::Paste",
|
||||
"cmd-alt-c": "project_panel::CopyPath"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Dock",
|
||||
|
||||
@@ -688,7 +688,9 @@
|
||||
// "TOML": ["Embargo.lock"]
|
||||
// }
|
||||
//
|
||||
"file_types": {},
|
||||
"file_types": {
|
||||
"JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json"]
|
||||
},
|
||||
// The extensions that Zed should automatically install on startup.
|
||||
//
|
||||
// If you don't want any of these extensions, add this field to your settings
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
@@ -169,6 +169,7 @@ pub enum AssistantProvider {
|
||||
model: OpenAiModel,
|
||||
api_url: String,
|
||||
low_speed_timeout_in_seconds: Option<u64>,
|
||||
available_models: Vec<OpenAiModel>,
|
||||
},
|
||||
Anthropic {
|
||||
model: AnthropicModel,
|
||||
@@ -188,6 +189,7 @@ impl Default for AssistantProvider {
|
||||
model: OpenAiModel::default(),
|
||||
api_url: open_ai::OPEN_AI_API_URL.into(),
|
||||
low_speed_timeout_in_seconds: None,
|
||||
available_models: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -202,6 +204,7 @@ pub enum AssistantProviderContent {
|
||||
default_model: Option<OpenAiModel>,
|
||||
api_url: Option<String>,
|
||||
low_speed_timeout_in_seconds: Option<u64>,
|
||||
available_models: Option<Vec<OpenAiModel>>,
|
||||
},
|
||||
#[serde(rename = "anthropic")]
|
||||
Anthropic {
|
||||
@@ -272,6 +275,7 @@ impl AssistantSettingsContent {
|
||||
default_model: settings.default_open_ai_model.clone(),
|
||||
api_url: Some(open_ai_api_url.clone()),
|
||||
low_speed_timeout_in_seconds: None,
|
||||
available_models: Some(Default::default()),
|
||||
})
|
||||
} else {
|
||||
settings.default_open_ai_model.clone().map(|open_ai_model| {
|
||||
@@ -279,6 +283,7 @@ impl AssistantSettingsContent {
|
||||
default_model: Some(open_ai_model),
|
||||
api_url: None,
|
||||
low_speed_timeout_in_seconds: None,
|
||||
available_models: Some(Default::default()),
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -326,6 +331,14 @@ impl AssistantSettingsContent {
|
||||
*model = Some(new_model);
|
||||
}
|
||||
}
|
||||
Some(AssistantProviderContent::Ollama {
|
||||
default_model: model,
|
||||
..
|
||||
}) => {
|
||||
if let LanguageModel::Ollama(new_model) = new_model {
|
||||
*model = Some(new_model);
|
||||
}
|
||||
}
|
||||
provider => match new_model {
|
||||
LanguageModel::Cloud(model) => {
|
||||
*provider = Some(AssistantProviderContent::ZedDotDev {
|
||||
@@ -337,6 +350,7 @@ impl AssistantSettingsContent {
|
||||
default_model: Some(model),
|
||||
api_url: None,
|
||||
low_speed_timeout_in_seconds: None,
|
||||
available_models: Some(Default::default()),
|
||||
})
|
||||
}
|
||||
LanguageModel::Anthropic(model) => {
|
||||
@@ -481,15 +495,18 @@ impl Settings for AssistantSettings {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models,
|
||||
},
|
||||
AssistantProviderContent::OpenAi {
|
||||
default_model: model_override,
|
||||
api_url: api_url_override,
|
||||
low_speed_timeout_in_seconds: low_speed_timeout_in_seconds_override,
|
||||
available_models: available_models_override,
|
||||
},
|
||||
) => {
|
||||
merge(model, model_override);
|
||||
merge(api_url, api_url_override);
|
||||
merge(available_models, available_models_override);
|
||||
if let Some(low_speed_timeout_in_seconds_override) =
|
||||
low_speed_timeout_in_seconds_override
|
||||
{
|
||||
@@ -550,10 +567,12 @@ impl Settings for AssistantSettings {
|
||||
default_model: model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models,
|
||||
} => AssistantProvider::OpenAi {
|
||||
model: model.unwrap_or_default(),
|
||||
api_url: api_url.unwrap_or_else(|| open_ai::OPEN_AI_API_URL.into()),
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models: available_models.unwrap_or_default(),
|
||||
},
|
||||
AssistantProviderContent::Anthropic {
|
||||
default_model: model,
|
||||
@@ -610,6 +629,7 @@ mod tests {
|
||||
model: OpenAiModel::FourOmni,
|
||||
api_url: open_ai::OPEN_AI_API_URL.into(),
|
||||
low_speed_timeout_in_seconds: None,
|
||||
available_models: Default::default(),
|
||||
}
|
||||
);
|
||||
|
||||
@@ -632,6 +652,7 @@ mod tests {
|
||||
model: OpenAiModel::FourOmni,
|
||||
api_url: "test-url".into(),
|
||||
low_speed_timeout_in_seconds: None,
|
||||
available_models: Default::default(),
|
||||
}
|
||||
);
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
@@ -652,6 +673,7 @@ mod tests {
|
||||
model: OpenAiModel::Four,
|
||||
api_url: open_ai::OPEN_AI_API_URL.into(),
|
||||
low_speed_timeout_in_seconds: None,
|
||||
available_models: Default::default(),
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -24,6 +24,20 @@ use settings::{Settings, SettingsStore};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Choose which model to use for openai provider.
|
||||
/// If the model is not available, try to use the first available model, or fallback to the original model.
|
||||
fn choose_openai_model(
|
||||
model: &::open_ai::Model,
|
||||
available_models: &[::open_ai::Model],
|
||||
) -> ::open_ai::Model {
|
||||
available_models
|
||||
.iter()
|
||||
.find(|&m| m == model)
|
||||
.or_else(|| available_models.first())
|
||||
.unwrap_or_else(|| model)
|
||||
.clone()
|
||||
}
|
||||
|
||||
pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
let mut settings_version = 0;
|
||||
let provider = match &AssistantSettings::get_global(cx).provider {
|
||||
@@ -34,8 +48,9 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models,
|
||||
} => CompletionProvider::OpenAi(OpenAiCompletionProvider::new(
|
||||
model.clone(),
|
||||
choose_openai_model(model, available_models),
|
||||
api_url.clone(),
|
||||
client.http_client(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
@@ -77,10 +92,11 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models,
|
||||
},
|
||||
) => {
|
||||
provider.update(
|
||||
model.clone(),
|
||||
choose_openai_model(model, available_models),
|
||||
api_url.clone(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
settings_version,
|
||||
@@ -136,10 +152,11 @@ pub fn init(client: Arc<Client>, cx: &mut AppContext) {
|
||||
model,
|
||||
api_url,
|
||||
low_speed_timeout_in_seconds,
|
||||
available_models,
|
||||
},
|
||||
) => {
|
||||
*provider = CompletionProvider::OpenAi(OpenAiCompletionProvider::new(
|
||||
model.clone(),
|
||||
choose_openai_model(model, available_models),
|
||||
api_url.clone(),
|
||||
client.http_client(),
|
||||
low_speed_timeout_in_seconds.map(Duration::from_secs),
|
||||
@@ -201,10 +218,10 @@ impl CompletionProvider {
|
||||
cx.global::<Self>()
|
||||
}
|
||||
|
||||
pub fn available_models(&self) -> Vec<LanguageModel> {
|
||||
pub fn available_models(&self, cx: &AppContext) -> Vec<LanguageModel> {
|
||||
match self {
|
||||
CompletionProvider::OpenAi(provider) => provider
|
||||
.available_models()
|
||||
.available_models(cx)
|
||||
.map(LanguageModel::OpenAi)
|
||||
.collect(),
|
||||
CompletionProvider::Anthropic(provider) => provider
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::assistant_settings::CloudModel;
|
||||
use crate::assistant_settings::{AssistantProvider, AssistantSettings};
|
||||
use crate::{
|
||||
assistant_settings::OpenAiModel, CompletionProvider, LanguageModel, LanguageModelRequest, Role,
|
||||
};
|
||||
@@ -56,8 +57,26 @@ impl OpenAiCompletionProvider {
|
||||
self.settings_version = settings_version;
|
||||
}
|
||||
|
||||
pub fn available_models(&self) -> impl Iterator<Item = OpenAiModel> {
|
||||
OpenAiModel::iter()
|
||||
pub fn available_models(&self, cx: &AppContext) -> impl Iterator<Item = OpenAiModel> {
|
||||
if let AssistantProvider::OpenAi {
|
||||
available_models, ..
|
||||
} = &AssistantSettings::get_global(cx).provider
|
||||
{
|
||||
if !available_models.is_empty() {
|
||||
// available_models is set, just return it
|
||||
return available_models.clone().into_iter();
|
||||
}
|
||||
}
|
||||
let available_models = if matches!(self.model, OpenAiModel::Custom { .. }) {
|
||||
// available_models is not set but the default model is set to custom, only show custom
|
||||
vec![self.model.clone()]
|
||||
} else {
|
||||
// default case, use all models except custom
|
||||
OpenAiModel::iter()
|
||||
.filter(|model| !matches!(model, OpenAiModel::Custom { .. }))
|
||||
.collect()
|
||||
};
|
||||
available_models.into_iter()
|
||||
}
|
||||
|
||||
pub fn settings_version(&self) -> usize {
|
||||
@@ -213,7 +232,8 @@ pub fn count_open_ai_tokens(
|
||||
| LanguageModel::Cloud(CloudModel::Claude3_5Sonnet)
|
||||
| LanguageModel::Cloud(CloudModel::Claude3Opus)
|
||||
| LanguageModel::Cloud(CloudModel::Claude3Sonnet)
|
||||
| LanguageModel::Cloud(CloudModel::Claude3Haiku) => {
|
||||
| LanguageModel::Cloud(CloudModel::Claude3Haiku)
|
||||
| LanguageModel::OpenAi(OpenAiModel::Custom { .. }) => {
|
||||
// Tiktoken doesn't yet support these models, so we manually use the
|
||||
// same tokenizer as GPT-4.
|
||||
tiktoken_rs::num_tokens_from_messages("gpt-4", &messages)
|
||||
|
||||
@@ -1298,7 +1298,8 @@ impl Render for PromptEditor {
|
||||
PopoverMenu::new("model-switcher")
|
||||
.menu(move |cx| {
|
||||
ContextMenu::build(cx, |mut menu, cx| {
|
||||
for model in CompletionProvider::global(cx).available_models() {
|
||||
for model in CompletionProvider::global(cx).available_models(cx)
|
||||
{
|
||||
menu = menu.custom_entry(
|
||||
{
|
||||
let model = model.clone();
|
||||
|
||||
@@ -23,7 +23,7 @@ impl RenderOnce for ModelSelector {
|
||||
.with_handle(self.handle)
|
||||
.menu(move |cx| {
|
||||
ContextMenu::build(cx, |mut menu, cx| {
|
||||
for model in CompletionProvider::global(cx).available_models() {
|
||||
for model in CompletionProvider::global(cx).available_models(cx) {
|
||||
menu = menu.custom_entry(
|
||||
{
|
||||
let model = model.clone();
|
||||
|
||||
@@ -841,6 +841,42 @@ impl PromptLibrary {
|
||||
h_flex()
|
||||
.h_full()
|
||||
.gap(Spacing::XXLarge.rems(cx))
|
||||
.children(prompt_editor.token_count.map(
|
||||
|token_count| {
|
||||
let token_count: SharedString =
|
||||
token_count.to_string().into();
|
||||
let label_token_count: SharedString =
|
||||
token_count.to_string().into();
|
||||
|
||||
h_flex()
|
||||
.id("token_count")
|
||||
.tooltip(move |cx| {
|
||||
let token_count =
|
||||
token_count.clone();
|
||||
|
||||
Tooltip::with_meta(
|
||||
format!(
|
||||
"{} tokens",
|
||||
token_count.clone()
|
||||
),
|
||||
None,
|
||||
format!(
|
||||
"Model: {}",
|
||||
current_model
|
||||
.display_name()
|
||||
),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.child(
|
||||
Label::new(format!(
|
||||
"{} tokens",
|
||||
label_token_count.clone()
|
||||
))
|
||||
.color(Color::Muted),
|
||||
)
|
||||
},
|
||||
))
|
||||
.child(
|
||||
IconButton::new(
|
||||
"delete-prompt",
|
||||
@@ -920,38 +956,7 @@ impl PromptLibrary {
|
||||
.on_action(cx.listener(Self::move_up_from_body))
|
||||
.flex_grow()
|
||||
.h_full()
|
||||
.child(prompt_editor.body_editor.clone())
|
||||
.children(prompt_editor.token_count.map(|token_count| {
|
||||
let token_count: SharedString = token_count.to_string().into();
|
||||
let label_token_count: SharedString =
|
||||
token_count.to_string().into();
|
||||
|
||||
h_flex()
|
||||
.id("token_count")
|
||||
.absolute()
|
||||
.bottom_1()
|
||||
.right_4()
|
||||
.flex_initial()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.tooltip(move |cx| {
|
||||
let token_count = token_count.clone();
|
||||
|
||||
Tooltip::with_meta(
|
||||
format!("{} tokens", token_count.clone()),
|
||||
None,
|
||||
format!("Model: {}", current_model.display_name()),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.child(
|
||||
Label::new(format!(
|
||||
"{} tokens",
|
||||
label_token_count.clone()
|
||||
))
|
||||
.color(Color::Muted),
|
||||
)
|
||||
})),
|
||||
.child(prompt_editor.body_editor.clone()),
|
||||
),
|
||||
)
|
||||
}))
|
||||
|
||||
@@ -6,118 +6,106 @@ pub fn generate_content_prompt(
|
||||
language_name: Option<&str>,
|
||||
buffer: BufferSnapshot,
|
||||
range: Range<usize>,
|
||||
project_name: Option<String>,
|
||||
_project_name: Option<String>,
|
||||
) -> anyhow::Result<String> {
|
||||
let mut prompt = String::new();
|
||||
|
||||
let content_type = match language_name {
|
||||
None | Some("Markdown" | "Plain Text") => {
|
||||
writeln!(prompt, "You are an expert engineer.")?;
|
||||
"Text"
|
||||
}
|
||||
Some(language_name) => {
|
||||
writeln!(prompt, "You are an expert {language_name} engineer.")?;
|
||||
writeln!(
|
||||
prompt,
|
||||
"Your answer MUST always and only be valid {}.",
|
||||
language_name
|
||||
"Here's a file of text that I'm going to ask you to make an edit to."
|
||||
)?;
|
||||
"Code"
|
||||
"text"
|
||||
}
|
||||
Some(language_name) => {
|
||||
writeln!(
|
||||
prompt,
|
||||
"Here's a file of {language_name} that I'm going to ask you to make an edit to."
|
||||
)?;
|
||||
"code"
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(project_name) = project_name {
|
||||
writeln!(
|
||||
prompt,
|
||||
"You are currently working inside the '{project_name}' project in code editor Zed."
|
||||
)?;
|
||||
}
|
||||
|
||||
writeln!(
|
||||
prompt,
|
||||
"The user has the following file open in the editor:"
|
||||
)?;
|
||||
const MAX_CTX: usize = 50000;
|
||||
let mut is_truncated = false;
|
||||
if range.is_empty() {
|
||||
write!(prompt, "```")?;
|
||||
if let Some(language_name) = language_name {
|
||||
write!(prompt, "{language_name}")?;
|
||||
}
|
||||
|
||||
for chunk in buffer.as_rope().chunks_in_range(0..range.start) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
prompt.push_str("<|CURSOR|>");
|
||||
for chunk in buffer.as_rope().chunks_in_range(range.start..buffer.len()) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
if !prompt.ends_with('\n') {
|
||||
prompt.push('\n');
|
||||
}
|
||||
writeln!(prompt, "```")?;
|
||||
prompt.push('\n');
|
||||
|
||||
writeln!(
|
||||
prompt,
|
||||
"Assume the cursor is located where the `<|CURSOR|>` span is."
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"{content_type} can't be replaced, so assume your answer will be inserted at the cursor.",
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Generate {content_type} based on the users prompt: {user_prompt}",
|
||||
)
|
||||
.unwrap();
|
||||
prompt.push_str("The point you'll need to insert at is marked with <insert_here></insert_here>.\n\n<document>");
|
||||
} else {
|
||||
write!(prompt, "```")?;
|
||||
for chunk in buffer.as_rope().chunks() {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
if !prompt.ends_with('\n') {
|
||||
prompt.push('\n');
|
||||
}
|
||||
writeln!(prompt, "```")?;
|
||||
prompt.push('\n');
|
||||
|
||||
writeln!(
|
||||
prompt,
|
||||
"In particular, the following piece of text is selected:"
|
||||
)?;
|
||||
write!(prompt, "```")?;
|
||||
if let Some(language_name) = language_name {
|
||||
write!(prompt, "{language_name}")?;
|
||||
}
|
||||
prompt.push('\n');
|
||||
prompt.push_str("The section you'll need to rewrite is marked with <rewrite_this></rewrite_this> tags.\n\n<document>");
|
||||
}
|
||||
// Include file content.
|
||||
let before_range = 0..range.start;
|
||||
let truncated_before = if before_range.len() > MAX_CTX {
|
||||
is_truncated = true;
|
||||
range.start - MAX_CTX..range.start
|
||||
} else {
|
||||
before_range
|
||||
};
|
||||
let mut non_rewrite_len = truncated_before.len();
|
||||
for chunk in buffer.text_for_range(truncated_before) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
if !range.is_empty() {
|
||||
prompt.push_str("<rewrite_this>\n");
|
||||
for chunk in buffer.text_for_range(range.clone()) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
if !prompt.ends_with('\n') {
|
||||
prompt.push('\n');
|
||||
}
|
||||
writeln!(prompt, "```")?;
|
||||
prompt.push('\n');
|
||||
|
||||
writeln!(
|
||||
prompt,
|
||||
"Modify the user's selected {content_type} based upon the users prompt: {user_prompt}"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"You must reply with only the adjusted {content_type}, not the entire file."
|
||||
)
|
||||
.unwrap();
|
||||
prompt.push_str("\n<rewrite_this>");
|
||||
} else {
|
||||
prompt.push_str("<insert_here></insert_here>");
|
||||
}
|
||||
let after_range = range.end..buffer.len();
|
||||
let truncated_after = if after_range.len() > MAX_CTX {
|
||||
is_truncated = true;
|
||||
range.end..range.end + MAX_CTX
|
||||
} else {
|
||||
after_range
|
||||
};
|
||||
non_rewrite_len += truncated_after.len();
|
||||
for chunk in buffer.text_for_range(truncated_after) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
|
||||
writeln!(prompt, "Never make remarks about the output.").unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Do not return anything else, except the generated {content_type}."
|
||||
)
|
||||
.unwrap();
|
||||
write!(prompt, "</document>\n\n").unwrap();
|
||||
|
||||
if is_truncated {
|
||||
writeln!(prompt, "The context around the relevant section has been truncated (possibly in the middle of a line) for brevity.\n")?;
|
||||
}
|
||||
|
||||
if range.is_empty() {
|
||||
writeln!(
|
||||
prompt,
|
||||
"You can't replace {content_type}, your answer will be inserted in place of the `<insert_here></insert_here>` tags. Don't include the insert_here tags in your output.",
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
prompt,
|
||||
"Generate {content_type} based on the following prompt:\n\n<prompt>\n{user_prompt}\n</prompt>",
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(prompt, "Match the indentation in the original file in the inserted {content_type}, don't include any indentation on blank lines.\n").unwrap();
|
||||
prompt.push_str("Immediately start with the following format with no remarks:\n\n```\n{{INSERTED_CODE}}\n```");
|
||||
} else {
|
||||
writeln!(prompt, "Edit the section of {content_type} in <rewrite_this></rewrite_this> tags based on the following prompt:'").unwrap();
|
||||
writeln!(prompt, "\n<prompt>\n{user_prompt}\n</prompt>\n").unwrap();
|
||||
let rewrite_len = range.end - range.start;
|
||||
if rewrite_len < 20000 && rewrite_len * 2 < non_rewrite_len {
|
||||
writeln!(prompt, "And here's the section to rewrite based on that prompt again for reference:\n\n<rewrite_this>\n").unwrap();
|
||||
for chunk in buffer.text_for_range(range.clone()) {
|
||||
prompt.push_str(chunk);
|
||||
}
|
||||
writeln!(prompt, "\n</rewrite_this>\n").unwrap();
|
||||
}
|
||||
writeln!(prompt, "Only make changes that are necessary to fulfill the prompt, leave everything else as-is. All surrounding {content_type} will be preserved.\n").unwrap();
|
||||
write!(
|
||||
prompt,
|
||||
"Start at the indentation level in the original file in the rewritten {content_type}. "
|
||||
)
|
||||
.unwrap();
|
||||
prompt.push_str("Don't stop until you've rewritten the entire section, even if you have no more changes to make, always write out the whole section with no unnecessary elisions.");
|
||||
prompt.push_str("\n\nImmediately start with the following format with no remarks:\n\n```\n{{REWRITTEN_CODE}}\n```");
|
||||
}
|
||||
|
||||
Ok(prompt)
|
||||
}
|
||||
|
||||
@@ -196,23 +196,24 @@ mod linux {
|
||||
impl Detect {
|
||||
pub fn detect(path: Option<&Path>) -> anyhow::Result<impl InstalledApp> {
|
||||
let path = if let Some(path) = path {
|
||||
path.to_path_buf().canonicalize()
|
||||
path.to_path_buf().canonicalize()?
|
||||
} else {
|
||||
let cli = env::current_exe()?;
|
||||
let dir = cli
|
||||
.parent()
|
||||
.and_then(Path::parent)
|
||||
.ok_or_else(|| anyhow!("no parent path for cli"))?;
|
||||
|
||||
match dir.join("libexec").join("zed-editor").canonicalize() {
|
||||
Ok(path) => Ok(path),
|
||||
// In development cli and zed are in the ./target/ directory together
|
||||
Err(e) => match cli.parent().unwrap().join("zed").canonicalize() {
|
||||
Ok(path) if path != cli => Ok(path),
|
||||
_ => Err(e),
|
||||
},
|
||||
}
|
||||
}?;
|
||||
// libexec is the standard, lib/zed is for Arch (and other non-libexec distros),
|
||||
// ./zed is for the target directory in development builds.
|
||||
let possible_locations =
|
||||
["../libexec/zed-editor", "../lib/zed/zed-editor", "./zed"];
|
||||
possible_locations
|
||||
.iter()
|
||||
.find_map(|p| dir.join(p).canonicalize().ok().filter(|path| path != &cli))
|
||||
.ok_or_else(|| {
|
||||
anyhow!("could not find any of: {}", possible_locations.join(", "))
|
||||
})?
|
||||
};
|
||||
|
||||
Ok(App(path))
|
||||
}
|
||||
|
||||
@@ -611,6 +611,7 @@ impl Telemetry {
|
||||
|
||||
let request_body = EventRequestBody {
|
||||
installation_id: state.installation_id.as_deref().map(Into::into),
|
||||
metrics_id: state.metrics_id.as_deref().map(Into::into),
|
||||
session_id: state.session_id.clone(),
|
||||
is_staff: state.is_staff,
|
||||
app_version: state.app_version.clone(),
|
||||
|
||||
@@ -87,51 +87,27 @@ impl Global {
|
||||
}
|
||||
|
||||
pub fn observed_any(&self, other: &Self) -> bool {
|
||||
let mut lhs = self.0.iter();
|
||||
let mut rhs = other.0.iter();
|
||||
loop {
|
||||
if let Some(left) = lhs.next() {
|
||||
if let Some(right) = rhs.next() {
|
||||
if *right > 0 && left >= right {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
self.0
|
||||
.iter()
|
||||
.zip(other.0.iter())
|
||||
.any(|(left, right)| *right > 0 && left >= right)
|
||||
}
|
||||
|
||||
pub fn observed_all(&self, other: &Self) -> bool {
|
||||
let mut lhs = self.0.iter();
|
||||
let mut rhs = other.0.iter();
|
||||
loop {
|
||||
if let Some(left) = lhs.next() {
|
||||
if let Some(right) = rhs.next() {
|
||||
if left < right {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return rhs.next().is_none();
|
||||
}
|
||||
}
|
||||
self.0.iter().all(|left| match rhs.next() {
|
||||
Some(right) => left >= right,
|
||||
None => true,
|
||||
}) && rhs.next().is_none()
|
||||
}
|
||||
|
||||
pub fn changed_since(&self, other: &Self) -> bool {
|
||||
if self.0.len() > other.0.len() {
|
||||
return true;
|
||||
}
|
||||
for (left, right) in self.0.iter().zip(other.0.iter()) {
|
||||
if left > right {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
self.0.len() > other.0.len()
|
||||
|| self
|
||||
.0
|
||||
.iter()
|
||||
.zip(other.0.iter())
|
||||
.any(|(left, right)| left > right)
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = Lamport> + '_ {
|
||||
|
||||
@@ -664,6 +664,7 @@ where
|
||||
#[derive(Serialize, Debug, clickhouse::Row)]
|
||||
pub struct EditorEventRow {
|
||||
installation_id: String,
|
||||
metrics_id: String,
|
||||
operation: String,
|
||||
app_version: String,
|
||||
file_extension: String,
|
||||
@@ -713,6 +714,7 @@ impl EditorEventRow {
|
||||
os_version: body.os_version.clone().unwrap_or_default(),
|
||||
architecture: body.architecture.clone(),
|
||||
installation_id: body.installation_id.clone().unwrap_or_default(),
|
||||
metrics_id: body.metrics_id.clone().unwrap_or_default(),
|
||||
session_id: body.session_id.clone(),
|
||||
is_staff: body.is_staff,
|
||||
time: time.timestamp_millis(),
|
||||
|
||||
@@ -124,6 +124,6 @@ fn notification_window_options(
|
||||
display_id: Some(screen.id()),
|
||||
window_background: WindowBackgroundAppearance::default(),
|
||||
app_id: Some(app_id.to_owned()),
|
||||
window_min_size: Size::default(),
|
||||
window_min_size: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -720,8 +720,7 @@ impl DisplaySnapshot {
|
||||
if let Some(severity) = chunk.diagnostic_severity {
|
||||
// Omit underlines for HINT/INFO diagnostics on 'unnecessary' code.
|
||||
if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary {
|
||||
let diagnostic_color =
|
||||
super::diagnostic_style(severity, true, &editor_style.status);
|
||||
let diagnostic_color = super::diagnostic_style(severity, &editor_style.status);
|
||||
diagnostic_highlight.underline = Some(UnderlineStyle {
|
||||
color: Some(diagnostic_color),
|
||||
thickness: 1.0.into(),
|
||||
@@ -957,16 +956,18 @@ impl DisplaySnapshot {
|
||||
return false;
|
||||
}
|
||||
|
||||
for next_row in (buffer_row.0 + 1)..=max_row.0 {
|
||||
let next_line_indent = self.line_indent_for_buffer_row(MultiBufferRow(next_row));
|
||||
if next_line_indent.raw_len() > line_indent.raw_len() {
|
||||
return true;
|
||||
} else if !next_line_indent.is_line_blank() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
(buffer_row.0 + 1..=max_row.0)
|
||||
.find_map(|next_row| {
|
||||
let next_line_indent = self.line_indent_for_buffer_row(MultiBufferRow(next_row));
|
||||
if next_line_indent.raw_len() > line_indent.raw_len() {
|
||||
Some(true)
|
||||
} else if !next_line_indent.is_line_blank() {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub fn foldable_range(
|
||||
|
||||
@@ -2914,6 +2914,9 @@ impl Editor {
|
||||
let start_offset = TO::to_offset(&range.start, &buffer_snapshot);
|
||||
let end_offset = start_offset + end_difference;
|
||||
let start_offset = start_offset + start_difference;
|
||||
if start_offset > buffer_snapshot.len() || end_offset > buffer_snapshot.len() {
|
||||
continue;
|
||||
}
|
||||
let start = buffer_snapshot.anchor_after(start_offset);
|
||||
let end = buffer_snapshot.anchor_after(end_offset);
|
||||
linked_edits
|
||||
@@ -8813,13 +8816,7 @@ impl Editor {
|
||||
let display_point = initial_point.to_display_point(snapshot);
|
||||
let mut hunks = hunks
|
||||
.map(|hunk| diff_hunk_to_display(&hunk, &snapshot))
|
||||
.filter(|hunk| {
|
||||
if is_wrapped {
|
||||
true
|
||||
} else {
|
||||
!hunk.contains_display_row(display_point.row())
|
||||
}
|
||||
})
|
||||
.filter(|hunk| is_wrapped || !hunk.contains_display_row(display_point.row()))
|
||||
.dedup();
|
||||
|
||||
if let Some(hunk) = hunks.next() {
|
||||
@@ -12390,6 +12387,7 @@ impl ViewInputHandler for Editor {
|
||||
let font_id = cx.text_system().resolve_font(&style.text.font());
|
||||
let font_size = style.text.font_size.to_pixels(cx.rem_size());
|
||||
let line_height = style.text.line_height_in_pixels(cx.rem_size());
|
||||
|
||||
let em_width = cx
|
||||
.text_system()
|
||||
.typographic_bounds(font_id, font_size, 'm')
|
||||
@@ -12517,7 +12515,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren
|
||||
let group_id: SharedString = cx.block_id.to_string().into();
|
||||
|
||||
let mut text_style = cx.text_style().clone();
|
||||
text_style.color = diagnostic_style(diagnostic.severity, true, cx.theme().status());
|
||||
text_style.color = diagnostic_style(diagnostic.severity, cx.theme().status());
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
text_style.font_family = theme_settings.buffer_font.family.clone();
|
||||
text_style.font_style = theme_settings.buffer_font.style;
|
||||
@@ -12613,25 +12611,19 @@ pub fn highlight_diagnostic_message(diagnostic: &Diagnostic) -> (SharedString, V
|
||||
prev_offset = ix + 1;
|
||||
if in_code_block {
|
||||
code_ranges.push(prev_len..text_without_backticks.len());
|
||||
in_code_block = false;
|
||||
} else {
|
||||
in_code_block = true;
|
||||
}
|
||||
in_code_block = !in_code_block;
|
||||
}
|
||||
|
||||
(text_without_backticks.into(), code_ranges)
|
||||
}
|
||||
|
||||
fn diagnostic_style(severity: DiagnosticSeverity, valid: bool, colors: &StatusColors) -> Hsla {
|
||||
match (severity, valid) {
|
||||
(DiagnosticSeverity::ERROR, true) => colors.error,
|
||||
(DiagnosticSeverity::ERROR, false) => colors.error,
|
||||
(DiagnosticSeverity::WARNING, true) => colors.warning,
|
||||
(DiagnosticSeverity::WARNING, false) => colors.warning,
|
||||
(DiagnosticSeverity::INFORMATION, true) => colors.info,
|
||||
(DiagnosticSeverity::INFORMATION, false) => colors.info,
|
||||
(DiagnosticSeverity::HINT, true) => colors.info,
|
||||
(DiagnosticSeverity::HINT, false) => colors.info,
|
||||
fn diagnostic_style(severity: DiagnosticSeverity, colors: &StatusColors) -> Hsla {
|
||||
match severity {
|
||||
DiagnosticSeverity::ERROR => colors.error,
|
||||
DiagnosticSeverity::WARNING => colors.warning,
|
||||
DiagnosticSeverity::INFORMATION => colors.info,
|
||||
DiagnosticSeverity::HINT => colors.info,
|
||||
_ => colors.ignored,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ backtrace = "0.3"
|
||||
collections = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
http = { workspace = true, features = ["test-support"] }
|
||||
unicode-segmentation.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
embed-resource = "2.4"
|
||||
@@ -157,3 +158,7 @@ path = "examples/image/image.rs"
|
||||
[[example]]
|
||||
name = "set_menus"
|
||||
path = "examples/set_menus.rs"
|
||||
|
||||
[[example]]
|
||||
name = "input"
|
||||
path = "examples/input.rs"
|
||||
|
||||
489
crates/gpui/examples/input.rs
Normal file
489
crates/gpui/examples/input.rs
Normal file
@@ -0,0 +1,489 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use gpui::*;
|
||||
use unicode_segmentation::*;
|
||||
|
||||
actions!(
|
||||
text_input,
|
||||
[
|
||||
Backspace,
|
||||
Delete,
|
||||
Left,
|
||||
Right,
|
||||
SelectLeft,
|
||||
SelectRight,
|
||||
SelectAll,
|
||||
Home,
|
||||
End,
|
||||
ShowCharacterPalette
|
||||
]
|
||||
);
|
||||
|
||||
struct TextInput {
|
||||
focus_handle: FocusHandle,
|
||||
content: SharedString,
|
||||
selected_range: Range<usize>,
|
||||
selection_reversed: bool,
|
||||
marked_range: Option<Range<usize>>,
|
||||
last_layout: Option<ShapedLine>,
|
||||
}
|
||||
|
||||
impl TextInput {
|
||||
fn left(&mut self, _: &Left, cx: &mut ViewContext<Self>) {
|
||||
if self.selected_range.is_empty() {
|
||||
self.move_to(self.previous_boundary(self.cursor_offset()), cx);
|
||||
} else {
|
||||
self.move_to(self.selected_range.start, cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn right(&mut self, _: &Right, cx: &mut ViewContext<Self>) {
|
||||
if self.selected_range.is_empty() {
|
||||
self.move_to(self.next_boundary(self.selected_range.end), cx);
|
||||
} else {
|
||||
self.move_to(self.selected_range.end, cx)
|
||||
}
|
||||
}
|
||||
|
||||
fn select_left(&mut self, _: &SelectLeft, cx: &mut ViewContext<Self>) {
|
||||
self.select_to(self.previous_boundary(self.cursor_offset()), cx);
|
||||
}
|
||||
|
||||
fn select_right(&mut self, _: &SelectRight, cx: &mut ViewContext<Self>) {
|
||||
self.select_to(self.next_boundary(self.cursor_offset()), cx);
|
||||
}
|
||||
|
||||
fn select_all(&mut self, _: &SelectAll, cx: &mut ViewContext<Self>) {
|
||||
self.move_to(0, cx);
|
||||
self.select_to(self.content.len(), cx)
|
||||
}
|
||||
|
||||
fn home(&mut self, _: &Home, cx: &mut ViewContext<Self>) {
|
||||
self.move_to(0, cx);
|
||||
}
|
||||
|
||||
fn end(&mut self, _: &End, cx: &mut ViewContext<Self>) {
|
||||
self.move_to(self.content.len(), cx);
|
||||
}
|
||||
|
||||
fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext<Self>) {
|
||||
if self.selected_range.is_empty() {
|
||||
self.select_to(self.previous_boundary(self.cursor_offset()), cx)
|
||||
}
|
||||
self.replace_text_in_range(None, "", cx)
|
||||
}
|
||||
|
||||
fn delete(&mut self, _: &Delete, cx: &mut ViewContext<Self>) {
|
||||
if self.selected_range.is_empty() {
|
||||
self.select_to(self.next_boundary(self.cursor_offset()), cx)
|
||||
}
|
||||
self.replace_text_in_range(None, "", cx)
|
||||
}
|
||||
|
||||
fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext<Self>) {
|
||||
cx.show_character_palette();
|
||||
}
|
||||
|
||||
fn move_to(&mut self, offset: usize, cx: &mut ViewContext<Self>) {
|
||||
self.selected_range = offset..offset;
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
fn cursor_offset(&self) -> usize {
|
||||
if self.selection_reversed {
|
||||
self.selected_range.start
|
||||
} else {
|
||||
self.selected_range.end
|
||||
}
|
||||
}
|
||||
|
||||
fn select_to(&mut self, offset: usize, cx: &mut ViewContext<Self>) {
|
||||
if self.selection_reversed {
|
||||
self.selected_range.start = offset
|
||||
} else {
|
||||
self.selected_range.end = offset
|
||||
};
|
||||
if self.selected_range.end < self.selected_range.start {
|
||||
self.selection_reversed = !self.selection_reversed;
|
||||
self.selected_range = self.selected_range.end..self.selected_range.start;
|
||||
}
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
fn offset_from_utf16(&self, offset: usize) -> usize {
|
||||
let mut utf8_offset = 0;
|
||||
let mut utf16_count = 0;
|
||||
|
||||
for ch in self.content.chars() {
|
||||
if utf16_count >= offset {
|
||||
break;
|
||||
}
|
||||
utf16_count += ch.len_utf16();
|
||||
utf8_offset += ch.len_utf8();
|
||||
}
|
||||
|
||||
utf8_offset
|
||||
}
|
||||
|
||||
fn offset_to_utf16(&self, offset: usize) -> usize {
|
||||
let mut utf16_offset = 0;
|
||||
let mut utf8_count = 0;
|
||||
|
||||
for ch in self.content.chars() {
|
||||
if utf8_count >= offset {
|
||||
break;
|
||||
}
|
||||
utf8_count += ch.len_utf8();
|
||||
utf16_offset += ch.len_utf16();
|
||||
}
|
||||
|
||||
utf16_offset
|
||||
}
|
||||
|
||||
fn range_to_utf16(&self, range: &Range<usize>) -> Range<usize> {
|
||||
self.offset_to_utf16(range.start)..self.offset_to_utf16(range.end)
|
||||
}
|
||||
|
||||
fn range_from_utf16(&self, range_utf16: &Range<usize>) -> Range<usize> {
|
||||
self.offset_from_utf16(range_utf16.start)..self.offset_from_utf16(range_utf16.end)
|
||||
}
|
||||
|
||||
fn previous_boundary(&self, offset: usize) -> usize {
|
||||
self.content
|
||||
.grapheme_indices(true)
|
||||
.rev()
|
||||
.find_map(|(idx, _)| (idx < offset).then_some(idx))
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn next_boundary(&self, offset: usize) -> usize {
|
||||
self.content
|
||||
.grapheme_indices(true)
|
||||
.find_map(|(idx, _)| (idx > offset).then_some(idx))
|
||||
.unwrap_or(self.content.len())
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewInputHandler for TextInput {
|
||||
fn text_for_range(
|
||||
&mut self,
|
||||
range_utf16: Range<usize>,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Option<String> {
|
||||
let range = self.range_from_utf16(&range_utf16);
|
||||
Some(self.content[range].to_string())
|
||||
}
|
||||
|
||||
fn selected_text_range(&mut self, _cx: &mut ViewContext<Self>) -> Option<Range<usize>> {
|
||||
Some(self.range_to_utf16(&self.selected_range))
|
||||
}
|
||||
|
||||
fn marked_text_range(&self, _cx: &mut ViewContext<Self>) -> Option<Range<usize>> {
|
||||
self.marked_range
|
||||
.as_ref()
|
||||
.map(|range| self.range_to_utf16(range))
|
||||
}
|
||||
|
||||
fn unmark_text(&mut self, _cx: &mut ViewContext<Self>) {
|
||||
self.marked_range = None;
|
||||
}
|
||||
|
||||
fn replace_text_in_range(
|
||||
&mut self,
|
||||
range_utf16: Option<Range<usize>>,
|
||||
new_text: &str,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let range = range_utf16
|
||||
.as_ref()
|
||||
.map(|range_utf16| self.range_from_utf16(range_utf16))
|
||||
.or(self.marked_range.clone())
|
||||
.unwrap_or(self.selected_range.clone());
|
||||
|
||||
self.content =
|
||||
(self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
|
||||
.into();
|
||||
self.selected_range = range.start + new_text.len()..range.start + new_text.len();
|
||||
self.marked_range.take();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn replace_and_mark_text_in_range(
|
||||
&mut self,
|
||||
range_utf16: Option<Range<usize>>,
|
||||
new_text: &str,
|
||||
new_selected_range_utf16: Option<Range<usize>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let range = range_utf16
|
||||
.as_ref()
|
||||
.map(|range_utf16| self.range_from_utf16(range_utf16))
|
||||
.or(self.marked_range.clone())
|
||||
.unwrap_or(self.selected_range.clone());
|
||||
|
||||
self.content =
|
||||
(self.content[0..range.start].to_owned() + new_text + &self.content[range.end..])
|
||||
.into();
|
||||
self.marked_range = Some(range.start..range.start + new_text.len());
|
||||
self.selected_range = new_selected_range_utf16
|
||||
.as_ref()
|
||||
.map(|range_utf16| self.range_from_utf16(range_utf16))
|
||||
.map(|new_range| new_range.start + range.start..new_range.end + range.end)
|
||||
.unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len());
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn bounds_for_range(
|
||||
&mut self,
|
||||
range_utf16: Range<usize>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_cx: &mut ViewContext<Self>,
|
||||
) -> Option<Bounds<Pixels>> {
|
||||
let Some(last_layout) = self.last_layout.as_ref() else {
|
||||
return None;
|
||||
};
|
||||
let range = self.range_from_utf16(&range_utf16);
|
||||
Some(Bounds::from_corners(
|
||||
point(
|
||||
bounds.left() + last_layout.x_for_index(range.start),
|
||||
bounds.top(),
|
||||
),
|
||||
point(
|
||||
bounds.left() + last_layout.x_for_index(range.end),
|
||||
bounds.bottom(),
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
struct TextElement {
|
||||
input: View<TextInput>,
|
||||
}
|
||||
|
||||
struct PrepaintState {
|
||||
line: Option<ShapedLine>,
|
||||
cursor: Option<PaintQuad>,
|
||||
selection: Option<PaintQuad>,
|
||||
}
|
||||
|
||||
impl IntoElement for TextElement {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for TextElement {
|
||||
type RequestLayoutState = ();
|
||||
|
||||
type PrepaintState = PrepaintState;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let mut style = Style::default();
|
||||
style.size.width = relative(1.).into();
|
||||
style.size.height = cx.line_height().into();
|
||||
(cx.request_layout(style, []), ())
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
cx: &mut WindowContext,
|
||||
) -> Self::PrepaintState {
|
||||
let input = self.input.read(cx);
|
||||
let content = input.content.clone();
|
||||
let selected_range = input.selected_range.clone();
|
||||
let cursor = input.cursor_offset();
|
||||
let style = cx.text_style();
|
||||
let run = TextRun {
|
||||
len: input.content.len(),
|
||||
font: style.font(),
|
||||
color: style.color,
|
||||
background_color: None,
|
||||
underline: None,
|
||||
strikethrough: None,
|
||||
};
|
||||
let runs = if let Some(marked_range) = input.marked_range.as_ref() {
|
||||
vec![
|
||||
TextRun {
|
||||
len: marked_range.start,
|
||||
..run.clone()
|
||||
},
|
||||
TextRun {
|
||||
len: marked_range.end - marked_range.start,
|
||||
underline: Some(UnderlineStyle {
|
||||
color: Some(run.color),
|
||||
thickness: px(1.0),
|
||||
wavy: false,
|
||||
}),
|
||||
..run.clone()
|
||||
},
|
||||
TextRun {
|
||||
len: input.content.len() - marked_range.end,
|
||||
..run.clone()
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|run| run.len > 0)
|
||||
.collect()
|
||||
} else {
|
||||
vec![run]
|
||||
};
|
||||
|
||||
let font_size = style.font_size.to_pixels(cx.rem_size());
|
||||
let line = cx
|
||||
.text_system()
|
||||
.shape_line(content, font_size, &runs)
|
||||
.unwrap();
|
||||
|
||||
let cursor_pos = line.x_for_index(cursor);
|
||||
let (selection, cursor) = if selected_range.is_empty() {
|
||||
(
|
||||
None,
|
||||
Some(fill(
|
||||
Bounds::new(
|
||||
point(bounds.left() + cursor_pos, bounds.top()),
|
||||
size(px(2.), bounds.bottom() - bounds.top()),
|
||||
),
|
||||
gpui::blue(),
|
||||
)),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
Some(fill(
|
||||
Bounds::from_corners(
|
||||
point(
|
||||
bounds.left() + line.x_for_index(selected_range.start),
|
||||
bounds.top(),
|
||||
),
|
||||
point(
|
||||
bounds.left() + line.x_for_index(selected_range.end),
|
||||
bounds.bottom(),
|
||||
),
|
||||
),
|
||||
rgba(0x3311FF30),
|
||||
)),
|
||||
None,
|
||||
)
|
||||
};
|
||||
PrepaintState {
|
||||
line: Some(line),
|
||||
cursor,
|
||||
selection,
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_id: Option<&GlobalElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
_request_layout: &mut Self::RequestLayoutState,
|
||||
prepaint: &mut Self::PrepaintState,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let focus_handle = self.input.read(cx).focus_handle.clone();
|
||||
cx.handle_input(
|
||||
&focus_handle,
|
||||
ElementInputHandler::new(bounds, self.input.clone()),
|
||||
);
|
||||
if let Some(selection) = prepaint.selection.take() {
|
||||
cx.paint_quad(selection)
|
||||
}
|
||||
let line = prepaint.line.take().unwrap();
|
||||
line.paint(bounds.origin, cx.line_height(), cx).unwrap();
|
||||
|
||||
if let Some(cursor) = prepaint.cursor.take() {
|
||||
cx.paint_quad(cursor);
|
||||
}
|
||||
self.input.update(cx, |input, _cx| {
|
||||
input.last_layout = Some(line);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for TextInput {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
.key_context("TextInput")
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_action(cx.listener(Self::backspace))
|
||||
.on_action(cx.listener(Self::delete))
|
||||
.on_action(cx.listener(Self::left))
|
||||
.on_action(cx.listener(Self::right))
|
||||
.on_action(cx.listener(Self::select_left))
|
||||
.on_action(cx.listener(Self::select_right))
|
||||
.on_action(cx.listener(Self::select_all))
|
||||
.on_action(cx.listener(Self::home))
|
||||
.on_action(cx.listener(Self::end))
|
||||
.on_action(cx.listener(Self::show_character_palette))
|
||||
.bg(rgb(0xeeeeee))
|
||||
.size_full()
|
||||
.line_height(px(30.))
|
||||
.text_size(px(24.))
|
||||
.child(
|
||||
div()
|
||||
.h(px(30. + 4. * 2.))
|
||||
.w_full()
|
||||
.p(px(4.))
|
||||
.bg(white())
|
||||
.child(TextElement {
|
||||
input: cx.view().clone(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
App::new().run(|cx: &mut AppContext| {
|
||||
let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx);
|
||||
cx.bind_keys([
|
||||
KeyBinding::new("backspace", Backspace, None),
|
||||
KeyBinding::new("delete", Delete, None),
|
||||
KeyBinding::new("left", Left, None),
|
||||
KeyBinding::new("right", Right, None),
|
||||
KeyBinding::new("shift-left", SelectLeft, None),
|
||||
KeyBinding::new("shift-right", SelectRight, None),
|
||||
KeyBinding::new("cmd-a", SelectAll, None),
|
||||
KeyBinding::new("home", Home, None),
|
||||
KeyBinding::new("end", End, None),
|
||||
KeyBinding::new("ctrl-cmd-space", ShowCharacterPalette, None),
|
||||
]);
|
||||
let window = cx
|
||||
.open_window(
|
||||
WindowOptions {
|
||||
window_bounds: Some(WindowBounds::Windowed(bounds)),
|
||||
..Default::default()
|
||||
},
|
||||
|cx| {
|
||||
cx.new_view(|cx| TextInput {
|
||||
focus_handle: cx.focus_handle(),
|
||||
content: "".into(),
|
||||
selected_range: 0..0,
|
||||
selection_reversed: false,
|
||||
marked_range: None,
|
||||
last_layout: None,
|
||||
})
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
window
|
||||
.update(cx, |view, cx| {
|
||||
view.focus_handle.focus(cx);
|
||||
cx.activate(true)
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
}
|
||||
@@ -51,7 +51,7 @@ fn main() {
|
||||
kind: WindowKind::PopUp,
|
||||
is_movable: false,
|
||||
app_id: None,
|
||||
window_min_size: Size::default(),
|
||||
window_min_size: None,
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -569,7 +569,7 @@ pub struct WindowOptions {
|
||||
pub app_id: Option<String>,
|
||||
|
||||
/// Window minimum size
|
||||
pub window_min_size: Size<Pixels>,
|
||||
pub window_min_size: Option<Size<Pixels>>,
|
||||
}
|
||||
|
||||
/// The variables that can be configured when creating a new window
|
||||
@@ -599,7 +599,7 @@ pub(crate) struct WindowParams {
|
||||
pub window_background: WindowBackgroundAppearance,
|
||||
|
||||
#[cfg_attr(target_os = "linux", allow(dead_code))]
|
||||
pub window_min_size: Size<Pixels>,
|
||||
pub window_min_size: Option<Size<Pixels>>,
|
||||
}
|
||||
|
||||
/// Represents the status of how a window should be opened.
|
||||
@@ -648,7 +648,7 @@ impl Default for WindowOptions {
|
||||
display_id: None,
|
||||
window_background: WindowBackgroundAppearance::default(),
|
||||
app_id: None,
|
||||
window_min_size: Size::default(),
|
||||
window_min_size: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +94,27 @@ impl Keystroke {
|
||||
}
|
||||
}
|
||||
|
||||
//Allow for the user to specify a keystroke modifier as the key itself
|
||||
//This sets the `key` to the modifier, and disables the modifier
|
||||
if key.is_none() {
|
||||
if shift {
|
||||
key = Some("shift".to_string());
|
||||
shift = false;
|
||||
} else if control {
|
||||
key = Some("control".to_string());
|
||||
control = false;
|
||||
} else if alt {
|
||||
key = Some("alt".to_string());
|
||||
alt = false;
|
||||
} else if platform {
|
||||
key = Some("platform".to_string());
|
||||
platform = false;
|
||||
} else if function {
|
||||
key = Some("function".to_string());
|
||||
function = false;
|
||||
}
|
||||
}
|
||||
|
||||
let key = key.ok_or_else(|| anyhow!("Invalid keystroke `{}`", source))?;
|
||||
|
||||
Ok(Keystroke {
|
||||
@@ -186,6 +207,10 @@ impl std::fmt::Display for Keystroke {
|
||||
"right" => '→',
|
||||
"tab" => '⇥',
|
||||
"escape" => '⎋',
|
||||
"shift" => '⇧',
|
||||
"control" => '⌃',
|
||||
"alt" => '⌥',
|
||||
"platform" => '⌘',
|
||||
key => {
|
||||
if key.len() == 1 {
|
||||
key.chars().next().unwrap().to_ascii_uppercase()
|
||||
@@ -241,6 +266,15 @@ impl Modifiers {
|
||||
}
|
||||
}
|
||||
|
||||
/// How many modifier keys are pressed
|
||||
pub fn number_of_modifiers(&self) -> u8 {
|
||||
self.control as u8
|
||||
+ self.alt as u8
|
||||
+ self.shift as u8
|
||||
+ self.platform as u8
|
||||
+ self.function as u8
|
||||
}
|
||||
|
||||
/// helper method for Modifiers with no modifiers
|
||||
pub fn none() -> Modifiers {
|
||||
Default::default()
|
||||
|
||||
@@ -22,7 +22,7 @@ impl HeadlessClient {
|
||||
pub(crate) fn new() -> Self {
|
||||
let event_loop = EventLoop::try_new().unwrap();
|
||||
|
||||
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
|
||||
let (common, main_receiver) = LinuxCommon::new(Box::new(event_loop.get_signal()));
|
||||
|
||||
let handle = event_loop.handle();
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use std::any::{type_name, Any};
|
||||
use std::cell::{self, RefCell};
|
||||
use std::env;
|
||||
use std::ffi::OsString;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
@@ -83,6 +84,16 @@ pub(crate) struct PlatformHandlers {
|
||||
pub(crate) validate_app_menu_command: Option<Box<dyn FnMut(&dyn Action) -> bool>>,
|
||||
}
|
||||
|
||||
pub trait QuitSignal {
|
||||
fn quit(&mut self);
|
||||
}
|
||||
|
||||
impl QuitSignal for LoopSignal {
|
||||
fn quit(&mut self) {
|
||||
self.stop();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct LinuxCommon {
|
||||
pub(crate) background_executor: BackgroundExecutor,
|
||||
pub(crate) foreground_executor: ForegroundExecutor,
|
||||
@@ -90,12 +101,12 @@ pub(crate) struct LinuxCommon {
|
||||
pub(crate) appearance: WindowAppearance,
|
||||
pub(crate) auto_hide_scrollbars: bool,
|
||||
pub(crate) callbacks: PlatformHandlers,
|
||||
pub(crate) signal: LoopSignal,
|
||||
pub(crate) signal: Box<dyn QuitSignal>,
|
||||
pub(crate) menus: Vec<OwnedMenu>,
|
||||
}
|
||||
|
||||
impl LinuxCommon {
|
||||
pub fn new(signal: LoopSignal) -> (Self, Channel<Runnable>) {
|
||||
pub fn new(signal: Box<dyn QuitSignal>) -> (Self, Channel<Runnable>) {
|
||||
let (main_sender, main_receiver) = calloop::channel::channel::<Runnable>();
|
||||
let text_system = Arc::new(CosmicTextSystem::new());
|
||||
let callbacks = PlatformHandlers::default();
|
||||
@@ -145,7 +156,7 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||
}
|
||||
|
||||
fn quit(&self) {
|
||||
self.with_common(|common| common.signal.stop());
|
||||
self.with_common(|common| common.signal.quit());
|
||||
}
|
||||
|
||||
fn compositor_name(&self) -> &'static str {
|
||||
@@ -508,6 +519,27 @@ pub(super) fn is_within_click_distance(a: Point<Pixels>, b: Point<Pixels>) -> bo
|
||||
diff.x.abs() <= DOUBLE_CLICK_DISTANCE && diff.y.abs() <= DOUBLE_CLICK_DISTANCE
|
||||
}
|
||||
|
||||
pub(super) fn get_xkb_compose_state(cx: &xkb::Context) -> Option<xkb::compose::State> {
|
||||
let mut locales = Vec::default();
|
||||
if let Some(locale) = std::env::var_os("LC_CTYPE") {
|
||||
locales.push(locale);
|
||||
}
|
||||
locales.push(OsString::from("C"));
|
||||
let mut state: Option<xkb::compose::State> = None;
|
||||
for locale in locales {
|
||||
if let Ok(table) =
|
||||
xkb::compose::Table::new_from_locale(&cx, &locale, xkb::compose::COMPILE_NO_FLAGS)
|
||||
{
|
||||
state = Some(xkb::compose::State::new(
|
||||
&table,
|
||||
xkb::compose::STATE_NO_FLAGS,
|
||||
));
|
||||
break;
|
||||
}
|
||||
}
|
||||
state
|
||||
}
|
||||
|
||||
pub(super) unsafe fn read_fd(mut fd: FileDescriptor) -> Result<String> {
|
||||
let mut file = File::from_raw_fd(fd.as_raw_fd());
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use std::cell::{RefCell, RefMut};
|
||||
use std::ffi::OsString;
|
||||
use std::hash::Hash;
|
||||
use std::os::fd::{AsRawFd, BorrowedFd};
|
||||
use std::path::PathBuf;
|
||||
@@ -65,7 +64,6 @@ use xkbcommon::xkb::{self, Keycode, KEYMAP_COMPILE_NO_FLAGS};
|
||||
use super::super::{open_uri_internal, read_fd, DOUBLE_CLICK_INTERVAL};
|
||||
use super::display::WaylandDisplay;
|
||||
use super::window::{ImeInput, WaylandWindowStatePtr};
|
||||
use crate::platform::linux::is_within_click_distance;
|
||||
use crate::platform::linux::wayland::clipboard::{
|
||||
Clipboard, DataOffer, FILE_LIST_MIME_TYPE, TEXT_MIME_TYPE,
|
||||
};
|
||||
@@ -74,6 +72,7 @@ use crate::platform::linux::wayland::serial::{SerialKind, SerialTracker};
|
||||
use crate::platform::linux::wayland::window::WaylandWindow;
|
||||
use crate::platform::linux::xdg_desktop_portal::{Event as XDPEvent, XDPEventSource};
|
||||
use crate::platform::linux::LinuxClient;
|
||||
use crate::platform::linux::{get_xkb_compose_state, is_within_click_distance};
|
||||
use crate::platform::PlatformWindow;
|
||||
use crate::{
|
||||
point, px, size, Bounds, DevicePixels, FileDropEvent, ForegroundExecutor, MouseExitEvent, Size,
|
||||
@@ -311,7 +310,7 @@ impl WaylandClientStatePtr {
|
||||
}
|
||||
}
|
||||
if state.windows.is_empty() {
|
||||
state.common.signal.stop();
|
||||
state.common.signal.quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -407,7 +406,7 @@ impl WaylandClient {
|
||||
|
||||
let event_loop = EventLoop::<WaylandClientStatePtr>::try_new().unwrap();
|
||||
|
||||
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
|
||||
let (common, main_receiver) = LinuxCommon::new(Box::new(event_loop.get_signal()));
|
||||
|
||||
let handle = event_loop.handle();
|
||||
handle
|
||||
@@ -671,7 +670,7 @@ impl LinuxClient for WaylandClient {
|
||||
return;
|
||||
};
|
||||
if state.mouse_focused_window.is_some() || state.keyboard_focused_window.is_some() {
|
||||
state.clipboard.set_primary(item.text);
|
||||
state.clipboard.set_primary(item);
|
||||
let serial = state.serial_tracker.get(SerialKind::KeyPress);
|
||||
let data_source = primary_selection_manager.create_source(&state.globals.qh, ());
|
||||
data_source.offer(state.clipboard.self_mime());
|
||||
@@ -689,7 +688,7 @@ impl LinuxClient for WaylandClient {
|
||||
return;
|
||||
};
|
||||
if state.mouse_focused_window.is_some() || state.keyboard_focused_window.is_some() {
|
||||
state.clipboard.set(item.text);
|
||||
state.clipboard.set(item);
|
||||
let serial = state.serial_tracker.get(SerialKind::KeyPress);
|
||||
let data_source = data_device_manager.create_data_source(&state.globals.qh, ());
|
||||
data_source.offer(state.clipboard.self_mime());
|
||||
@@ -699,25 +698,11 @@ impl LinuxClient for WaylandClient {
|
||||
}
|
||||
|
||||
fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
|
||||
self.0
|
||||
.borrow_mut()
|
||||
.clipboard
|
||||
.read_primary()
|
||||
.map(|s| crate::ClipboardItem {
|
||||
text: s,
|
||||
metadata: None,
|
||||
})
|
||||
self.0.borrow_mut().clipboard.read_primary()
|
||||
}
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
|
||||
self.0
|
||||
.borrow_mut()
|
||||
.clipboard
|
||||
.read()
|
||||
.map(|s| crate::ClipboardItem {
|
||||
text: s,
|
||||
metadata: None,
|
||||
})
|
||||
self.0.borrow_mut().clipboard.read()
|
||||
}
|
||||
|
||||
fn active_window(&self) -> Option<AnyWindowHandle> {
|
||||
@@ -1068,21 +1053,8 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
.flatten()
|
||||
.expect("Failed to create keymap")
|
||||
};
|
||||
let table = {
|
||||
let locale = std::env::var_os("LC_CTYPE").unwrap_or(OsString::from("C"));
|
||||
xkb::compose::Table::new_from_locale(
|
||||
&xkb_context,
|
||||
&locale,
|
||||
xkb::compose::COMPILE_NO_FLAGS,
|
||||
)
|
||||
.log_err()
|
||||
.unwrap()
|
||||
};
|
||||
state.keymap_state = Some(xkb::State::new(&keymap));
|
||||
state.compose_state = Some(xkb::compose::State::new(
|
||||
&table,
|
||||
xkb::compose::STATE_NO_FLAGS,
|
||||
));
|
||||
state.compose_state = get_xkb_compose_state(&xkb_context);
|
||||
}
|
||||
wl_keyboard::Event::Enter {
|
||||
serial, surface, ..
|
||||
@@ -1162,6 +1134,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
compose.feed(keysym);
|
||||
match compose.status() {
|
||||
xkb::Status::Composing => {
|
||||
keystroke.ime_key = None;
|
||||
state.pre_edit_text =
|
||||
compose.utf8().or(Keystroke::underlying_dead_key(keysym));
|
||||
let pre_edit =
|
||||
@@ -1174,7 +1147,9 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr {
|
||||
xkb::Status::Composed => {
|
||||
state.pre_edit_text.take();
|
||||
keystroke.ime_key = compose.utf8();
|
||||
keystroke.key = xkb::keysym_get_name(compose.keysym().unwrap());
|
||||
if let Some(keysym) = compose.keysym() {
|
||||
keystroke.key = xkb::keysym_get_name(keysym);
|
||||
}
|
||||
}
|
||||
xkb::Status::Cancelled => {
|
||||
let pre_edit = state.pre_edit_text.take();
|
||||
|
||||
@@ -9,7 +9,7 @@ use filedescriptor::Pipe;
|
||||
use wayland_client::{protocol::wl_data_offer::WlDataOffer, Connection};
|
||||
use wayland_protocols::wp::primary_selection::zv1::client::zwp_primary_selection_offer_v1::ZwpPrimarySelectionOfferV1;
|
||||
|
||||
use crate::{platform::linux::platform::read_fd, WaylandClientStatePtr};
|
||||
use crate::{platform::linux::platform::read_fd, ClipboardItem, WaylandClientStatePtr};
|
||||
|
||||
pub(crate) const TEXT_MIME_TYPE: &str = "text/plain;charset=utf-8";
|
||||
pub(crate) const FILE_LIST_MIME_TYPE: &str = "text/uri-list";
|
||||
@@ -23,13 +23,13 @@ pub(crate) struct Clipboard {
|
||||
self_mime: String,
|
||||
|
||||
// Internal clipboard
|
||||
contents: Option<String>,
|
||||
primary_contents: Option<String>,
|
||||
contents: Option<ClipboardItem>,
|
||||
primary_contents: Option<ClipboardItem>,
|
||||
|
||||
// External clipboard
|
||||
cached_read: Option<String>,
|
||||
cached_read: Option<ClipboardItem>,
|
||||
current_offer: Option<DataOffer<WlDataOffer>>,
|
||||
cached_primary_read: Option<String>,
|
||||
cached_primary_read: Option<ClipboardItem>,
|
||||
current_primary_offer: Option<DataOffer<ZwpPrimarySelectionOfferV1>>,
|
||||
}
|
||||
|
||||
@@ -89,12 +89,12 @@ impl Clipboard {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set(&mut self, text: String) {
|
||||
self.contents = Some(text);
|
||||
pub fn set(&mut self, item: ClipboardItem) {
|
||||
self.contents = Some(item);
|
||||
}
|
||||
|
||||
pub fn set_primary(&mut self, text: String) {
|
||||
self.primary_contents = Some(text);
|
||||
pub fn set_primary(&mut self, item: ClipboardItem) {
|
||||
self.primary_contents = Some(item);
|
||||
}
|
||||
|
||||
pub fn set_offer(&mut self, data_offer: Option<DataOffer<WlDataOffer>>) {
|
||||
@@ -113,17 +113,17 @@ impl Clipboard {
|
||||
|
||||
pub fn send(&self, _mime_type: String, fd: OwnedFd) {
|
||||
if let Some(contents) = &self.contents {
|
||||
self.send_internal(fd, contents.as_bytes().to_owned());
|
||||
self.send_internal(fd, contents.text.as_bytes().to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_primary(&self, _mime_type: String, fd: OwnedFd) {
|
||||
if let Some(primary_contents) = &self.primary_contents {
|
||||
self.send_internal(fd, primary_contents.as_bytes().to_owned());
|
||||
self.send_internal(fd, primary_contents.text.as_bytes().to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(&mut self) -> Option<String> {
|
||||
pub fn read(&mut self) -> Option<ClipboardItem> {
|
||||
let offer = self.current_offer.clone()?;
|
||||
if let Some(cached) = self.cached_read.clone() {
|
||||
return Some(cached);
|
||||
@@ -145,8 +145,8 @@ impl Clipboard {
|
||||
|
||||
match unsafe { read_fd(fd) } {
|
||||
Ok(v) => {
|
||||
self.cached_read = Some(v.clone());
|
||||
Some(v)
|
||||
self.cached_read = Some(ClipboardItem::new(v));
|
||||
self.cached_read.clone()
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("error reading clipboard pipe: {err:?}");
|
||||
@@ -155,7 +155,7 @@ impl Clipboard {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_primary(&mut self) -> Option<String> {
|
||||
pub fn read_primary(&mut self) -> Option<ClipboardItem> {
|
||||
let offer = self.current_primary_offer.clone()?;
|
||||
if let Some(cached) = self.cached_primary_read.clone() {
|
||||
return Some(cached);
|
||||
@@ -177,8 +177,8 @@ impl Clipboard {
|
||||
|
||||
match unsafe { read_fd(fd) } {
|
||||
Ok(v) => {
|
||||
self.cached_primary_read = Some(v.clone());
|
||||
Some(v)
|
||||
self.cached_primary_read = Some(ClipboardItem::new(v.clone()));
|
||||
self.cached_primary_read.clone()
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("error reading clipboard pipe: {err:?}");
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashSet;
|
||||
use std::ffi::OsString;
|
||||
use std::ops::Deref;
|
||||
use std::rc::{Rc, Weak};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use calloop::generic::{FdWrapper, Generic};
|
||||
use calloop::{EventLoop, LoopHandle, RegistrationToken};
|
||||
use anyhow::Context;
|
||||
|
||||
use async_task::Runnable;
|
||||
use calloop::channel::Channel;
|
||||
|
||||
use collections::HashMap;
|
||||
use util::ResultExt;
|
||||
|
||||
use futures::channel::oneshot;
|
||||
use util::ResultExt;
|
||||
use x11rb::connection::{Connection, RequestConnection};
|
||||
use x11rb::cursor;
|
||||
use x11rb::errors::ConnectionError;
|
||||
use x11rb::protocol::randr::ConnectionExt as _;
|
||||
use x11rb::protocol::xinput::ConnectionExt;
|
||||
use x11rb::protocol::xkb::ConnectionExt as _;
|
||||
use x11rb::protocol::xproto::{ChangeWindowAttributesAux, ConnectionExt as _};
|
||||
@@ -29,13 +30,13 @@ use xkbcommon::xkb as xkbc;
|
||||
use crate::platform::linux::LinuxClient;
|
||||
use crate::platform::{LinuxCommon, PlatformWindow};
|
||||
use crate::{
|
||||
modifiers_from_xinput_info, point, px, AnyWindowHandle, Bounds, CursorStyle, DisplayId,
|
||||
Keystroke, Modifiers, ModifiersChangedEvent, Pixels, PlatformDisplay, PlatformInput, Point,
|
||||
ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
|
||||
modifiers_from_xinput_info, point, px, AnyWindowHandle, Bounds, ClipboardItem, CursorStyle,
|
||||
DisplayId, Keystroke, Modifiers, ModifiersChangedEvent, Pixels, PlatformDisplay, PlatformInput,
|
||||
Point, QuitSignal, ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
|
||||
};
|
||||
|
||||
use super::{
|
||||
super::{open_uri_internal, SCROLL_LINES},
|
||||
super::{get_xkb_compose_state, open_uri_internal, SCROLL_LINES},
|
||||
X11Display, X11WindowStatePtr, XcbAtoms,
|
||||
};
|
||||
use super::{button_of_key, modifiers_from_state, pressed_button_from_mask};
|
||||
@@ -48,7 +49,6 @@ pub(super) const XINPUT_MASTER_DEVICE: u16 = 1;
|
||||
|
||||
pub(crate) struct WindowRef {
|
||||
window: X11WindowStatePtr,
|
||||
refresh_event_token: RegistrationToken,
|
||||
}
|
||||
|
||||
impl WindowRef {
|
||||
@@ -96,9 +96,6 @@ impl From<xim::ClientError> for EventHandlerError {
|
||||
}
|
||||
|
||||
pub struct X11ClientState {
|
||||
pub(crate) loop_handle: LoopHandle<'static, X11Client>,
|
||||
pub(crate) event_loop: Option<calloop::EventLoop<'static, X11Client>>,
|
||||
|
||||
pub(crate) last_click: Instant,
|
||||
pub(crate) last_location: Point<Pixels>,
|
||||
pub(crate) current_count: usize,
|
||||
@@ -116,7 +113,7 @@ pub struct X11ClientState {
|
||||
pub(crate) xim_handler: Option<XimHandler>,
|
||||
pub modifiers: Modifiers,
|
||||
|
||||
pub(crate) compose_state: xkbc::compose::State,
|
||||
pub(crate) compose_state: Option<xkbc::compose::State>,
|
||||
pub(crate) pre_edit_text: Option<String>,
|
||||
pub(crate) composing: bool,
|
||||
pub(crate) cursor_handle: cursor::Handle,
|
||||
@@ -129,6 +126,12 @@ pub struct X11ClientState {
|
||||
|
||||
pub(crate) common: LinuxCommon,
|
||||
pub(crate) clipboard: x11_clipboard::Clipboard,
|
||||
pub(crate) clipboard_item: Option<ClipboardItem>,
|
||||
|
||||
quit_signal_rx: oneshot::Receiver<()>,
|
||||
|
||||
runnables: Channel<Runnable>,
|
||||
xdp_event_source: XDPEventSource,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -139,14 +142,39 @@ impl X11ClientStatePtr {
|
||||
let client = X11Client(self.0.upgrade().expect("client already dropped"));
|
||||
let mut state = client.0.borrow_mut();
|
||||
|
||||
if let Some(window_ref) = state.windows.remove(&x_window) {
|
||||
state.loop_handle.remove(window_ref.refresh_event_token);
|
||||
if state.windows.remove(&x_window).is_none() {
|
||||
log::warn!(
|
||||
"failed to remove X window {} from client state, does not exist",
|
||||
x_window
|
||||
);
|
||||
}
|
||||
|
||||
state.cursor_styles.remove(&x_window);
|
||||
|
||||
if state.windows.is_empty() {
|
||||
state.common.signal.stop();
|
||||
state.common.signal.quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ChannelQuitSignal {
|
||||
tx: Option<oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
impl ChannelQuitSignal {
|
||||
fn new() -> (Self, oneshot::Receiver<()>) {
|
||||
let (tx, rx) = oneshot::channel::<()>();
|
||||
|
||||
let quit_signal = ChannelQuitSignal { tx: Some(tx) };
|
||||
|
||||
(quit_signal, rx)
|
||||
}
|
||||
}
|
||||
|
||||
impl QuitSignal for ChannelQuitSignal {
|
||||
fn quit(&mut self) {
|
||||
if let Some(tx) = self.tx.take() {
|
||||
tx.send(()).log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,27 +184,9 @@ pub(crate) struct X11Client(Rc<RefCell<X11ClientState>>);
|
||||
|
||||
impl X11Client {
|
||||
pub(crate) fn new() -> Self {
|
||||
let event_loop = EventLoop::try_new().unwrap();
|
||||
let (quit_signal, quit_signal_rx) = ChannelQuitSignal::new();
|
||||
|
||||
let (common, main_receiver) = LinuxCommon::new(event_loop.get_signal());
|
||||
|
||||
let handle = event_loop.handle();
|
||||
|
||||
handle
|
||||
.insert_source(main_receiver, {
|
||||
let handle = handle.clone();
|
||||
move |event, _, _: &mut X11Client| {
|
||||
if let calloop::channel::Event::Msg(runnable) = event {
|
||||
// Insert the runnables as idle callbacks, so we make sure that user-input and X11
|
||||
// events have higher priority and runnables are only worked off after the event
|
||||
// callbacks.
|
||||
handle.insert_idle(|_| {
|
||||
runnable.run();
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
let (common, runnables) = LinuxCommon::new(Box::new(quit_signal));
|
||||
|
||||
let (xcb_connection, x_root_index) = XCBConnection::connect(None).unwrap();
|
||||
xcb_connection
|
||||
@@ -249,18 +259,7 @@ impl X11Client {
|
||||
);
|
||||
xkbc::x11::state_new_from_device(&xkb_keymap, &xcb_connection, xkb_device_id)
|
||||
};
|
||||
let compose_state = {
|
||||
let locale = std::env::var_os("LC_CTYPE").unwrap_or(OsString::from("C"));
|
||||
let table = xkbc::compose::Table::new_from_locale(
|
||||
&xkb_context,
|
||||
&locale,
|
||||
xkbc::compose::COMPILE_NO_FLAGS,
|
||||
)
|
||||
.log_err()
|
||||
.unwrap();
|
||||
xkbc::compose::State::new(&table, xkbc::compose::STATE_NO_FLAGS)
|
||||
};
|
||||
|
||||
let compose_state = get_xkb_compose_state(&xkb_context);
|
||||
let resource_database = x11rb::resource_manager::new_from_default(&xcb_connection).unwrap();
|
||||
|
||||
let scale_factor = resource_database
|
||||
@@ -286,105 +285,16 @@ impl X11Client {
|
||||
None
|
||||
};
|
||||
|
||||
// Safety: Safe if xcb::Connection always returns a valid fd
|
||||
let fd = unsafe { FdWrapper::new(Rc::clone(&xcb_connection)) };
|
||||
|
||||
handle
|
||||
.insert_source(
|
||||
Generic::new_with_error::<EventHandlerError>(
|
||||
fd,
|
||||
calloop::Interest::READ,
|
||||
calloop::Mode::Level,
|
||||
),
|
||||
{
|
||||
let xcb_connection = xcb_connection.clone();
|
||||
move |_readiness, _, client| {
|
||||
let mut events = Vec::new();
|
||||
let mut windows_to_refresh = HashSet::new();
|
||||
|
||||
while let Some(event) = xcb_connection.poll_for_event()? {
|
||||
if let Event::Expose(event) = event {
|
||||
windows_to_refresh.insert(event.window);
|
||||
} else {
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
for window in windows_to_refresh.into_iter() {
|
||||
if let Some(window) = client.get_window(window) {
|
||||
window.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
for event in events.into_iter() {
|
||||
let mut state = client.0.borrow_mut();
|
||||
if state.ximc.is_none() || state.xim_handler.is_none() {
|
||||
drop(state);
|
||||
client.handle_event(event);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut ximc = state.ximc.take().unwrap();
|
||||
let mut xim_handler = state.xim_handler.take().unwrap();
|
||||
let xim_connected = xim_handler.connected;
|
||||
drop(state);
|
||||
|
||||
let xim_filtered = match ximc.filter_event(&event, &mut xim_handler) {
|
||||
Ok(handled) => handled,
|
||||
Err(err) => {
|
||||
log::error!("XIMClientError: {}", err);
|
||||
false
|
||||
}
|
||||
};
|
||||
let xim_callback_event = xim_handler.last_callback_event.take();
|
||||
|
||||
let mut state = client.0.borrow_mut();
|
||||
state.ximc = Some(ximc);
|
||||
state.xim_handler = Some(xim_handler);
|
||||
drop(state);
|
||||
|
||||
if let Some(event) = xim_callback_event {
|
||||
client.handle_xim_callback_event(event);
|
||||
}
|
||||
|
||||
if xim_filtered {
|
||||
continue;
|
||||
}
|
||||
|
||||
if xim_connected {
|
||||
client.xim_handle_event(event);
|
||||
} else {
|
||||
client.handle_event(event);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(calloop::PostAction::Continue)
|
||||
}
|
||||
},
|
||||
)
|
||||
.expect("Failed to initialize x11 event source");
|
||||
|
||||
handle
|
||||
.insert_source(XDPEventSource::new(&common.background_executor), {
|
||||
move |event, _, client| match event {
|
||||
XDPEvent::WindowAppearance(appearance) => {
|
||||
client.with_common(|common| common.appearance = appearance);
|
||||
for (_, window) in &mut client.0.borrow_mut().windows {
|
||||
window.window.set_appearance(appearance);
|
||||
}
|
||||
}
|
||||
XDPEvent::CursorTheme(_) | XDPEvent::CursorSize(_) => {
|
||||
// noop, X11 manages this for us.
|
||||
}
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
let xdp_event_source = XDPEventSource::new(&common.background_executor);
|
||||
|
||||
X11Client(Rc::new(RefCell::new(X11ClientState {
|
||||
modifiers: Modifiers::default(),
|
||||
event_loop: Some(event_loop),
|
||||
loop_handle: handle,
|
||||
runnables,
|
||||
xdp_event_source,
|
||||
quit_signal_rx,
|
||||
|
||||
common,
|
||||
|
||||
modifiers: Modifiers::default(),
|
||||
last_click: Instant::now(),
|
||||
last_location: Point::new(px(0.0), px(0.0)),
|
||||
current_count: 0,
|
||||
@@ -400,7 +310,7 @@ impl X11Client {
|
||||
ximc,
|
||||
xim_handler,
|
||||
|
||||
compose_state: compose_state,
|
||||
compose_state,
|
||||
pre_edit_text: None,
|
||||
composing: false,
|
||||
|
||||
@@ -413,6 +323,7 @@ impl X11Client {
|
||||
scroll_y: None,
|
||||
|
||||
clipboard,
|
||||
clipboard_item: None,
|
||||
})))
|
||||
}
|
||||
|
||||
@@ -478,6 +389,50 @@ impl X11Client {
|
||||
.map(|window_reference| window_reference.window.clone())
|
||||
}
|
||||
|
||||
fn handle_events(&self, events: Vec<Event>) {
|
||||
for event in events.into_iter() {
|
||||
let mut state = self.0.borrow_mut();
|
||||
if state.ximc.is_none() || state.xim_handler.is_none() {
|
||||
drop(state);
|
||||
self.handle_event(event);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut ximc = state.ximc.take().unwrap();
|
||||
let mut xim_handler = state.xim_handler.take().unwrap();
|
||||
let xim_connected = xim_handler.connected;
|
||||
drop(state);
|
||||
|
||||
let xim_filtered = match ximc.filter_event(&event, &mut xim_handler) {
|
||||
Ok(handled) => handled,
|
||||
Err(err) => {
|
||||
log::error!("XIMClientError: {}", err);
|
||||
false
|
||||
}
|
||||
};
|
||||
let xim_callback_event = xim_handler.last_callback_event.take();
|
||||
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.ximc = Some(ximc);
|
||||
state.xim_handler = Some(xim_handler);
|
||||
drop(state);
|
||||
|
||||
if let Some(event) = xim_callback_event {
|
||||
self.handle_xim_callback_event(event);
|
||||
}
|
||||
|
||||
if xim_filtered {
|
||||
continue;
|
||||
}
|
||||
|
||||
if xim_connected {
|
||||
self.xim_handle_event(event);
|
||||
} else {
|
||||
self.handle_event(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_event(&self, event: Event) -> Option<()> {
|
||||
match event {
|
||||
Event::ClientMessage(event) => {
|
||||
@@ -524,7 +479,9 @@ impl X11Client {
|
||||
window.set_focused(false);
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.focused_window = None;
|
||||
state.compose_state.reset();
|
||||
if let Some(compose_state) = state.compose_state.as_mut() {
|
||||
compose_state.reset();
|
||||
}
|
||||
state.pre_edit_text.take();
|
||||
drop(state);
|
||||
self.disable_ime();
|
||||
@@ -570,37 +527,42 @@ impl X11Client {
|
||||
if keysym.is_modifier_key() {
|
||||
return Some(());
|
||||
}
|
||||
state.compose_state.feed(keysym);
|
||||
match state.compose_state.status() {
|
||||
xkbc::Status::Composed => {
|
||||
state.pre_edit_text.take();
|
||||
keystroke.ime_key = state.compose_state.utf8();
|
||||
keystroke.key =
|
||||
xkbc::keysym_get_name(state.compose_state.keysym().unwrap());
|
||||
}
|
||||
xkbc::Status::Composing => {
|
||||
state.pre_edit_text = state
|
||||
.compose_state
|
||||
.utf8()
|
||||
.or(crate::Keystroke::underlying_dead_key(keysym));
|
||||
let pre_edit = state.pre_edit_text.clone().unwrap_or(String::default());
|
||||
drop(state);
|
||||
window.handle_ime_preedit(pre_edit);
|
||||
state = self.0.borrow_mut();
|
||||
}
|
||||
xkbc::Status::Cancelled => {
|
||||
let pre_edit = state.pre_edit_text.take();
|
||||
drop(state);
|
||||
if let Some(pre_edit) = pre_edit {
|
||||
window.handle_ime_commit(pre_edit);
|
||||
if let Some(mut compose_state) = state.compose_state.take() {
|
||||
compose_state.feed(keysym);
|
||||
match compose_state.status() {
|
||||
xkbc::Status::Composed => {
|
||||
state.pre_edit_text.take();
|
||||
keystroke.ime_key = compose_state.utf8();
|
||||
if let Some(keysym) = compose_state.keysym() {
|
||||
keystroke.key = xkbc::keysym_get_name(keysym);
|
||||
}
|
||||
}
|
||||
if let Some(current_key) = Keystroke::underlying_dead_key(keysym) {
|
||||
window.handle_ime_preedit(current_key);
|
||||
xkbc::Status::Composing => {
|
||||
keystroke.ime_key = None;
|
||||
state.pre_edit_text = compose_state
|
||||
.utf8()
|
||||
.or(crate::Keystroke::underlying_dead_key(keysym));
|
||||
let pre_edit =
|
||||
state.pre_edit_text.clone().unwrap_or(String::default());
|
||||
drop(state);
|
||||
window.handle_ime_preedit(pre_edit);
|
||||
state = self.0.borrow_mut();
|
||||
}
|
||||
state = self.0.borrow_mut();
|
||||
state.compose_state.feed(keysym);
|
||||
xkbc::Status::Cancelled => {
|
||||
let pre_edit = state.pre_edit_text.take();
|
||||
drop(state);
|
||||
if let Some(pre_edit) = pre_edit {
|
||||
window.handle_ime_commit(pre_edit);
|
||||
}
|
||||
if let Some(current_key) = Keystroke::underlying_dead_key(keysym) {
|
||||
window.handle_ime_preedit(current_key);
|
||||
}
|
||||
state = self.0.borrow_mut();
|
||||
compose_state.feed(keysym);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
_ => {}
|
||||
state.compose_state = Some(compose_state);
|
||||
}
|
||||
keystroke
|
||||
};
|
||||
@@ -649,7 +611,9 @@ impl X11Client {
|
||||
window.handle_ime_unmark();
|
||||
state = self.0.borrow_mut();
|
||||
} else if let Some(text) = state.pre_edit_text.take() {
|
||||
state.compose_state.reset();
|
||||
if let Some(compose_state) = state.compose_state.as_mut() {
|
||||
compose_state.reset();
|
||||
}
|
||||
drop(state);
|
||||
window.handle_ime_commit(text);
|
||||
state = self.0.borrow_mut();
|
||||
@@ -901,6 +865,39 @@ impl X11Client {
|
||||
drop(state);
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn send_window_expose_events(
|
||||
&self,
|
||||
x_windows: impl IntoIterator<Item = xproto::Window>,
|
||||
) -> anyhow::Result<()> {
|
||||
let state = self.0.borrow_mut();
|
||||
|
||||
for x_window in x_windows.into_iter() {
|
||||
state
|
||||
.xcb_connection
|
||||
.send_event(
|
||||
false,
|
||||
x_window,
|
||||
xproto::EventMask::EXPOSURE,
|
||||
xproto::ExposeEvent {
|
||||
response_type: xproto::EXPOSE_EVENT,
|
||||
sequence: 0,
|
||||
window: x_window,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
count: 1,
|
||||
},
|
||||
)
|
||||
.context("failed to send ExposeEvent for window")?;
|
||||
}
|
||||
|
||||
state
|
||||
.xcb_connection
|
||||
.flush()
|
||||
.context("failed to flush XCB connection after sending ExposeEvent")
|
||||
}
|
||||
}
|
||||
|
||||
impl LinuxClient for X11Client {
|
||||
@@ -973,69 +970,8 @@ impl LinuxClient for X11Client {
|
||||
state.common.appearance,
|
||||
)?;
|
||||
|
||||
let screen_resources = state
|
||||
.xcb_connection
|
||||
.randr_get_screen_resources(x_window)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.expect("Could not find available screens");
|
||||
|
||||
let mode = screen_resources
|
||||
.crtcs
|
||||
.iter()
|
||||
.find_map(|crtc| {
|
||||
let crtc_info = state
|
||||
.xcb_connection
|
||||
.randr_get_crtc_info(*crtc, x11rb::CURRENT_TIME)
|
||||
.ok()?
|
||||
.reply()
|
||||
.ok()?;
|
||||
|
||||
screen_resources
|
||||
.modes
|
||||
.iter()
|
||||
.find(|m| m.id == crtc_info.mode)
|
||||
})
|
||||
.expect("Unable to find screen refresh rate");
|
||||
|
||||
let refresh_event_token = state
|
||||
.loop_handle
|
||||
.insert_source(calloop::timer::Timer::immediate(), {
|
||||
let refresh_duration = mode_refresh_rate(mode);
|
||||
move |mut instant, (), client| {
|
||||
let state = client.0.borrow_mut();
|
||||
state
|
||||
.xcb_connection
|
||||
.send_event(
|
||||
false,
|
||||
x_window,
|
||||
xproto::EventMask::EXPOSURE,
|
||||
xproto::ExposeEvent {
|
||||
response_type: xproto::EXPOSE_EVENT,
|
||||
sequence: 0,
|
||||
window: x_window,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 0,
|
||||
height: 0,
|
||||
count: 1,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let _ = state.xcb_connection.flush().unwrap();
|
||||
// Take into account that some frames have been skipped
|
||||
let now = Instant::now();
|
||||
while instant < now {
|
||||
instant += refresh_duration;
|
||||
}
|
||||
calloop::timer::TimeoutAction::ToInstant(instant)
|
||||
}
|
||||
})
|
||||
.expect("Failed to initialize refresh timer");
|
||||
|
||||
let window_ref = WindowRef {
|
||||
window: window.0.clone(),
|
||||
refresh_event_token,
|
||||
};
|
||||
|
||||
state.windows.insert(x_window, window_ref);
|
||||
@@ -1097,7 +1033,7 @@ impl LinuxClient for X11Client {
|
||||
}
|
||||
|
||||
fn write_to_clipboard(&self, item: crate::ClipboardItem) {
|
||||
let state = self.0.borrow_mut();
|
||||
let mut state = self.0.borrow_mut();
|
||||
state
|
||||
.clipboard
|
||||
.store(
|
||||
@@ -1106,6 +1042,7 @@ impl LinuxClient for X11Client {
|
||||
item.text().as_bytes(),
|
||||
)
|
||||
.ok();
|
||||
state.clipboard_item.replace(item);
|
||||
}
|
||||
|
||||
fn read_from_primary(&self) -> Option<crate::ClipboardItem> {
|
||||
@@ -1127,6 +1064,20 @@ impl LinuxClient for X11Client {
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<crate::ClipboardItem> {
|
||||
let state = self.0.borrow_mut();
|
||||
// if the last copy was from this app, return our cached item
|
||||
// which has metadata attached.
|
||||
if state
|
||||
.clipboard
|
||||
.setter
|
||||
.connection
|
||||
.get_selection_owner(state.clipboard.setter.atoms.clipboard)
|
||||
.ok()
|
||||
.and_then(|r| r.reply().ok())
|
||||
.map(|reply| reply.owner == state.clipboard.setter.window)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return state.clipboard_item.clone();
|
||||
}
|
||||
state
|
||||
.clipboard
|
||||
.load(
|
||||
@@ -1143,14 +1094,108 @@ impl LinuxClient for X11Client {
|
||||
}
|
||||
|
||||
fn run(&self) {
|
||||
let mut event_loop = self
|
||||
.0
|
||||
.borrow_mut()
|
||||
.event_loop
|
||||
.take()
|
||||
.expect("App is already running");
|
||||
loop {
|
||||
{
|
||||
let mut state = self.0.borrow_mut();
|
||||
if let Ok(Some(())) = state.quit_signal_rx.try_recv() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
event_loop.run(None, &mut self.clone(), |_| {}).log_err();
|
||||
// Send expose events to windows that need refreshing
|
||||
let mut windows_to_expose = HashSet::new();
|
||||
{
|
||||
let state = self.0.borrow_mut();
|
||||
for (x_window, window_ref) in state.windows.iter() {
|
||||
if window_ref.window.needs_refresh() {
|
||||
windows_to_expose.insert(*x_window);
|
||||
window_ref.window.set_refresh_queued(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut sleep = windows_to_expose.is_empty();
|
||||
let _ = self.send_window_expose_events(windows_to_expose).log_err();
|
||||
|
||||
// Read all X11 events and then handle them in a batch
|
||||
{
|
||||
let mut events = Vec::new();
|
||||
let mut windows_to_refresh = HashSet::new();
|
||||
|
||||
{
|
||||
let state = self.0.borrow_mut();
|
||||
while let Ok(Some(event)) = state.xcb_connection.poll_for_event() {
|
||||
if let Event::Expose(event) = event {
|
||||
windows_to_refresh.insert(event.window);
|
||||
} else {
|
||||
events.push(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sleep = !sleep && events.is_empty() && windows_to_refresh.is_empty();
|
||||
// We prioritize Expose events so that a lot of input events don't hold up
|
||||
// a render.
|
||||
for window in windows_to_refresh.into_iter() {
|
||||
if let Some(window) = self.get_window(window) {
|
||||
window.refresh();
|
||||
window.set_refresh_queued(false);
|
||||
}
|
||||
}
|
||||
|
||||
self.handle_events(events);
|
||||
}
|
||||
|
||||
// Handle runnables
|
||||
{
|
||||
let mut state = self.0.borrow_mut();
|
||||
|
||||
let now = Instant::now();
|
||||
while let Ok(runnable) = state.runnables.try_recv() {
|
||||
drop(state);
|
||||
|
||||
runnable.run();
|
||||
|
||||
sleep = false;
|
||||
|
||||
if now.elapsed() >= Duration::from_millis(2) {
|
||||
println!("ran runnables for over 2ms");
|
||||
break;
|
||||
}
|
||||
|
||||
state = self.0.borrow_mut();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle XDG events
|
||||
{
|
||||
let mut state = self.0.borrow_mut();
|
||||
while let Ok(event) = state.xdp_event_source.try_recv() {
|
||||
drop(state);
|
||||
|
||||
sleep = false;
|
||||
|
||||
match event {
|
||||
XDPEvent::WindowAppearance(appearance) => {
|
||||
self.with_common(|common| common.appearance = appearance);
|
||||
for (_, window) in &mut self.0.borrow_mut().windows {
|
||||
window.window.set_appearance(appearance);
|
||||
}
|
||||
}
|
||||
XDPEvent::CursorTheme(_) | XDPEvent::CursorSize(_) => {
|
||||
// noop, X11 manages this for us.
|
||||
}
|
||||
};
|
||||
|
||||
state = self.0.borrow_mut();
|
||||
}
|
||||
}
|
||||
|
||||
// Sleep for a very short duration to prevent busy-waiting
|
||||
// But only if we had nothing to do in this iteration.
|
||||
if sleep {
|
||||
std::thread::sleep(Duration::from_millis(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn active_window(&self) -> Option<AnyWindowHandle> {
|
||||
@@ -1164,15 +1209,6 @@ impl LinuxClient for X11Client {
|
||||
}
|
||||
}
|
||||
|
||||
// Adatpted from:
|
||||
// https://docs.rs/winit/0.29.11/src/winit/platform_impl/linux/x11/monitor.rs.html#103-111
|
||||
pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration {
|
||||
let millihertz = mode.dot_clock as u64 * 1_000 / (mode.htotal as u64 * mode.vtotal as u64);
|
||||
let micros = 1_000_000_000 / millihertz;
|
||||
log::info!("Refreshing at {} micros", micros);
|
||||
Duration::from_micros(micros)
|
||||
}
|
||||
|
||||
fn fp3232_to_f32(value: xinput::Fp3232) -> f32 {
|
||||
value.integral as f32 + value.frac as f32 / u32::MAX as f32
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ use util::{maybe, ResultExt};
|
||||
use x11rb::{
|
||||
connection::Connection,
|
||||
protocol::{
|
||||
randr::{self, ConnectionExt as _},
|
||||
xinput::{self, ConnectionExt as _},
|
||||
xproto::{
|
||||
self, ClientMessageEvent, ConnectionExt as _, EventMask, TranslateCoordinatesReply,
|
||||
@@ -31,10 +32,10 @@ use std::{
|
||||
ptr::NonNull,
|
||||
rc::Rc,
|
||||
sync::{self, Arc},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use super::{X11Display, XINPUT_MASTER_DEVICE};
|
||||
|
||||
x11rb::atom_manager! {
|
||||
pub XcbAtoms: AtomsCookie {
|
||||
UTF8_STRING,
|
||||
@@ -159,6 +160,9 @@ pub struct Callbacks {
|
||||
|
||||
pub struct X11WindowState {
|
||||
pub destroyed: bool,
|
||||
pub refresh_rate: Duration,
|
||||
refresh_queued: bool,
|
||||
pub last_refresh_at: Option<Instant>,
|
||||
client: X11ClientStatePtr,
|
||||
executor: ForegroundExecutor,
|
||||
atoms: XcbAtoms,
|
||||
@@ -178,7 +182,7 @@ pub(crate) struct X11WindowStatePtr {
|
||||
pub state: Rc<RefCell<X11WindowState>>,
|
||||
pub(crate) callbacks: Rc<RefCell<Callbacks>>,
|
||||
xcb_connection: Rc<XCBConnection>,
|
||||
x_window: xproto::Window,
|
||||
pub x_window: xproto::Window,
|
||||
}
|
||||
|
||||
impl rwh::HasWindowHandle for RawWindow {
|
||||
@@ -397,6 +401,31 @@ impl X11WindowState {
|
||||
};
|
||||
xcb_connection.map_window(x_window).unwrap();
|
||||
|
||||
let screen_resources = xcb_connection
|
||||
.randr_get_screen_resources(x_window)
|
||||
.unwrap()
|
||||
.reply()
|
||||
.expect("Could not find available screens");
|
||||
|
||||
let mode = screen_resources
|
||||
.crtcs
|
||||
.iter()
|
||||
.find_map(|crtc| {
|
||||
let crtc_info = xcb_connection
|
||||
.randr_get_crtc_info(*crtc, x11rb::CURRENT_TIME)
|
||||
.ok()?
|
||||
.reply()
|
||||
.ok()?;
|
||||
|
||||
screen_resources
|
||||
.modes
|
||||
.iter()
|
||||
.find(|m| m.id == crtc_info.mode)
|
||||
})
|
||||
.expect("Unable to find screen refresh rate");
|
||||
|
||||
let refresh_rate = mode_refresh_rate(&mode);
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
executor,
|
||||
@@ -413,6 +442,9 @@ impl X11WindowState {
|
||||
appearance,
|
||||
handle,
|
||||
destroyed: false,
|
||||
refresh_rate,
|
||||
refresh_queued: false,
|
||||
last_refresh_at: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -582,6 +614,10 @@ impl X11WindowStatePtr {
|
||||
let mut cb = self.callbacks.borrow_mut();
|
||||
if let Some(ref mut fun) = cb.request_frame {
|
||||
fun();
|
||||
self.state
|
||||
.borrow_mut()
|
||||
.last_refresh_at
|
||||
.replace(Instant::now());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -715,6 +751,23 @@ impl X11WindowStatePtr {
|
||||
(fun)()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn needs_refresh(&self) -> bool {
|
||||
let state = self.state.borrow();
|
||||
|
||||
if state.refresh_queued {
|
||||
return false;
|
||||
}
|
||||
|
||||
let refresh_rate = state.refresh_rate;
|
||||
state.last_refresh_at.map_or(false, |last_refresh_at| {
|
||||
last_refresh_at.elapsed() >= refresh_rate
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_refresh_queued(&self, value: bool) {
|
||||
self.state.borrow_mut().refresh_queued = value;
|
||||
}
|
||||
}
|
||||
|
||||
impl PlatformWindow for X11Window {
|
||||
@@ -1028,3 +1081,16 @@ impl PlatformWindow for X11Window {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Adapted from:
|
||||
// https://docs.rs/winit/0.29.11/src/winit/platform_impl/linux/x11/monitor.rs.html#103-111
|
||||
pub fn mode_refresh_rate(mode: &randr::ModeInfo) -> Duration {
|
||||
if mode.dot_clock == 0 || mode.htotal == 0 || mode.vtotal == 0 {
|
||||
return Duration::from_millis(16);
|
||||
}
|
||||
|
||||
let millihertz = mode.dot_clock as u64 * 1_000 / (mode.htotal as u64 * mode.vtotal as u64);
|
||||
let micros = 1_000_000_000 / millihertz;
|
||||
log::info!("Refreshing at {} micros", micros);
|
||||
Duration::from_micros(micros)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//!
|
||||
//! This module uses the [ashpd] crate
|
||||
|
||||
use anyhow::anyhow;
|
||||
use ashpd::desktop::settings::{ColorScheme, Settings};
|
||||
use calloop::channel::Channel;
|
||||
use calloop::{EventSource, Poll, PostAction, Readiness, Token, TokenFactory};
|
||||
@@ -98,6 +99,12 @@ impl XDPEventSource {
|
||||
|
||||
Self { channel }
|
||||
}
|
||||
|
||||
pub fn try_recv(&self) -> anyhow::Result<Event> {
|
||||
self.channel
|
||||
.try_recv()
|
||||
.map_err(|error| anyhow!("{}", error))
|
||||
}
|
||||
}
|
||||
|
||||
impl EventSource for XDPEventSource {
|
||||
|
||||
@@ -647,10 +647,12 @@ impl MacWindow {
|
||||
|
||||
native_window.setMovable_(is_movable as BOOL);
|
||||
|
||||
native_window.setContentMinSize_(NSSize {
|
||||
width: window_min_size.width.to_f64(),
|
||||
height: window_min_size.height.to_f64(),
|
||||
});
|
||||
if let Some(window_min_size) = window_min_size {
|
||||
native_window.setContentMinSize_(NSSize {
|
||||
width: window_min_size.width.to_f64(),
|
||||
height: window_min_size.height.to_f64(),
|
||||
});
|
||||
}
|
||||
|
||||
if titlebar.map_or(true, |titlebar| titlebar.appears_transparent) {
|
||||
native_window.setTitlebarAppearsTransparent_(YES);
|
||||
@@ -1670,9 +1672,13 @@ extern "C" fn first_rect_for_character_range(
|
||||
range: NSRange,
|
||||
_: id,
|
||||
) -> NSRect {
|
||||
let frame = unsafe {
|
||||
let window = get_window_state(this).lock().native_window;
|
||||
NSView::frame(window)
|
||||
let frame: NSRect = unsafe {
|
||||
let state = get_window_state(this);
|
||||
let lock = state.lock();
|
||||
let mut frame = NSWindow::frame(lock.native_window);
|
||||
let content_layout_rect: CGRect = msg_send![lock.native_window, contentLayoutRect];
|
||||
frame.origin.y -= frame.size.height - content_layout_rect.size.height;
|
||||
frame
|
||||
};
|
||||
with_input_handler(this, |input_handler| {
|
||||
input_handler.bounds_for_range(range.to_range()?)
|
||||
|
||||
@@ -58,7 +58,7 @@ impl<'de> serde::Deserialize<'de> for FontFeatures {
|
||||
while let Some((key, value)) =
|
||||
access.next_entry::<String, Option<FeatureValue>>()?
|
||||
{
|
||||
if key.len() != 4 && !key.is_ascii() {
|
||||
if !is_valid_feature_tag(&key) {
|
||||
log::error!("Incorrect font feature tag: {}", key);
|
||||
continue;
|
||||
}
|
||||
@@ -142,3 +142,7 @@ impl schemars::JsonSchema for FontFeatures {
|
||||
schema.into()
|
||||
}
|
||||
}
|
||||
|
||||
fn is_valid_feature_tag(tag: &str) -> bool {
|
||||
tag.len() == 4 && tag.chars().all(|c| c.is_ascii_alphanumeric())
|
||||
}
|
||||
|
||||
@@ -549,6 +549,7 @@ pub struct Window {
|
||||
pub(crate) focus: Option<FocusId>,
|
||||
focus_enabled: bool,
|
||||
pending_input: Option<PendingInput>,
|
||||
pending_modifiers: Option<Modifiers>,
|
||||
pending_input_observers: SubscriberSet<(), AnyObserver>,
|
||||
prompt: Option<RenderablePromptHandle>,
|
||||
}
|
||||
@@ -754,11 +755,6 @@ impl Window {
|
||||
handle
|
||||
.update(&mut cx, |_, cx| {
|
||||
cx.window.active.set(active);
|
||||
// If the window is occluded we may not render it again
|
||||
// until
|
||||
if !active {
|
||||
cx.window.rendered_frame.window_active = false;
|
||||
}
|
||||
cx.window
|
||||
.activation_observers
|
||||
.clone()
|
||||
@@ -828,6 +824,7 @@ impl Window {
|
||||
focus: None,
|
||||
focus_enabled: true,
|
||||
pending_input: None,
|
||||
pending_modifiers: None,
|
||||
pending_input_observers: SubscriberSet::new(),
|
||||
prompt: None,
|
||||
})
|
||||
@@ -3166,70 +3163,129 @@ impl<'a> WindowContext<'a> {
|
||||
.dispatch_tree
|
||||
.dispatch_path(node_id);
|
||||
|
||||
let mut bindings: SmallVec<[KeyBinding; 1]> = SmallVec::new();
|
||||
let mut pending = false;
|
||||
let mut keystroke: Option<Keystroke> = None;
|
||||
|
||||
if let Some(event) = event.downcast_ref::<ModifiersChangedEvent>() {
|
||||
if let Some(previous) = self.window.pending_modifiers.take() {
|
||||
if event.modifiers.number_of_modifiers() == 0 {
|
||||
let key = match previous {
|
||||
modifiers if modifiers.shift => Some("shift"),
|
||||
modifiers if modifiers.control => Some("control"),
|
||||
modifiers if modifiers.alt => Some("alt"),
|
||||
modifiers if modifiers.platform => Some("platform"),
|
||||
modifiers if modifiers.function => Some("function"),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(key) = key {
|
||||
let key = Keystroke {
|
||||
key: key.to_string(),
|
||||
ime_key: None,
|
||||
modifiers: Modifiers::default(),
|
||||
};
|
||||
let KeymatchResult {
|
||||
bindings: modifier_bindings,
|
||||
pending: pending_bindings,
|
||||
} = self
|
||||
.window
|
||||
.rendered_frame
|
||||
.dispatch_tree
|
||||
.dispatch_key(&key, &dispatch_path);
|
||||
|
||||
keystroke = Some(key);
|
||||
bindings = modifier_bindings;
|
||||
pending = pending_bindings;
|
||||
}
|
||||
}
|
||||
} else if event.modifiers.number_of_modifiers() == 1 {
|
||||
self.window.pending_modifiers = Some(event.modifiers);
|
||||
}
|
||||
if keystroke.is_none() {
|
||||
self.finish_dispatch_key_event(event, dispatch_path);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(key_down_event) = event.downcast_ref::<KeyDownEvent>() {
|
||||
let KeymatchResult { bindings, pending } = self
|
||||
self.window.pending_modifiers.take();
|
||||
let KeymatchResult {
|
||||
bindings: key_down_bindings,
|
||||
pending: key_down_pending,
|
||||
} = self
|
||||
.window
|
||||
.rendered_frame
|
||||
.dispatch_tree
|
||||
.dispatch_key(&key_down_event.keystroke, &dispatch_path);
|
||||
|
||||
if pending {
|
||||
let mut currently_pending = self.window.pending_input.take().unwrap_or_default();
|
||||
if currently_pending.focus.is_some() && currently_pending.focus != self.window.focus
|
||||
{
|
||||
currently_pending = PendingInput::default();
|
||||
}
|
||||
currently_pending.focus = self.window.focus;
|
||||
currently_pending
|
||||
.keystrokes
|
||||
.push(key_down_event.keystroke.clone());
|
||||
for binding in bindings {
|
||||
currently_pending.bindings.push(binding);
|
||||
}
|
||||
keystroke = Some(key_down_event.keystroke.clone());
|
||||
|
||||
currently_pending.timer = Some(self.spawn(|mut cx| async move {
|
||||
cx.background_executor.timer(Duration::from_secs(1)).await;
|
||||
cx.update(move |cx| {
|
||||
cx.clear_pending_keystrokes();
|
||||
let Some(currently_pending) = cx.window.pending_input.take() else {
|
||||
return;
|
||||
};
|
||||
cx.pending_input_changed();
|
||||
cx.replay_pending_input(currently_pending);
|
||||
})
|
||||
.log_err();
|
||||
}));
|
||||
bindings = key_down_bindings;
|
||||
pending = key_down_pending;
|
||||
}
|
||||
|
||||
self.window.pending_input = Some(currently_pending);
|
||||
self.pending_input_changed();
|
||||
|
||||
self.propagate_event = false;
|
||||
|
||||
return;
|
||||
} else if let Some(currently_pending) = self.window.pending_input.take() {
|
||||
self.pending_input_changed();
|
||||
if bindings
|
||||
.iter()
|
||||
.all(|binding| !currently_pending.used_by_binding(binding))
|
||||
{
|
||||
self.replay_pending_input(currently_pending)
|
||||
}
|
||||
if pending {
|
||||
let mut currently_pending = self.window.pending_input.take().unwrap_or_default();
|
||||
if currently_pending.focus.is_some() && currently_pending.focus != self.window.focus {
|
||||
currently_pending = PendingInput::default();
|
||||
}
|
||||
|
||||
if !bindings.is_empty() {
|
||||
self.clear_pending_keystrokes();
|
||||
currently_pending.focus = self.window.focus;
|
||||
if let Some(keystroke) = keystroke {
|
||||
currently_pending.keystrokes.push(keystroke.clone());
|
||||
}
|
||||
|
||||
self.propagate_event = true;
|
||||
for binding in bindings {
|
||||
self.dispatch_action_on_node(node_id, binding.action.as_ref());
|
||||
if !self.propagate_event {
|
||||
self.dispatch_keystroke_observers(event, Some(binding.action));
|
||||
return;
|
||||
}
|
||||
currently_pending.bindings.push(binding);
|
||||
}
|
||||
|
||||
currently_pending.timer = Some(self.spawn(|mut cx| async move {
|
||||
cx.background_executor.timer(Duration::from_secs(1)).await;
|
||||
cx.update(move |cx| {
|
||||
cx.clear_pending_keystrokes();
|
||||
let Some(currently_pending) = cx.window.pending_input.take() else {
|
||||
return;
|
||||
};
|
||||
cx.replay_pending_input(currently_pending);
|
||||
cx.pending_input_changed();
|
||||
})
|
||||
.log_err();
|
||||
}));
|
||||
|
||||
self.window.pending_input = Some(currently_pending);
|
||||
self.pending_input_changed();
|
||||
|
||||
self.propagate_event = false;
|
||||
return;
|
||||
} else if let Some(currently_pending) = self.window.pending_input.take() {
|
||||
self.pending_input_changed();
|
||||
if bindings
|
||||
.iter()
|
||||
.all(|binding| !currently_pending.used_by_binding(binding))
|
||||
{
|
||||
self.replay_pending_input(currently_pending)
|
||||
}
|
||||
}
|
||||
|
||||
if !bindings.is_empty() {
|
||||
self.clear_pending_keystrokes();
|
||||
}
|
||||
|
||||
self.propagate_event = true;
|
||||
for binding in bindings {
|
||||
self.dispatch_action_on_node(node_id, binding.action.as_ref());
|
||||
if !self.propagate_event {
|
||||
self.dispatch_keystroke_observers(event, Some(binding.action));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.finish_dispatch_key_event(event, dispatch_path)
|
||||
}
|
||||
|
||||
fn finish_dispatch_key_event(
|
||||
&mut self,
|
||||
event: &dyn Any,
|
||||
dispatch_path: SmallVec<[DispatchNodeId; 32]>,
|
||||
) {
|
||||
self.dispatch_key_down_up_event(event, &dispatch_path);
|
||||
if !self.propagate_event {
|
||||
return;
|
||||
|
||||
@@ -511,7 +511,7 @@ impl LanguageRegistry {
|
||||
) -> impl Future<Output = Result<Arc<Language>>> {
|
||||
let filename = path.file_name().and_then(|name| name.to_str());
|
||||
let extension = path.extension_or_hidden_file_name();
|
||||
let path_suffixes = [extension, filename];
|
||||
let path_suffixes = [extension, filename, path.to_str()];
|
||||
let empty = GlobSet::empty();
|
||||
|
||||
let rx = self.get_or_load_language(move |language_name, config| {
|
||||
|
||||
@@ -662,6 +662,17 @@ impl settings::Settings for AllLanguageSettings {
|
||||
.ok_or_else(Self::missing_default)?;
|
||||
|
||||
let mut file_types: HashMap<Arc<str>, GlobSet> = HashMap::default();
|
||||
|
||||
for (language, suffixes) in &default_value.file_types {
|
||||
let mut builder = GlobSetBuilder::new();
|
||||
|
||||
for suffix in suffixes {
|
||||
builder.add(Glob::new(suffix)?);
|
||||
}
|
||||
|
||||
file_types.insert(language.clone(), builder.build()?);
|
||||
}
|
||||
|
||||
for user_settings in sources.customizations() {
|
||||
if let Some(copilot) = user_settings.features.as_ref().and_then(|f| f.copilot) {
|
||||
copilot_enabled = Some(copilot);
|
||||
@@ -701,6 +712,15 @@ impl settings::Settings for AllLanguageSettings {
|
||||
for (language, suffixes) in &user_settings.file_types {
|
||||
let mut builder = GlobSetBuilder::new();
|
||||
|
||||
let default_value = default_value.file_types.get(&language.clone());
|
||||
|
||||
// Merge the default value with the user's value.
|
||||
if let Some(suffixes) = default_value {
|
||||
for suffix in suffixes {
|
||||
builder.add(Glob::new(suffix)?);
|
||||
}
|
||||
}
|
||||
|
||||
for suffix in suffixes {
|
||||
builder.add(Glob::new(suffix)?);
|
||||
}
|
||||
|
||||
@@ -213,7 +213,12 @@ impl LspAdapter for JsonLspAdapter {
|
||||
}
|
||||
|
||||
fn language_ids(&self) -> HashMap<String, String> {
|
||||
[("JSON".into(), "jsonc".into())].into_iter().collect()
|
||||
[
|
||||
("JSON".into(), "json".into()),
|
||||
("JSONC".into(), "jsonc".into()),
|
||||
]
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,7 +353,7 @@ impl LspAdapter for NodeVersionAdapter {
|
||||
}
|
||||
|
||||
Ok(LanguageServerBinary {
|
||||
path: destination_path.join("package-version-server"),
|
||||
path: destination_path,
|
||||
env: None,
|
||||
arguments: Default::default(),
|
||||
})
|
||||
|
||||
3
crates/languages/src/jsonc/brackets.scm
Normal file
3
crates/languages/src/jsonc/brackets.scm
Normal file
@@ -0,0 +1,3 @@
|
||||
("[" @open "]" @close)
|
||||
("{" @open "}" @close)
|
||||
("\"" @open "\"" @close)
|
||||
12
crates/languages/src/jsonc/config.toml
Normal file
12
crates/languages/src/jsonc/config.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
name = "JSONC"
|
||||
grammar = "jsonc"
|
||||
path_suffixes = ["jsonc"]
|
||||
line_comments = ["// "]
|
||||
autoclose_before = ",]}"
|
||||
brackets = [
|
||||
{ start = "{", end = "}", close = true, newline = true },
|
||||
{ start = "[", end = "]", close = true, newline = true },
|
||||
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
|
||||
]
|
||||
tab_size = 2
|
||||
prettier_parser_name = "jsonc"
|
||||
14
crates/languages/src/jsonc/embedding.scm
Normal file
14
crates/languages/src/jsonc/embedding.scm
Normal file
@@ -0,0 +1,14 @@
|
||||
; Only produce one embedding for the entire file.
|
||||
(document) @item
|
||||
|
||||
; Collapse arrays, except for the first object.
|
||||
(array
|
||||
"[" @keep
|
||||
.
|
||||
(object)? @keep
|
||||
"]" @keep) @collapse
|
||||
|
||||
; Collapse string values (but not keys).
|
||||
(pair value: (string
|
||||
"\"" @keep
|
||||
"\"" @keep) @collapse)
|
||||
21
crates/languages/src/jsonc/highlights.scm
Normal file
21
crates/languages/src/jsonc/highlights.scm
Normal file
@@ -0,0 +1,21 @@
|
||||
(comment) @comment
|
||||
|
||||
(string) @string
|
||||
|
||||
(pair
|
||||
key: (string) @property.json_key)
|
||||
|
||||
(number) @number
|
||||
|
||||
[
|
||||
(true)
|
||||
(false)
|
||||
(null)
|
||||
] @constant
|
||||
|
||||
[
|
||||
"{"
|
||||
"}"
|
||||
"["
|
||||
"]"
|
||||
] @punctuation.bracket
|
||||
2
crates/languages/src/jsonc/indents.scm
Normal file
2
crates/languages/src/jsonc/indents.scm
Normal file
@@ -0,0 +1,2 @@
|
||||
(array "]" @end) @indent
|
||||
(object "}" @end) @indent
|
||||
2
crates/languages/src/jsonc/outline.scm
Normal file
2
crates/languages/src/jsonc/outline.scm
Normal file
@@ -0,0 +1,2 @@
|
||||
(pair
|
||||
key: (string (string_content) @name)) @item
|
||||
1
crates/languages/src/jsonc/overrides.scm
Normal file
1
crates/languages/src/jsonc/overrides.scm
Normal file
@@ -0,0 +1 @@
|
||||
(string) @string
|
||||
4
crates/languages/src/jsonc/redactions.scm
Normal file
4
crates/languages/src/jsonc/redactions.scm
Normal file
@@ -0,0 +1,4 @@
|
||||
(pair value: (number) @redact)
|
||||
(pair value: (string) @redact)
|
||||
(array (number) @redact)
|
||||
(array (string) @redact)
|
||||
@@ -45,6 +45,7 @@ pub fn init(
|
||||
("gowork", tree_sitter_gowork::language()),
|
||||
("jsdoc", tree_sitter_jsdoc::language()),
|
||||
("json", tree_sitter_json::language()),
|
||||
("jsonc", tree_sitter_json::language()),
|
||||
("markdown", tree_sitter_markdown::language()),
|
||||
("proto", tree_sitter_proto::language()),
|
||||
("python", tree_sitter_python::language()),
|
||||
@@ -126,6 +127,14 @@ pub fn init(
|
||||
],
|
||||
json_task_context()
|
||||
);
|
||||
language!(
|
||||
"jsonc",
|
||||
vec![Arc::new(json::JsonLspAdapter::new(
|
||||
node_runtime.clone(),
|
||||
languages.clone(),
|
||||
))],
|
||||
json_task_context()
|
||||
);
|
||||
language!("markdown");
|
||||
language!(
|
||||
"python",
|
||||
|
||||
@@ -200,6 +200,7 @@ impl LspAdapter for TypeScriptLspAdapter {
|
||||
) -> Result<Option<serde_json::Value>> {
|
||||
Ok(Some(json!({
|
||||
"provideFormatter": true,
|
||||
"hostInfo": "zed",
|
||||
"tsserver": {
|
||||
"path": "node_modules/typescript/lib",
|
||||
},
|
||||
|
||||
@@ -162,47 +162,42 @@ impl LspAdapter for VtslsLspAdapter {
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
) -> Result<Option<serde_json::Value>> {
|
||||
Ok(Some(json!({
|
||||
"typescript":
|
||||
{
|
||||
"typescript": {
|
||||
"tsdk": "node_modules/typescript/lib",
|
||||
"format": {
|
||||
"enable": true
|
||||
},
|
||||
"inlayHints":{
|
||||
"parameterNames":
|
||||
{
|
||||
"inlayHints": {
|
||||
"parameterNames": {
|
||||
"enabled": "all",
|
||||
"suppressWhenArgumentMatchesName": false,
|
||||
|
||||
},
|
||||
|
||||
"parameterTypes":
|
||||
{
|
||||
"parameterTypes": {
|
||||
"enabled": true
|
||||
},
|
||||
"variableTypes": {
|
||||
"enabled": true,
|
||||
"suppressWhenTypeMatchesName": false,
|
||||
},
|
||||
"propertyDeclarationTypes":{
|
||||
"propertyDeclarationTypes": {
|
||||
"enabled": true,
|
||||
},
|
||||
"functionLikeReturnTypes": {
|
||||
"enabled": true,
|
||||
},
|
||||
"enumMemberValues":{
|
||||
"enumMemberValues": {
|
||||
"enabled": true,
|
||||
}
|
||||
}
|
||||
},
|
||||
"vtsls":
|
||||
{"experimental": {
|
||||
"completion": {
|
||||
"enableServerSideFuzzyMatch": true,
|
||||
"entriesLimit": 5000,
|
||||
"vtsls": {
|
||||
"experimental": {
|
||||
"completion": {
|
||||
"enableServerSideFuzzyMatch": true,
|
||||
"entriesLimit": 5000,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
@@ -220,40 +215,36 @@ impl LspAdapter for VtslsLspAdapter {
|
||||
"format": {
|
||||
"enable": true
|
||||
},
|
||||
"inlayHints":{
|
||||
"parameterNames":
|
||||
{
|
||||
"inlayHints": {
|
||||
"parameterNames": {
|
||||
"enabled": "all",
|
||||
"suppressWhenArgumentMatchesName": false,
|
||||
|
||||
},
|
||||
|
||||
"parameterTypes":
|
||||
{
|
||||
"parameterTypes": {
|
||||
"enabled": true
|
||||
},
|
||||
"variableTypes": {
|
||||
"enabled": true,
|
||||
"suppressWhenTypeMatchesName": false,
|
||||
},
|
||||
"propertyDeclarationTypes":{
|
||||
"propertyDeclarationTypes": {
|
||||
"enabled": true,
|
||||
},
|
||||
"functionLikeReturnTypes": {
|
||||
"enabled": true,
|
||||
},
|
||||
"enumMemberValues":{
|
||||
"enumMemberValues": {
|
||||
"enabled": true,
|
||||
}
|
||||
}
|
||||
},
|
||||
"vtsls":
|
||||
{"experimental": {
|
||||
"completion": {
|
||||
"enableServerSideFuzzyMatch": true,
|
||||
"entriesLimit": 5000,
|
||||
}
|
||||
}
|
||||
},
|
||||
"vtsls": {
|
||||
"experimental": {
|
||||
"completion": {
|
||||
"enableServerSideFuzzyMatch": true,
|
||||
"entriesLimit": 5000,
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -55,6 +55,8 @@ pub enum Model {
|
||||
#[serde(rename = "gpt-4o", alias = "gpt-4o-2024-05-13")]
|
||||
#[default]
|
||||
FourOmni,
|
||||
#[serde(rename = "custom")]
|
||||
Custom { name: String, max_tokens: usize },
|
||||
}
|
||||
|
||||
impl Model {
|
||||
@@ -74,15 +76,17 @@ impl Model {
|
||||
Self::Four => "gpt-4",
|
||||
Self::FourTurbo => "gpt-4-turbo-preview",
|
||||
Self::FourOmni => "gpt-4o",
|
||||
Self::Custom { .. } => "custom",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display_name(&self) -> &'static str {
|
||||
pub fn display_name(&self) -> &str {
|
||||
match self {
|
||||
Self::ThreePointFiveTurbo => "gpt-3.5-turbo",
|
||||
Self::Four => "gpt-4",
|
||||
Self::FourTurbo => "gpt-4-turbo",
|
||||
Self::FourOmni => "gpt-4o",
|
||||
Self::Custom { name, .. } => name,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,12 +96,24 @@ impl Model {
|
||||
Model::Four => 8192,
|
||||
Model::FourTurbo => 128000,
|
||||
Model::FourOmni => 128000,
|
||||
Model::Custom { max_tokens, .. } => *max_tokens,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_model<S>(model: &Model, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match model {
|
||||
Model::Custom { name, .. } => serializer.serialize_str(name),
|
||||
_ => serializer.serialize_str(model.id()),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Request {
|
||||
#[serde(serialize_with = "serialize_model")]
|
||||
pub model: Model,
|
||||
pub messages: Vec<RequestMessage>,
|
||||
pub stream: bool,
|
||||
|
||||
@@ -8192,7 +8192,7 @@ impl Project {
|
||||
}
|
||||
};
|
||||
|
||||
if abs_path.ends_with(local_settings_file_relative_path()) {
|
||||
if path.ends_with(local_settings_file_relative_path()) {
|
||||
let settings_dir = Arc::from(
|
||||
path.ancestors()
|
||||
.nth(local_settings_file_relative_path().components().count())
|
||||
@@ -8209,7 +8209,7 @@ impl Project {
|
||||
},
|
||||
)
|
||||
});
|
||||
} else if abs_path.ends_with(local_tasks_file_relative_path()) {
|
||||
} else if path.ends_with(local_tasks_file_relative_path()) {
|
||||
self.task_inventory().update(cx, |task_inventory, cx| {
|
||||
if removed {
|
||||
task_inventory.remove_local_static_source(&abs_path);
|
||||
@@ -8229,7 +8229,7 @@ impl Project {
|
||||
);
|
||||
}
|
||||
})
|
||||
} else if abs_path.ends_with(local_vscode_tasks_file_relative_path()) {
|
||||
} else if path.ends_with(local_vscode_tasks_file_relative_path()) {
|
||||
self.task_inventory().update(cx, |task_inventory, cx| {
|
||||
if removed {
|
||||
task_inventory.remove_local_static_source(&abs_path);
|
||||
|
||||
@@ -31,6 +31,7 @@ pub enum ComponentStory {
|
||||
OverflowScroll,
|
||||
Picker,
|
||||
Scroll,
|
||||
Setting,
|
||||
Tab,
|
||||
TabBar,
|
||||
Text,
|
||||
@@ -64,6 +65,7 @@ impl ComponentStory {
|
||||
Self::ListItem => cx.new_view(|_| ui::ListItemStory).into(),
|
||||
Self::OverflowScroll => cx.new_view(|_| crate::stories::OverflowScrollStory).into(),
|
||||
Self::Scroll => ScrollStory::view(cx).into(),
|
||||
Self::Setting => cx.new_view(|cx| ui::SettingStory::init(cx)).into(),
|
||||
Self::Text => TextStory::view(cx).into(),
|
||||
Self::Tab => cx.new_view(|_| ui::TabStory).into(),
|
||||
Self::TabBar => cx.new_view(|_| ui::TabBarStory).into(),
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::{fmt::Display, sync::Arc, time::Duration};
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct EventRequestBody {
|
||||
pub installation_id: Option<String>,
|
||||
pub metrics_id: Option<String>,
|
||||
pub session_id: Option<String>,
|
||||
pub is_staff: Option<bool>,
|
||||
pub app_version: String,
|
||||
|
||||
@@ -4,6 +4,7 @@ mod checkbox;
|
||||
mod context_menu;
|
||||
mod disclosure;
|
||||
mod divider;
|
||||
mod dropdown_menu;
|
||||
mod icon;
|
||||
mod indicator;
|
||||
mod keybinding;
|
||||
@@ -14,6 +15,7 @@ mod popover;
|
||||
mod popover_menu;
|
||||
mod radio;
|
||||
mod right_click_menu;
|
||||
mod setting;
|
||||
mod stack;
|
||||
mod tab;
|
||||
mod tab_bar;
|
||||
@@ -30,6 +32,7 @@ pub use checkbox::*;
|
||||
pub use context_menu::*;
|
||||
pub use disclosure::*;
|
||||
pub use divider::*;
|
||||
use dropdown_menu::*;
|
||||
pub use icon::*;
|
||||
pub use indicator::*;
|
||||
pub use keybinding::*;
|
||||
@@ -40,6 +43,7 @@ pub use popover::*;
|
||||
pub use popover_menu::*;
|
||||
pub use radio::*;
|
||||
pub use right_click_menu::*;
|
||||
pub use setting::*;
|
||||
pub use stack::*;
|
||||
pub use tab::*;
|
||||
pub use tab_bar::*;
|
||||
|
||||
85
crates/ui/src/components/dropdown_menu.rs
Normal file
85
crates/ui/src/components/dropdown_menu.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use crate::prelude::*;
|
||||
|
||||
/// !!don't use this yet – it's not functional!!
|
||||
///
|
||||
/// pub crate until this is functional
|
||||
///
|
||||
/// just a placeholder for now for filling out the settings menu stories.
|
||||
#[derive(Debug, Clone, IntoElement)]
|
||||
pub(crate) struct DropdownMenu {
|
||||
pub id: ElementId,
|
||||
current_item: Option<SharedString>,
|
||||
// items: Vec<SharedString>,
|
||||
full_width: bool,
|
||||
disabled: bool,
|
||||
}
|
||||
|
||||
impl DropdownMenu {
|
||||
pub fn new(id: impl Into<ElementId>, _cx: &WindowContext) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
current_item: None,
|
||||
// items: Vec::new(),
|
||||
full_width: false,
|
||||
disabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_item(mut self, current_item: Option<SharedString>) -> Self {
|
||||
self.current_item = current_item;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn full_width(mut self, full_width: bool) -> Self {
|
||||
self.full_width = full_width;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for DropdownMenu {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let disabled = self.disabled;
|
||||
|
||||
h_flex()
|
||||
.id(self.id)
|
||||
.justify_between()
|
||||
.rounded_md()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.pl_2()
|
||||
.pr_1p5()
|
||||
.py_0p5()
|
||||
.gap_2()
|
||||
.min_w_20()
|
||||
.when_else(
|
||||
self.full_width,
|
||||
|full_width| full_width.w_full(),
|
||||
|auto_width| auto_width.flex_none().w_auto(),
|
||||
)
|
||||
.when_else(
|
||||
disabled,
|
||||
|disabled| disabled.cursor_not_allowed(),
|
||||
|enabled| enabled.cursor_pointer(),
|
||||
)
|
||||
.child(
|
||||
Label::new(self.current_item.unwrap_or("".into())).color(if disabled {
|
||||
Color::Disabled
|
||||
} else {
|
||||
Color::Default
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Icon::new(IconName::ChevronUpDown)
|
||||
.size(IconSize::XSmall)
|
||||
.color(if disabled {
|
||||
Color::Disabled
|
||||
} else {
|
||||
Color::Muted
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -106,6 +106,7 @@ pub enum IconName {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ChevronUp,
|
||||
ChevronUpDown,
|
||||
Close,
|
||||
Code,
|
||||
Collab,
|
||||
@@ -141,6 +142,9 @@ pub enum IconName {
|
||||
Folder,
|
||||
FolderOpen,
|
||||
FolderX,
|
||||
Font,
|
||||
FontSize,
|
||||
FontWeight,
|
||||
Github,
|
||||
Hash,
|
||||
HistoryRerun,
|
||||
@@ -148,6 +152,7 @@ pub enum IconName {
|
||||
IndicatorX,
|
||||
InlayHint,
|
||||
Library,
|
||||
LineHeight,
|
||||
Link,
|
||||
ListTree,
|
||||
MagicWand,
|
||||
@@ -181,8 +186,8 @@ pub enum IconName {
|
||||
RotateCw,
|
||||
Save,
|
||||
Screen,
|
||||
SelectAll,
|
||||
SearchSelection,
|
||||
SelectAll,
|
||||
Server,
|
||||
Settings,
|
||||
Shift,
|
||||
@@ -212,6 +217,7 @@ pub enum IconName {
|
||||
ZedAssistant,
|
||||
ZedAssistantFilled,
|
||||
ZedXCopilot,
|
||||
Visible,
|
||||
}
|
||||
|
||||
impl IconName {
|
||||
@@ -224,6 +230,7 @@ impl IconName {
|
||||
IconName::ArrowLeft => "icons/arrow_left.svg",
|
||||
IconName::ArrowRight => "icons/arrow_right.svg",
|
||||
IconName::ArrowUp => "icons/arrow_up.svg",
|
||||
IconName::ArrowUpFromLine => "icons/arrow_up_from_line.svg",
|
||||
IconName::ArrowUpRight => "icons/arrow_up_right.svg",
|
||||
IconName::AtSign => "icons/at_sign.svg",
|
||||
IconName::AudioOff => "icons/speaker_off.svg",
|
||||
@@ -243,6 +250,7 @@ impl IconName {
|
||||
IconName::ChevronLeft => "icons/chevron_left.svg",
|
||||
IconName::ChevronRight => "icons/chevron_right.svg",
|
||||
IconName::ChevronUp => "icons/chevron_up.svg",
|
||||
IconName::ChevronUpDown => "icons/chevron_up_down.svg",
|
||||
IconName::Close => "icons/x.svg",
|
||||
IconName::Code => "icons/code.svg",
|
||||
IconName::Collab => "icons/user_group_16.svg",
|
||||
@@ -278,6 +286,9 @@ impl IconName {
|
||||
IconName::Folder => "icons/file_icons/folder.svg",
|
||||
IconName::FolderOpen => "icons/file_icons/folder_open.svg",
|
||||
IconName::FolderX => "icons/stop_sharing.svg",
|
||||
IconName::Font => "icons/font.svg",
|
||||
IconName::FontSize => "icons/font_size.svg",
|
||||
IconName::FontWeight => "icons/font_weight.svg",
|
||||
IconName::Github => "icons/github.svg",
|
||||
IconName::Hash => "icons/hash.svg",
|
||||
IconName::HistoryRerun => "icons/history_rerun.svg",
|
||||
@@ -285,6 +296,7 @@ impl IconName {
|
||||
IconName::IndicatorX => "icons/indicator_x.svg",
|
||||
IconName::InlayHint => "icons/inlay_hint.svg",
|
||||
IconName::Library => "icons/library.svg",
|
||||
IconName::LineHeight => "icons/line_height.svg",
|
||||
IconName::Link => "icons/link.svg",
|
||||
IconName::ListTree => "icons/list_tree.svg",
|
||||
IconName::MagicWand => "icons/magic_wand.svg",
|
||||
@@ -308,18 +320,18 @@ impl IconName {
|
||||
IconName::Quote => "icons/quote.svg",
|
||||
IconName::Regex => "icons/regex.svg",
|
||||
IconName::Replace => "icons/replace.svg",
|
||||
IconName::Reveal => "icons/reveal.svg",
|
||||
IconName::ReplaceAll => "icons/replace_all.svg",
|
||||
IconName::ReplaceNext => "icons/replace_next.svg",
|
||||
IconName::ReplyArrowRight => "icons/reply_arrow_right.svg",
|
||||
IconName::Rerun => "icons/rerun.svg",
|
||||
IconName::Return => "icons/return.svg",
|
||||
IconName::RotateCw => "icons/rotate_cw.svg",
|
||||
IconName::Reveal => "icons/reveal.svg",
|
||||
IconName::RotateCcw => "icons/rotate_ccw.svg",
|
||||
IconName::RotateCw => "icons/rotate_cw.svg",
|
||||
IconName::Save => "icons/save.svg",
|
||||
IconName::Screen => "icons/desktop.svg",
|
||||
IconName::SelectAll => "icons/select_all.svg",
|
||||
IconName::SearchSelection => "icons/search_selection.svg",
|
||||
IconName::SelectAll => "icons/select_all.svg",
|
||||
IconName::Server => "icons/server.svg",
|
||||
IconName::Settings => "icons/file_icons/settings.svg",
|
||||
IconName::Shift => "icons/shift.svg",
|
||||
@@ -349,7 +361,7 @@ impl IconName {
|
||||
IconName::ZedAssistant => "icons/zed_assistant.svg",
|
||||
IconName::ZedAssistantFilled => "icons/zed_assistant_filled.svg",
|
||||
IconName::ZedXCopilot => "icons/zed_x_copilot.svg",
|
||||
IconName::ArrowUpFromLine => "icons/arrow_up_from_line.svg",
|
||||
IconName::Visible => "icons/visible.svg",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ impl KeyBinding {
|
||||
Some(Self::new(key_binding))
|
||||
}
|
||||
|
||||
fn icon_for_key(keystroke: &Keystroke) -> Option<IconName> {
|
||||
fn icon_for_key(&self, keystroke: &Keystroke) -> Option<IconName> {
|
||||
match keystroke.key.as_str() {
|
||||
"left" => Some(IconName::ArrowLeft),
|
||||
"right" => Some(IconName::ArrowRight),
|
||||
@@ -45,6 +45,11 @@ impl KeyBinding {
|
||||
"escape" => Some(IconName::Escape),
|
||||
"pagedown" => Some(IconName::PageDown),
|
||||
"pageup" => Some(IconName::PageUp),
|
||||
"shift" if self.platform_style == PlatformStyle::Mac => Some(IconName::Shift),
|
||||
"control" if self.platform_style == PlatformStyle::Mac => Some(IconName::Control),
|
||||
"platform" if self.platform_style == PlatformStyle::Mac => Some(IconName::Command),
|
||||
"function" if self.platform_style == PlatformStyle::Mac => Some(IconName::Control),
|
||||
"alt" if self.platform_style == PlatformStyle::Mac => Some(IconName::Option),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -80,7 +85,7 @@ impl RenderOnce for KeyBinding {
|
||||
.gap(Spacing::Small.rems(cx))
|
||||
.flex_none()
|
||||
.children(self.key_binding.keystrokes().iter().map(|keystroke| {
|
||||
let key_icon = Self::icon_for_key(keystroke);
|
||||
let key_icon = self.icon_for_key(keystroke);
|
||||
|
||||
h_flex()
|
||||
.flex_none()
|
||||
|
||||
351
crates/ui/src/components/setting.rs
Normal file
351
crates/ui/src/components/setting.rs
Normal file
@@ -0,0 +1,351 @@
|
||||
use crate::{prelude::*, Checkbox, ListHeader};
|
||||
|
||||
use super::DropdownMenu;
|
||||
|
||||
#[derive(PartialEq, Clone, Eq, Debug)]
|
||||
pub enum ToggleType {
|
||||
Checkbox,
|
||||
// Switch,
|
||||
}
|
||||
|
||||
impl From<ToggleType> for SettingType {
|
||||
fn from(toggle_type: ToggleType) -> Self {
|
||||
SettingType::Toggle(toggle_type)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum InputType {
|
||||
Text,
|
||||
Number,
|
||||
}
|
||||
|
||||
impl From<InputType> for SettingType {
|
||||
fn from(input_type: InputType) -> Self {
|
||||
SettingType::Input(input_type)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SecondarySettingType {
|
||||
Dropdown,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SettingType {
|
||||
Toggle(ToggleType),
|
||||
ToggleAnd(SecondarySettingType),
|
||||
Input(InputType),
|
||||
Dropdown,
|
||||
Range,
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, IntoElement)]
|
||||
pub struct SettingsGroup {
|
||||
pub name: String,
|
||||
settings: Vec<SettingsItem>,
|
||||
}
|
||||
|
||||
impl SettingsGroup {
|
||||
pub fn new(name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
settings: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_setting(mut self, setting: SettingsItem) -> Self {
|
||||
self.settings.push(setting);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for SettingsGroup {
|
||||
fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
|
||||
let empty_message = format!("No settings available for {}", self.name);
|
||||
|
||||
let header = ListHeader::new(self.name);
|
||||
|
||||
let settings = self.settings.clone().into_iter();
|
||||
|
||||
v_flex()
|
||||
.p_1()
|
||||
.gap_2()
|
||||
.child(header)
|
||||
.when(self.settings.len() == 0, |this| {
|
||||
this.child(Label::new(empty_message))
|
||||
})
|
||||
.children(settings)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum SettingLayout {
|
||||
Stacked,
|
||||
AutoWidth,
|
||||
FullLine,
|
||||
FullLineJustified,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct SettingId(pub SharedString);
|
||||
|
||||
impl From<SettingId> for ElementId {
|
||||
fn from(id: SettingId) -> Self {
|
||||
ElementId::Name(id.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for SettingId {
|
||||
fn from(id: &str) -> Self {
|
||||
Self(id.to_string().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SharedString> for SettingId {
|
||||
fn from(id: SharedString) -> Self {
|
||||
Self(id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct SettingValue(pub SharedString);
|
||||
|
||||
impl From<SharedString> for SettingValue {
|
||||
fn from(value: SharedString) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for SettingValue {
|
||||
fn from(value: String) -> Self {
|
||||
Self(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for SettingValue {
|
||||
fn from(value: bool) -> Self {
|
||||
Self(value.to_string().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SettingValue> for bool {
|
||||
fn from(value: SettingValue) -> Self {
|
||||
value.0 == "true"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, IntoElement)]
|
||||
pub struct SettingsItem {
|
||||
pub id: SettingId,
|
||||
current_value: Option<SettingValue>,
|
||||
disabled: bool,
|
||||
hide_label: bool,
|
||||
icon: Option<IconName>,
|
||||
layout: SettingLayout,
|
||||
name: SharedString,
|
||||
// possible_values: Option<Vec<SettingValue>>,
|
||||
setting_type: SettingType,
|
||||
toggled: Option<bool>,
|
||||
}
|
||||
|
||||
impl SettingsItem {
|
||||
pub fn new(
|
||||
id: impl Into<SettingId>,
|
||||
name: SharedString,
|
||||
setting_type: SettingType,
|
||||
current_value: Option<SettingValue>,
|
||||
) -> Self {
|
||||
let toggled = match setting_type {
|
||||
SettingType::Toggle(_) | SettingType::ToggleAnd(_) => Some(false),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
Self {
|
||||
id: id.into(),
|
||||
current_value,
|
||||
disabled: false,
|
||||
hide_label: false,
|
||||
icon: None,
|
||||
layout: SettingLayout::FullLine,
|
||||
name,
|
||||
// possible_values: None,
|
||||
setting_type,
|
||||
toggled,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn layout(mut self, layout: SettingLayout) -> Self {
|
||||
self.layout = layout;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn toggled(mut self, toggled: bool) -> Self {
|
||||
self.toggled = Some(toggled);
|
||||
self
|
||||
}
|
||||
|
||||
// pub fn hide_label(mut self, hide_label: bool) -> Self {
|
||||
// self.hide_label = hide_label;
|
||||
// self
|
||||
// }
|
||||
|
||||
pub fn icon(mut self, icon: IconName) -> Self {
|
||||
self.icon = Some(icon);
|
||||
self
|
||||
}
|
||||
|
||||
// pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
// self.disabled = disabled;
|
||||
// self
|
||||
// }
|
||||
}
|
||||
|
||||
impl RenderOnce for SettingsItem {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let id: ElementId = self.id.clone().into();
|
||||
|
||||
// When the setting is disabled or toggled off, we don't want any secondary elements to be interactable
|
||||
let secondary_element_disabled = self.disabled || self.toggled == Some(false);
|
||||
|
||||
let full_width = match self.layout {
|
||||
SettingLayout::FullLine | SettingLayout::FullLineJustified => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
let hide_label = self.hide_label || self.icon.is_some();
|
||||
|
||||
let justified = match (self.layout.clone(), self.setting_type.clone()) {
|
||||
(_, SettingType::ToggleAnd(_)) => true,
|
||||
(SettingLayout::FullLineJustified, _) => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
let (setting_type, current_value) = (self.setting_type.clone(), self.current_value.clone());
|
||||
let current_string = if let Some(current_value) = current_value.clone() {
|
||||
Some(current_value.0)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let toggleable = match setting_type {
|
||||
SettingType::Toggle(_) => true,
|
||||
SettingType::ToggleAnd(_) => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
let setting_element = match setting_type {
|
||||
SettingType::Toggle(_) => None,
|
||||
SettingType::ToggleAnd(secondary_setting_type) => match secondary_setting_type {
|
||||
SecondarySettingType::Dropdown => Some(
|
||||
DropdownMenu::new(id.clone(), &cx)
|
||||
.current_item(current_string)
|
||||
.disabled(secondary_element_disabled)
|
||||
.into_any_element(),
|
||||
),
|
||||
},
|
||||
SettingType::Input(input_type) => match input_type {
|
||||
InputType::Text => Some(div().child("text").into_any_element()),
|
||||
InputType::Number => Some(div().child("number").into_any_element()),
|
||||
},
|
||||
SettingType::Dropdown => Some(
|
||||
DropdownMenu::new(id.clone(), &cx)
|
||||
.current_item(current_string)
|
||||
.full_width(true)
|
||||
.into_any_element(),
|
||||
),
|
||||
SettingType::Range => Some(div().child("range").into_any_element()),
|
||||
SettingType::Unsupported => None,
|
||||
};
|
||||
|
||||
let checkbox = Checkbox::new(
|
||||
ElementId::Name(format!("toggle-{}", self.id.0).to_string().into()),
|
||||
self.toggled.into(),
|
||||
)
|
||||
.disabled(self.disabled);
|
||||
|
||||
let toggle_element = match (toggleable, self.setting_type.clone()) {
|
||||
(true, SettingType::Toggle(toggle_type)) => match toggle_type {
|
||||
ToggleType::Checkbox => Some(checkbox.into_any_element()),
|
||||
},
|
||||
(true, SettingType::ToggleAnd(_)) => Some(checkbox.into_any_element()),
|
||||
(_, _) => None,
|
||||
};
|
||||
|
||||
let item = if self.layout == SettingLayout::Stacked {
|
||||
v_flex()
|
||||
} else {
|
||||
h_flex()
|
||||
};
|
||||
|
||||
item.id(id)
|
||||
.gap_2()
|
||||
.w_full()
|
||||
.when_some(self.icon, |this, icon| {
|
||||
this.child(div().px_0p5().child(Icon::new(icon).color(Color::Muted)))
|
||||
})
|
||||
.children(toggle_element)
|
||||
.children(if hide_label {
|
||||
None
|
||||
} else {
|
||||
Some(Label::new(self.name.clone()))
|
||||
})
|
||||
.when(justified, |this| this.child(div().flex_1().size_full()))
|
||||
.child(
|
||||
h_flex()
|
||||
.when(full_width, |this| this.w_full())
|
||||
.when(self.layout == SettingLayout::FullLineJustified, |this| {
|
||||
this.justify_end()
|
||||
})
|
||||
.children(setting_element),
|
||||
)
|
||||
// help flex along when full width is disabled
|
||||
//
|
||||
// this probably isn't needed, but fighting with flex to
|
||||
// get this right without inspection tools will be a pain
|
||||
.when(!full_width, |this| this.child(div().size_full().flex_1()))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SettingsMenu {
|
||||
name: SharedString,
|
||||
groups: Vec<SettingsGroup>,
|
||||
}
|
||||
|
||||
impl SettingsMenu {
|
||||
pub fn new(name: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
groups: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_group(mut self, group: SettingsGroup) -> Self {
|
||||
self.groups.push(group);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for SettingsMenu {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let is_empty = self.groups.is_empty();
|
||||
v_flex()
|
||||
.id(ElementId::Name(self.name.clone()))
|
||||
.elevation_2(cx)
|
||||
.min_w_56()
|
||||
.max_w_96()
|
||||
.max_h_2_3()
|
||||
.px_2()
|
||||
.when_else(
|
||||
is_empty,
|
||||
|empty| empty.py_1(),
|
||||
|not_empty| not_empty.pt_0().pb_1(),
|
||||
)
|
||||
.gap_1()
|
||||
.when(is_empty, |this| {
|
||||
this.child(Label::new("No settings found").color(Color::Muted))
|
||||
})
|
||||
.children(self.groups.clone())
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ mod label;
|
||||
mod list;
|
||||
mod list_header;
|
||||
mod list_item;
|
||||
mod setting;
|
||||
mod tab;
|
||||
mod tab_bar;
|
||||
mod title_bar;
|
||||
@@ -28,6 +29,7 @@ pub use label::*;
|
||||
pub use list::*;
|
||||
pub use list_header::*;
|
||||
pub use list_item::*;
|
||||
pub use setting::*;
|
||||
pub use tab::*;
|
||||
pub use tab_bar::*;
|
||||
pub use title_bar::*;
|
||||
|
||||
225
crates/ui/src/components/stories/setting.rs
Normal file
225
crates/ui/src/components/stories/setting.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
use gpui::View;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::{
|
||||
SecondarySettingType, SettingLayout, SettingType, SettingsGroup, SettingsItem, SettingsMenu,
|
||||
ToggleType,
|
||||
};
|
||||
|
||||
pub struct SettingStory {
|
||||
menus: Vec<(SharedString, View<SettingsMenu>)>,
|
||||
}
|
||||
|
||||
impl SettingStory {
|
||||
pub fn new() -> Self {
|
||||
Self { menus: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut ViewContext<Self>) -> Self {
|
||||
let mut story = Self::new();
|
||||
story.empty_menu(cx);
|
||||
story.editor_example(cx);
|
||||
story.menu_single_group(cx);
|
||||
story
|
||||
}
|
||||
}
|
||||
|
||||
impl SettingStory {
|
||||
pub fn empty_menu(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let menu = cx.new_view(|_cx| SettingsMenu::new("Empty Menu"));
|
||||
|
||||
self.menus.push(("Empty Menu".into(), menu));
|
||||
}
|
||||
|
||||
pub fn menu_single_group(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let theme_setting = SettingsItem::new(
|
||||
"theme-setting",
|
||||
"Theme".into(),
|
||||
SettingType::Dropdown,
|
||||
Some(cx.theme().name.clone().into()),
|
||||
)
|
||||
.layout(SettingLayout::Stacked);
|
||||
let high_contrast_setting = SettingsItem::new(
|
||||
"theme-contrast",
|
||||
"Use high contrast theme".into(),
|
||||
SettingType::Toggle(ToggleType::Checkbox),
|
||||
None,
|
||||
)
|
||||
.toggled(false);
|
||||
let appearance_setting = SettingsItem::new(
|
||||
"switch-appearance",
|
||||
"Match system appearance".into(),
|
||||
SettingType::ToggleAnd(SecondarySettingType::Dropdown),
|
||||
Some("When Dark".to_string().into()),
|
||||
)
|
||||
.layout(SettingLayout::FullLineJustified);
|
||||
|
||||
let group = SettingsGroup::new("Appearance")
|
||||
.add_setting(theme_setting)
|
||||
.add_setting(appearance_setting)
|
||||
.add_setting(high_contrast_setting);
|
||||
|
||||
let menu = cx.new_view(|_cx| SettingsMenu::new("Appearance").add_group(group));
|
||||
|
||||
self.menus.push(("Single Group".into(), menu));
|
||||
}
|
||||
|
||||
pub fn editor_example(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let font_group = SettingsGroup::new("Font")
|
||||
.add_setting(
|
||||
SettingsItem::new(
|
||||
"font-family",
|
||||
"Font".into(),
|
||||
SettingType::Dropdown,
|
||||
Some("Berkeley Mono".to_string().into()),
|
||||
)
|
||||
.icon(IconName::Font)
|
||||
.layout(SettingLayout::AutoWidth),
|
||||
)
|
||||
.add_setting(
|
||||
SettingsItem::new(
|
||||
"font-weifht",
|
||||
"Font Weight".into(),
|
||||
SettingType::Dropdown,
|
||||
Some("400".to_string().into()),
|
||||
)
|
||||
.icon(IconName::FontWeight)
|
||||
.layout(SettingLayout::AutoWidth),
|
||||
)
|
||||
.add_setting(
|
||||
SettingsItem::new(
|
||||
"font-size",
|
||||
"Font Size".into(),
|
||||
SettingType::Dropdown,
|
||||
Some("14".to_string().into()),
|
||||
)
|
||||
.icon(IconName::FontSize)
|
||||
.layout(SettingLayout::AutoWidth),
|
||||
)
|
||||
.add_setting(
|
||||
SettingsItem::new(
|
||||
"line-height",
|
||||
"Line Height".into(),
|
||||
SettingType::Dropdown,
|
||||
Some("1.35".to_string().into()),
|
||||
)
|
||||
.icon(IconName::LineHeight)
|
||||
.layout(SettingLayout::AutoWidth),
|
||||
)
|
||||
.add_setting(
|
||||
SettingsItem::new(
|
||||
"enable-ligatures",
|
||||
"Enable Ligatures".into(),
|
||||
SettingType::Toggle(ToggleType::Checkbox),
|
||||
None,
|
||||
)
|
||||
.toggled(true),
|
||||
);
|
||||
|
||||
let editor_group = SettingsGroup::new("Editor")
|
||||
.add_setting(
|
||||
SettingsItem::new(
|
||||
"show-indent-guides",
|
||||
"Indent Guides".into(),
|
||||
SettingType::Toggle(ToggleType::Checkbox),
|
||||
None,
|
||||
)
|
||||
.toggled(true),
|
||||
)
|
||||
.add_setting(
|
||||
SettingsItem::new(
|
||||
"show-git-blame",
|
||||
"Git Blame".into(),
|
||||
SettingType::Toggle(ToggleType::Checkbox),
|
||||
None,
|
||||
)
|
||||
.toggled(false),
|
||||
);
|
||||
|
||||
let gutter_group = SettingsGroup::new("Gutter")
|
||||
.add_setting(
|
||||
SettingsItem::new(
|
||||
"enable-git-hunks",
|
||||
"Show Git Hunks".into(),
|
||||
SettingType::Toggle(ToggleType::Checkbox),
|
||||
None,
|
||||
)
|
||||
.toggled(true),
|
||||
)
|
||||
.add_setting(
|
||||
SettingsItem::new(
|
||||
"show-line-numbers",
|
||||
"Line Numbers".into(),
|
||||
SettingType::ToggleAnd(SecondarySettingType::Dropdown),
|
||||
Some("Ascending".to_string().into()),
|
||||
)
|
||||
.toggled(true)
|
||||
.layout(SettingLayout::FullLineJustified),
|
||||
);
|
||||
|
||||
let scrollbar_group = SettingsGroup::new("Scrollbar")
|
||||
.add_setting(
|
||||
SettingsItem::new(
|
||||
"scrollbar-visibility",
|
||||
"Show scrollbar when:".into(),
|
||||
SettingType::Dropdown,
|
||||
Some("Always Visible".to_string().into()),
|
||||
)
|
||||
.layout(SettingLayout::AutoWidth)
|
||||
.icon(IconName::Visible),
|
||||
)
|
||||
.add_setting(
|
||||
SettingsItem::new(
|
||||
"show-diagnostic-markers",
|
||||
"Diagnostic Markers".into(),
|
||||
SettingType::Toggle(ToggleType::Checkbox),
|
||||
None,
|
||||
)
|
||||
.toggled(true),
|
||||
)
|
||||
.add_setting(
|
||||
SettingsItem::new(
|
||||
"show-git-markers",
|
||||
"Git Status Markers".into(),
|
||||
SettingType::Toggle(ToggleType::Checkbox),
|
||||
None,
|
||||
)
|
||||
.toggled(false),
|
||||
)
|
||||
.add_setting(
|
||||
SettingsItem::new(
|
||||
"show-selection-markers",
|
||||
"Selection & Match Markers".into(),
|
||||
SettingType::Toggle(ToggleType::Checkbox),
|
||||
None,
|
||||
)
|
||||
.toggled(true),
|
||||
);
|
||||
|
||||
let menu = cx.new_view(|_cx| {
|
||||
SettingsMenu::new("Editor")
|
||||
.add_group(font_group)
|
||||
.add_group(editor_group)
|
||||
.add_group(gutter_group)
|
||||
.add_group(scrollbar_group)
|
||||
});
|
||||
|
||||
self.menus.push(("Editor Example".into(), menu));
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for SettingStory {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.bg(cx.theme().colors().background)
|
||||
.text_color(cx.theme().colors().text)
|
||||
.children(self.menus.iter().map(|(name, menu)| {
|
||||
v_flex()
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.child(Headline::new(name.clone()).size(HeadlineSize::Medium))
|
||||
.child(menu.clone())
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -29,3 +29,23 @@ impl Selection {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for Selection {
|
||||
fn from(selected: bool) -> Self {
|
||||
if selected {
|
||||
Self::Selected
|
||||
} else {
|
||||
Self::Unselected
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<bool>> for Selection {
|
||||
fn from(selected: Option<bool>) -> Self {
|
||||
match selected {
|
||||
Some(true) => Self::Selected,
|
||||
Some(false) => Self::Unselected,
|
||||
None => Self::Unselected,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3826,7 +3826,8 @@ impl BackgroundScanner {
|
||||
.await;
|
||||
|
||||
// Ensure that .git and .gitignore are processed first.
|
||||
child_paths.sort_unstable();
|
||||
swap_to_front(&mut child_paths, *GITIGNORE);
|
||||
swap_to_front(&mut child_paths, *DOT_GIT);
|
||||
|
||||
for child_abs_path in child_paths {
|
||||
let child_abs_path: Arc<Path> = child_abs_path.into();
|
||||
@@ -4620,6 +4621,16 @@ impl BackgroundScanner {
|
||||
}
|
||||
}
|
||||
|
||||
fn swap_to_front(child_paths: &mut Vec<PathBuf>, file: &OsStr) {
|
||||
let position = child_paths
|
||||
.iter()
|
||||
.position(|path| path.file_name().unwrap() == file);
|
||||
if let Some(position) = position {
|
||||
let temp = child_paths.remove(position);
|
||||
child_paths.insert(0, temp);
|
||||
}
|
||||
}
|
||||
|
||||
fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag {
|
||||
let mut result = root_char_bag;
|
||||
result.extend(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.142.0"
|
||||
version = "0.143.0"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -105,10 +105,10 @@ pub fn build_window_options(display_uuid: Option<Uuid>, cx: &mut AppContext) ->
|
||||
display_id: display.map(|display| display.id()),
|
||||
window_background: cx.theme().window_background_appearance(),
|
||||
app_id: Some(app_id.to_owned()),
|
||||
window_min_size: gpui::Size {
|
||||
window_min_size: Some(gpui::Size {
|
||||
width: px(360.0),
|
||||
height: px(240.0),
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ Thank you for taking on the task of packaging Zed!
|
||||
Zed has two main binaries:
|
||||
|
||||
* You will need to build `crates/cli` and make it's binary available in `$PATH` with the name `zed`.
|
||||
* You will need to build `crates/zed` and put it at `$PATH/to/cli/../../libexec/zed-editor`. For example, if you are going to put the cli at `~/.local/bin/zed` put zed at `~/.local/libexec/zed-editor`.
|
||||
* You will need to build `crates/zed` and put it at `$PATH/to/cli/../../libexec/zed-editor`. For example, if you are going to put the cli at `~/.local/bin/zed` put zed at `~/.local/libexec/zed-editor`. As some linux distributions (notably Arch) discourage the use of `libexec`, you can also put this binary at `$PATH/to/cli/../../lib/zed/zed-editor` (e.g. `~/.local/lib/zed/zed-editor`) instead.
|
||||
* If you are going to provide a `.desktop` file you can find a template in `crates/zed/resources/zed.desktop.in`, and use `envsubst` to populate it with the values required. This file should also be renamed to `$APP_ID.desktop`, so that the file [follows the FreeDesktop standards](https://github.com/zed-industries/zed/issues/12707#issuecomment-2168742761).
|
||||
* You will need to ensure that the necessary libraries are installed. You can get the current list by [inspecting the built binary](https://github.com/zed-industries/zed/blob/059a4141b756cf4afac4c977afc488539aec6470/script/bundle-linux#L65-L70) on your system.
|
||||
* For an example of a complete build script, see [script/bundle-linux](https://github.com/zed-industries/zed/blob/main/script/bundle-linux).
|
||||
|
||||
@@ -50,12 +50,12 @@ Zed has the ability to match against not just a single keypress, but a sequence
|
||||
Each key press is a sequence of modifiers followed by a key. The modifiers are:
|
||||
|
||||
- `ctrl-` The control key
|
||||
- `cmd-` On macOS, this is the command key
|
||||
- `alt-` On macOS, this is the option key
|
||||
* `cmd-`, `win-` or `super-` for the platform modifier (Command on macOS, Windows key on Windows, and the Super key on Linux).
|
||||
- `alt-` for alt (option on macOS)
|
||||
- `shift-` The shift key
|
||||
- `fn-` The function key
|
||||
|
||||
The keys can be any single unicode codepoint that your keyboard generates (for example `a`, `0`, `£` or `ç`).
|
||||
The keys can be any single unicode codepoint that your keyboard generates (for example `a`, `0`, `£` or `ç`), or any named key (`tab`, `f1`, `shift`, or `cmd`).
|
||||
|
||||
A few examples:
|
||||
|
||||
@@ -64,10 +64,15 @@ A few examples:
|
||||
"cmd-k cmd-s": "zed::OpenKeymap", // matches ⌘-k then ⌘-s
|
||||
"space e": "editor::Complete", // type space then e
|
||||
"ç": "editor::Complete", // matches ⌥-c
|
||||
"shift shift": "file_finder::Toggle", // matches pressing and releasing shift twice
|
||||
}
|
||||
```
|
||||
|
||||
NOTE: Keys on a keyboard are not always the same as the character they generate. For example `shift-e` actually types `E` (or `alt-c` types `ç`). Zed allows you to match against either the key and its modifiers or the character it generates. This means you can specify `alt-c` or `ç`, but not `alt-ç`. It is usually better to specify the key and its modifiers, as this will work better on different keyboard layouts.
|
||||
The `shift-` modifier can only be used in combination with a letter to indicate the uppercase version. For example `shift-g` matches typing `G`. Although on many keyboards shift is used to type punctuation characters like `(`, the keypress is not considered to be modified and so `shift-(` does not match.
|
||||
|
||||
The `alt-` modifier can be used on many layouts to generate a different key. For example on macOS US keyboard the combination `alt-c` types `ç`. You can match against either in your keymap file, though by convention Zed spells this combination as `alt-c`.
|
||||
|
||||
It is possible to match against typing a modifier key on its own. For example `shift shift` can be used to implement JetBrains search everywhere shortcut. In this case the binding happens on key release instead of key press.
|
||||
|
||||
### Remapping keys
|
||||
|
||||
@@ -155,7 +160,7 @@ See the [tasks documentation](/docs/tasks#custom-keybindings-for-tasks) for more
|
||||
| Open | Workspace | `⌘ + O` |
|
||||
| Toggle zoom | Workspace | `Shift + Escape` |
|
||||
| Debug elements | Zed | `⌘ + Alt + I` |
|
||||
| Decrease buffer font size | Zed | `⌘ + ` |
|
||||
| Decrease buffer font size | Zed | `⌘ + -` |
|
||||
| Hide | Zed | `⌘ + H` |
|
||||
| Hide others | Zed | `Alt + ⌘ + H` |
|
||||
| Increase buffer font size | Zed | `⌘ + +` |
|
||||
|
||||
Reference in New Issue
Block a user