Compare commits
21 Commits
vim-mrnugg
...
v0.142.1-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b0b7fe064 | ||
|
|
985644bacb | ||
|
|
c686c4c800 | ||
|
|
5d766f61fa | ||
|
|
18b4573064 | ||
|
|
d044dc8485 | ||
|
|
f00bea5d0f | ||
|
|
b43df6048b | ||
|
|
eb914682b3 | ||
|
|
5b7e31c075 | ||
|
|
922fcaf5a6 | ||
|
|
9f88460870 | ||
|
|
e5d1cf84cf | ||
|
|
41d2c52638 | ||
|
|
d1a55d64a8 | ||
|
|
db06244972 | ||
|
|
597469bbbd | ||
|
|
e0c192d831 | ||
|
|
b2a0a7fa3c | ||
|
|
0b1a589183 | ||
|
|
7e694d1bcf |
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.142.1"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -94,6 +94,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",
|
||||
|
||||
@@ -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()),
|
||||
),
|
||||
)
|
||||
}))
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12390,6 +12390,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')
|
||||
|
||||
@@ -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.end, 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.start, 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, _: &SelectRight, 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()
|
||||
|
||||
@@ -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};
|
||||
@@ -508,6 +509,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,
|
||||
@@ -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,6 +1,5 @@
|
||||
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};
|
||||
@@ -29,13 +28,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, 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};
|
||||
@@ -116,7 +115,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 +128,7 @@ pub struct X11ClientState {
|
||||
|
||||
pub(crate) common: LinuxCommon,
|
||||
pub(crate) clipboard: x11_clipboard::Clipboard,
|
||||
pub(crate) clipboard_item: Option<ClipboardItem>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -249,18 +249,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
|
||||
@@ -400,7 +389,7 @@ impl X11Client {
|
||||
ximc,
|
||||
xim_handler,
|
||||
|
||||
compose_state: compose_state,
|
||||
compose_state,
|
||||
pre_edit_text: None,
|
||||
composing: false,
|
||||
|
||||
@@ -413,6 +402,7 @@ impl X11Client {
|
||||
scroll_y: None,
|
||||
|
||||
clipboard,
|
||||
clipboard_item: None,
|
||||
})))
|
||||
}
|
||||
|
||||
@@ -524,7 +514,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 +562,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 +646,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();
|
||||
@@ -1097,7 +1096,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 +1105,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 +1127,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(
|
||||
@@ -1167,6 +1181,10 @@ 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 {
|
||||
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);
|
||||
|
||||
@@ -34,7 +34,6 @@ use std::{
|
||||
};
|
||||
|
||||
use super::{X11Display, XINPUT_MASTER_DEVICE};
|
||||
|
||||
x11rb::atom_manager! {
|
||||
pub XcbAtoms: AtomsCookie {
|
||||
UTF8_STRING,
|
||||
|
||||
@@ -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,15 @@ impl schemars::JsonSchema for FontFeatures {
|
||||
schema.into()
|
||||
}
|
||||
}
|
||||
|
||||
fn is_valid_feature_tag(tag: &str) -> bool {
|
||||
if tag.len() != 4 {
|
||||
return false;
|
||||
}
|
||||
for ch in tag.chars() {
|
||||
if !ch.is_ascii_alphanumeric() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -823,6 +824,7 @@ impl Window {
|
||||
focus: None,
|
||||
focus_enabled: true,
|
||||
pending_input: None,
|
||||
pending_modifiers: None,
|
||||
pending_input_observers: SubscriberSet::new(),
|
||||
prompt: None,
|
||||
})
|
||||
@@ -3161,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;
|
||||
|
||||
@@ -348,7 +348,7 @@ impl LspAdapter for NodeVersionAdapter {
|
||||
}
|
||||
|
||||
Ok(LanguageServerBinary {
|
||||
path: destination_path.join("package-version-server"),
|
||||
path: destination_path,
|
||||
env: None,
|
||||
arguments: Default::default(),
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.142.1"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
authors = ["Zed Team <hi@zed.dev>"]
|
||||
|
||||
@@ -1 +1 @@
|
||||
dev
|
||||
preview
|
||||
@@ -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),
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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