Compare commits

..

37 Commits

Author SHA1 Message Date
Thorsten Ball
feb7efff4e linux/x11: use a channel for quit signal 2024-06-27 18:01:59 +02:00
Thorsten Ball
3956717065 WIP linux/x11: Tweak when we sleep in the event loop 2024-06-27 16:24:30 +02:00
Thorsten Ball
d3e2327099 WIP: linux/x11: Replace calloop event loop with custom loop 2024-06-27 16:06:32 +02:00
Thorsten Ball
fc945cc351 linux/x11: Store refresh rate on X11Window itself 2024-06-27 13:52:10 +02:00
Piotr Osiewicz
da22e0dd0b Revert "vue: Release 0.0.4" (#13584)
Reverts zed-industries/zed#13580 as it turned out that the issue lied in
incorrect user settings.

Release notes:
- N/A
2024-06-27 11:36:17 +02:00
Tim Havlicek
fb3ef0d140 Add separate JSONC language (#12655)
Resolves https://github.com/zed-industries/extensions/issues/860 and
https://github.com/zed-industries/zed/issues/10921, also
https://github.com/biomejs/biome-zed/issues/11.

### Problem:
When opening .json files, zed allows comments by default in the JSON
language, which can cause some problems.
For example, language-servers also get "json" as the language, which may
show errors for those comments.

<img width="935" alt="image"
src="https://github.com/zed-industries/zed/assets/10381895/fed3d83d-abc0-44b5-9982-eb249bb04c3b">

### Solution:

This PR adds a JSONC language. 

<img width="816" alt="image"
src="https://github.com/zed-industries/zed/assets/10381895/8b40e671-d4f0-4e8d-80cb-82ee7c0ec490">

This allows for more specific configuration for language servers. 
Also any json file can be set explicitly to be JSONC using the
file_types setting:

```jsonc
{
  "file_types": {
    // set all .json files to be seen as JSONC
    "JSONC": ["*.json"]
  }
}
```


Release Notes:

- N/A
2024-06-27 11:12:02 +02:00
Piotr Osiewicz
e71b642f44 vue: Release 0.0.4 (#13580)
Respect user settings in initialization_options.


Release Notes:

- Fixed Vue extension not picking up user-provided initialization
options.
2024-06-27 11:11:22 +02:00
Jason Lee
6cedfa0ce7 example: Fix Input example mistake (#13574)
![CleanShot 2024-06-27 at 15 52
48](https://github.com/zed-industries/zed/assets/5518/71b25759-0cd5-40ed-b7c2-2f1045f81683)

Release Notes:

- N/A
2024-06-27 11:28:44 +03:00
Gilles Peiffer
209b1d1931 Code maintenance in the editor crate (#13565)
Release Notes:

- N/A
2024-06-27 09:40:48 +03:00
Gilles Peiffer
6986ac4c27 Use iterators instead of loops in clock.rs (#13561)
This should be slightly faster and makes the code easier to read.

Release Notes:

- N/A
2024-06-27 09:30:21 +03:00
Peter Tripp
d50d1611b9 Release notes upload fix (#13560)
- Action for release notes upload (softprops/action-gh-release) configured with incorrect key. 
- Valid keys here: https://github.com/softprops/action-gh-release?tab=readme-ov-file#-customizing
2024-06-26 17:24:59 -04:00
Gilles Peiffer
1260c616ba Simplify font feature tag validation (#13548)
Simplifies the logic for the changes of #13542.

Release Notes:

- N/A
2024-06-26 17:11:57 -04:00
Joseph T. Lyons
89951f7e66 Add shift shift to open command palette (#13556)
I've add `shift shift` as a default keybinding to open command palette,
when using JetBrains keymap, along with the already existing
`cmd-shift-a`. This isn't quite right, as in JetBrains, `cmd-shift-a`
opens the actions modal, which would be our command palette, and `shift
shift` actually opens up a view for searching everything, commands,
actions, settings, etc - we do not have a unified modal for these
things, so I think this is the best thing we can do. Some users might
want to change this to be our file picker, but I think adding it as the
default at least puts it on their radar that they can use this type of
binding; they can change it if they want.

Release Notes:

- Added `shift shift` as a default binding to open the command palette
in the JetBrains keymap.
2024-06-26 16:44:40 -04:00
Conrad Irwin
cd81dad2fa fix panics (#13554)
Release Notes:

- Fixed a panic when editing HTML near the end of a file
- Fixed a panic when editing settings.json from inside the .zed
directory
2024-06-26 14:32:16 -06:00
Piotr Osiewicz
3a08d7ab43 json: Fix package-version-server referencing the wrong path to the binary (#13555)
We were trying to access the binary at
package-version-server-{VERSION}/package-version-server, whereas the
binary itself is placed at package-version-server-{VERSION}

Release Notes:

- Fixed package.json language server failing to start.

Co-authored-by: Peter Tripp <peter@zed.dev>
2024-06-26 16:17:55 -04:00
Josef Zoller
49dc63812a Stop relying on binary location to be in libexec on Linux (#13374)
This fixes #13360 by adding fallback directories that are searched by
the CLI if the main executable cannot be found in the `libexec`
directory.

Release Notes:

- Added the fallback directories `lib/zed` and `lib/zed-editor` for the
main executable search in the CLI
([#13360](https://github.com/zed-industries/zed/issues/13360)).

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-06-26 13:00:52 -06:00
Tristan Hume
c0a3642f77 Improve prompt for Claude models (#13531)
This inline assistant prompt is one I designed that in my experience
works much better with Claude 3.5 Sonnet than the default prompt.

Mainly because it takes advantage of a weird property of our finetuning
which is that when you use XML tags it knows that it's doing a
machine-read tasks and stops trying to elide things for brevity. The
default prompt will often remove comments and otherwise add elisions for
brevity when doing large rewrites.

It also avoids giving the entire file content twice when the rewrite
region is large relative to the non-rewritten region.

Not necessarily meant to be merged as-is since it may mess up OAI
models. This is mainly meant for your reference. But everyone should be
using 3.5 Sonnet for coding use cases now anyhow 😛

Release Notes:

- N/A
2024-06-26 20:41:40 +02:00
Nate Butler
4d5441c09d Add UI setting components (#13550)
Adds some of the UI components to allow us to visually render settings.

These are UI only and are not functional yet (@maxdeviant will be
working on these when he is back.)

You can see some examples by running `script/storybook setting`.

![CleanShot 2024-06-26 at 12 38
37@2x](https://github.com/zed-industries/zed/assets/1714999/b5e6434d-3bc5-4fcd-9c0a-d280950cbef2)

Release Notes:

- N/A
2024-06-26 13:02:58 -04:00
Peter Tripp
2dc840132b v0.143.x dev 2024-06-26 12:20:15 -04:00
Fernando Tagawa
5d766f61fa linux: Fix some panics related to xkb compose (#13529)
Release Notes:

- N/A

Fixed #13463 Fixed crash when the locale was non UTF-8 and fixed the
fallback locale.
Fixed #13010 Fixed crash when `compose.keysym()` was `XKB_KEY_NoSymbol`

I also extracted the `xkb_compose_state` to a single place
2024-06-26 09:34:39 -06:00
张小白
18b4573064 Fix font feature tag validation (#13542)
The previous implementation that I implemented had two issues:
1. It did not throw an error when the user input some invalid values
such as "panic".
2. The feature tag for OpenType fonts should be a combination of letters
and digits. We only checked if the input was an ASCII character, which
could lead to undefined behavior.

Closes #13517 

Release Notes:

- N/A
2024-06-26 11:01:48 -04:00
Toshimaru
d044dc8485 Update Docker Compose configuration (#13530)
- Fix Docker Compose obsolete setting

## Remove `version`

Fix the following error:

```
WARN[0000] /docker-compose.yml: `version` is obsolete
```

see also.
https://github.com/compose-spec/compose-spec/blob/master/spec.md#version-top-level-element-obsolete

## Rename: docker-compose.yml -> compose.yml

The preferred file name is now `compose.yml`.

> The default path for a Compose file is compose.yaml (preferred)

ref.
https://docs.docker.com/compose/compose-application-model/#the-compose-file

Release Notes:

- N/A
2024-06-26 08:05:23 -04:00
Alexander Mankuta
f00bea5d0f docs: Fix Decrease buffer font size key binding (#13453)
Release Notes:

- N/A
2024-06-26 10:48:00 +03:00
Conrad Irwin
b43df6048b Add an input example to gpui (#13534)
Add a single-line text input example to gpui

(I'm hoping to be able to debug keyboard issues without rebuilding the
whole
app every time)

Release Notes:

- N/A
2024-06-25 22:06:50 -06:00
Conrad Irwin
eb914682b3 Fix multi-cursor copy/paste on linux (#13523)
The clipboard library we use for X11 doesn't yet support multiple
formats on the clipboard, so for now we just store this in memory for
the current zed process, as we do for Wayland.

Fixes: #11971

Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla@zed.dev>
2024-06-25 14:54:52 -06:00
Joseph T. Lyons
5b7e31c075 Add metrics_id to editor_events (#13525)
Release Notes:

- N/A
2024-06-25 16:47:55 -04:00
ᴀᴍᴛᴏᴀᴇʀ
922fcaf5a6 Add the ability to customize available models for OpenAI-compatible services (#13276)
Closes #11984, closes #11075.

Release Notes:

- Added the ability to customize available models for OpenAI-compatible
services ([#11984](https://github.com/zed-industries/zed/issues/11984))
([#11075](https://github.com/zed-industries/zed/issues/11075)).


![image](https://github.com/zed-industries/zed/assets/32017007/01057e7b-1f21-49ad-a3ad-abc5282ffaf0)
2024-06-25 16:37:02 -04:00
Nate Butler
9f88460870 Move token count in prompt editor (#13524)
Moves the token count back up to the editor header.

Release Notes:

- N/A
2024-06-25 16:10:05 -04:00
Mikayla Maki
e5d1cf84cf Fix 9263 (#13521)
Fix #9263

Release Notes:

- N/A
2024-06-25 11:35:50 -07:00
Mikayla Maki
41d2c52638 Adjust keybindings for deletion in the project panel (#13326)
- Improve compatibility keybindings (Atom, JetBrains, TextMate)
- Revert MacOS cmd+backspace regression. Should trash without prompting (like MacOS)

Co-authored-by: Peter Tripp <peter@zed.dev>
2024-06-25 14:21:44 -04:00
张小白
d1a55d64a8 Change window_min_size from Size<Pixels> to Option<Size<Pixels>> (#13501)
Now we can set `window_min_size` to `None` instead of `Size::default()`.
I think this makes more sense.

Release Notes:

- N/A
2024-06-25 12:09:08 -06:00
Shubham Kanodia
db06244972 typescript: Pass hostInfo to tsserver (#12055)
- Added `hostInfo` property to zed's typescript plugin. This can be
useful for telemetry (for e.g. identifying the usage of editors based on
typescript usage) when building typescript plugins.

- VSCode / IntelliJ based editors already set this property
([see](aa31bfc9fd/extensions/typescript-language-features/src/typescriptServiceClient.ts (L574)))

The config option as available —
https://github.com/typescript-language-server/typescript-language-server/blob/master/docs/configuration.md#initializationoptions

Release Notes:

- N/A
2024-06-25 13:51:30 -04:00
Marshall Bowers
597469bbbd Remove blank line (#13519)
This PR removes an extra blank line that was missed in #13518.

Release Notes:

- N/A
2024-06-25 13:11:25 -04:00
Marshall Bowers
e0c192d831 Clean up json! literal for vtsls configuration (#13518)
This PR cleans up the formatting of the `json!` literal used to provided
`vtsls` configuration.

Release Notes:

- N/A
2024-06-25 13:04:31 -04:00
Mikayla Maki
b2a0a7fa3c Fix a bug introduced by #13479 (#13516)
Fixes a bug introduced by
https://github.com/zed-industries/zed/pull/13479 where dot files might
not be processed in the correct order.

Release Notes:

- N/A
2024-06-25 10:03:29 -07:00
Dov Alperin
0b1a589183 keymap: Allow modifiers as keys (#12047)
It is sometimes desirable to allow modifers to serve as keys themselves
for the purposes of keybinds. For example, the popular keybind in
jetbrains IDEs `shift shift` which opens the file finder.

This change treats modifers in the keymaps as keys themselves if they
are not accompanied by a key they are modifying.

Further this change wires up they key dispatcher to treat modifer change
events as key presses which are considered for matching against
keybinds.


Release Notes:

- Fixes #6460

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-06-25 10:17:23 -06:00
ᴀᴍᴛᴏᴀᴇʀ
7e694d1bcf Fix an issue where provider settings were lost when switching between Ollama models (#13402)
Closes #13399.

Release Notes:

- Fixed an issue where provider settings were lost when switching
between Ollama models
([#13399](https://github.com/zed-industries/zed/issues/13399)).
2024-06-25 11:58:13 -04:00
75 changed files with 2263 additions and 667 deletions

View File

@@ -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
View File

@@ -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",

View 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
View 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

View 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

View 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

View 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
View 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

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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"
}
}

View File

@@ -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",

View File

@@ -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

View File

@@ -1,5 +1,3 @@
version: "3.7"
services:
postgres:
image: postgres:15

View File

@@ -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(),
}
);

View File

@@ -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

View File

@@ -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)

View File

@@ -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();

View File

@@ -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();

View File

@@ -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()),
),
)
}))

View File

@@ -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)
}

View File

@@ -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))
}

View File

@@ -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(),

View File

@@ -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> + '_ {

View File

@@ -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(),

View File

@@ -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,
}
}

View File

@@ -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(

View File

@@ -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,
}
}

View File

@@ -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"

View 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();
});
}

View File

@@ -51,7 +51,7 @@ fn main() {
kind: WindowKind::PopUp,
is_movable: false,
app_id: None,
window_min_size: Size::default(),
window_min_size: None,
}
};

View File

@@ -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,
}
}
}

View File

@@ -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()

View File

@@ -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();

View File

@@ -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());

View File

@@ -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();

View File

@@ -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:?}");

View File

@@ -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
}

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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()?)

View File

@@ -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())
}

View File

@@ -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;

View File

@@ -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| {

View File

@@ -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)?);
}

View File

@@ -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(),
})

View File

@@ -0,0 +1,3 @@
("[" @open "]" @close)
("{" @open "}" @close)
("\"" @open "\"" @close)

View 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"

View 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)

View File

@@ -0,0 +1,21 @@
(comment) @comment
(string) @string
(pair
key: (string) @property.json_key)
(number) @number
[
(true)
(false)
(null)
] @constant
[
"{"
"}"
"["
"]"
] @punctuation.bracket

View File

@@ -0,0 +1,2 @@
(array "]" @end) @indent
(object "}" @end) @indent

View File

@@ -0,0 +1,2 @@
(pair
key: (string (string_content) @name)) @item

View File

@@ -0,0 +1 @@
(string) @string

View File

@@ -0,0 +1,4 @@
(pair value: (number) @redact)
(pair value: (string) @redact)
(array (number) @redact)
(array (string) @redact)

View File

@@ -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",

View File

@@ -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",
},

View File

@@ -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,
}
}
}
}))
}

View File

@@ -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,

View File

@@ -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);

View File

@@ -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(),

View File

@@ -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,

View File

@@ -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::*;

View 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
}),
)
}
}

View File

@@ -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",
}
}
}

View File

@@ -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()

View 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())
}
}

View File

@@ -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::*;

View 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())
}))
}
}

View File

@@ -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,
}
}
}

View File

@@ -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(

View File

@@ -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>"]

View File

@@ -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),
},
}),
}
}

View File

@@ -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).

View File

@@ -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 | `⌘ + +` |