Compare commits
28 Commits
v3-editor-
...
proto_refo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a2665b940 | ||
|
|
1d2172aba8 | ||
|
|
a752bbcee8 | ||
|
|
938a0679c0 | ||
|
|
77506afd83 | ||
|
|
ecb7144b95 | ||
|
|
837756198f | ||
|
|
eb9fd62a90 | ||
|
|
3010dfe038 | ||
|
|
432de00e89 | ||
|
|
09424edc35 | ||
|
|
74cba2407f | ||
|
|
053e31994f | ||
|
|
69e698c3be | ||
|
|
215bce1974 | ||
|
|
e64a86ce9f | ||
|
|
8ae74bc6df | ||
|
|
65f6a7e5bc | ||
|
|
533416c5a9 | ||
|
|
57ad5778fa | ||
|
|
707ccb04d2 | ||
|
|
1f72069b42 | ||
|
|
ed5eb725f9 | ||
|
|
3fafdeb1a8 | ||
|
|
898d48a574 | ||
|
|
5b40debb5f | ||
|
|
e39695bf1c | ||
|
|
77df7e56f7 |
81
.github/workflows/ci.yml
vendored
81
.github/workflows/ci.yml
vendored
@@ -192,29 +192,12 @@ jobs:
|
||||
- name: Determine version and release channel
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: |
|
||||
set -eu
|
||||
# This exports RELEASE_CHANNEL into env (GITHUB_ENV)
|
||||
script/determine-release-channel
|
||||
|
||||
version=$(script/get-crate-version zed)
|
||||
channel=$(cat crates/zed/RELEASE_CHANNEL)
|
||||
echo "Publishing version: ${version} on release channel ${channel}"
|
||||
echo "RELEASE_CHANNEL=${channel}" >> $GITHUB_ENV
|
||||
|
||||
expected_tag_name=""
|
||||
case ${channel} in
|
||||
stable)
|
||||
expected_tag_name="v${version}";;
|
||||
preview)
|
||||
expected_tag_name="v${version}-pre";;
|
||||
nightly)
|
||||
expected_tag_name="v${version}-nightly";;
|
||||
*)
|
||||
echo "can't publish a release on channel ${channel}"
|
||||
exit 1;;
|
||||
esac
|
||||
if [[ $GITHUB_REF_NAME != $expected_tag_name ]]; then
|
||||
echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}"
|
||||
exit 1
|
||||
fi
|
||||
- name: Draft release notes
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: |
|
||||
mkdir -p target/
|
||||
# Ignore any errors that occur while drafting release notes to not fail the build.
|
||||
script/draft-release-notes "$version" "$channel" > target/release-notes.md || true
|
||||
@@ -271,7 +254,7 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
name: Create a Linux bundle
|
||||
runs-on:
|
||||
- buildjet-16vcpu-ubuntu-2204
|
||||
- buildjet-16vcpu-ubuntu-2004
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }}
|
||||
needs: [linux_tests]
|
||||
env:
|
||||
@@ -284,34 +267,13 @@ jobs:
|
||||
clean: false
|
||||
|
||||
- name: Install Linux dependencies
|
||||
run: ./script/linux
|
||||
run: ./script/linux && ./script/install-mold 2.34.0
|
||||
|
||||
- name: Determine version and release channel
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
version=$(script/get-crate-version zed)
|
||||
channel=$(cat crates/zed/RELEASE_CHANNEL)
|
||||
echo "Publishing version: ${version} on release channel ${channel}"
|
||||
echo "RELEASE_CHANNEL=${channel}" >> $GITHUB_ENV
|
||||
|
||||
expected_tag_name=""
|
||||
case ${channel} in
|
||||
stable)
|
||||
expected_tag_name="v${version}";;
|
||||
preview)
|
||||
expected_tag_name="v${version}-pre";;
|
||||
nightly)
|
||||
expected_tag_name="v${version}-nightly";;
|
||||
*)
|
||||
echo "can't publish a release on channel ${channel}"
|
||||
exit 1;;
|
||||
esac
|
||||
if [[ $GITHUB_REF_NAME != $expected_tag_name ]]; then
|
||||
echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}"
|
||||
exit 1
|
||||
fi
|
||||
# This exports RELEASE_CHANNEL into env (GITHUB_ENV)
|
||||
script/determine-release-channel
|
||||
|
||||
- name: Create Linux .tar.gz bundle
|
||||
run: script/bundle-linux
|
||||
@@ -357,29 +319,8 @@ jobs:
|
||||
- name: Determine version and release channel
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
run: |
|
||||
set -eu
|
||||
|
||||
version=$(script/get-crate-version zed)
|
||||
channel=$(cat crates/zed/RELEASE_CHANNEL)
|
||||
echo "Publishing version: ${version} on release channel ${channel}"
|
||||
echo "RELEASE_CHANNEL=${channel}" >> $GITHUB_ENV
|
||||
|
||||
expected_tag_name=""
|
||||
case ${channel} in
|
||||
stable)
|
||||
expected_tag_name="v${version}";;
|
||||
preview)
|
||||
expected_tag_name="v${version}-pre";;
|
||||
nightly)
|
||||
expected_tag_name="v${version}-nightly";;
|
||||
*)
|
||||
echo "can't publish a release on channel ${channel}"
|
||||
exit 1;;
|
||||
esac
|
||||
if [[ $GITHUB_REF_NAME != $expected_tag_name ]]; then
|
||||
echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}"
|
||||
exit 1
|
||||
fi
|
||||
# This exports RELEASE_CHANNEL into env (GITHUB_ENV)
|
||||
script/determine-release-channel
|
||||
|
||||
- name: Create and upload Linux .tar.gz bundle
|
||||
run: script/bundle-linux
|
||||
|
||||
6
.github/workflows/deploy_collab.yml
vendored
6
.github/workflows/deploy_collab.yml
vendored
@@ -76,7 +76,11 @@ jobs:
|
||||
clean: false
|
||||
|
||||
- name: Build docker image
|
||||
run: docker build . --build-arg GITHUB_SHA=$GITHUB_SHA --tag registry.digitalocean.com/zed/collab:$GITHUB_SHA
|
||||
run: |
|
||||
docker build -f Dockerfile-collab \
|
||||
--build-arg GITHUB_SHA=$GITHUB_SHA \
|
||||
--tag registry.digitalocean.com/zed/collab:$GITHUB_SHA \
|
||||
.
|
||||
|
||||
- name: Publish docker image
|
||||
run: docker push registry.digitalocean.com/zed/collab:${GITHUB_SHA}
|
||||
|
||||
4
.github/workflows/release_nightly.yml
vendored
4
.github/workflows/release_nightly.yml
vendored
@@ -100,7 +100,7 @@ jobs:
|
||||
name: Create a Linux *.tar.gz bundle for x86
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on:
|
||||
- buildjet-16vcpu-ubuntu-2204
|
||||
- buildjet-16vcpu-ubuntu-2004
|
||||
needs: tests
|
||||
env:
|
||||
DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }}
|
||||
@@ -117,7 +117,7 @@ jobs:
|
||||
run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Install Linux dependencies
|
||||
run: ./script/linux
|
||||
run: ./script/linux && ./script/install-mold 2.34.0
|
||||
|
||||
- name: Limit target directory size
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
"tab_size": 2,
|
||||
"formatter": "prettier"
|
||||
},
|
||||
"proto": {
|
||||
"tab_size": 2
|
||||
},
|
||||
"JSON": {
|
||||
"tab_size": 2,
|
||||
"preferred_line_length": 100,
|
||||
@@ -38,6 +41,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"file_types": {
|
||||
"Dockerfile": ["Dockerfile*[!dockerignore]"],
|
||||
"Git Ignore": ["dockerignore"]
|
||||
},
|
||||
"hard_tabs": false,
|
||||
"formatter": "auto",
|
||||
"remove_trailing_whitespace_on_save": true,
|
||||
|
||||
26
Dockerfile-distros
Normal file
26
Dockerfile-distros
Normal file
@@ -0,0 +1,26 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
ARG BASE_IMAGE
|
||||
FROM ${BASE_IMAGE}
|
||||
WORKDIR /app
|
||||
ARG TZ=Etc/UTC \
|
||||
LANG=C.UTF-8 \
|
||||
LC_ALL=C.UTF-8 \
|
||||
DEBIAN_FRONTEND=noninteractive
|
||||
ENV CARGO_TERM_COLOR=always
|
||||
|
||||
COPY script/linux script/
|
||||
RUN ./script/linux
|
||||
COPY script/install-mold script/install-cmake script/
|
||||
RUN ./script/install-mold "2.34.0"
|
||||
RUN ./script/install-cmake "3.30.4"
|
||||
|
||||
COPY . .
|
||||
|
||||
# When debugging, make these into individual RUN statements.
|
||||
# Cleanup to avoid saving big layers we aren't going to use.
|
||||
RUN . "$HOME/.cargo/env" \
|
||||
&& cargo fetch \
|
||||
&& cargo build \
|
||||
&& cargo run -- --help \
|
||||
&& cargo clean --quiet
|
||||
2
Dockerfile-distros.dockerignore
Normal file
2
Dockerfile-distros.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
||||
**/target
|
||||
**/node_modules
|
||||
@@ -599,13 +599,11 @@
|
||||
}
|
||||
},
|
||||
// Configuration for how direnv configuration should be loaded. May take 2 values:
|
||||
// 1. Load direnv configuration through the shell hook, works for POSIX shells and fish.
|
||||
// "load_direnv": "shell_hook"
|
||||
// 2. Load direnv configuration using `direnv export json` directly.
|
||||
// This can help with some shells that otherwise would not detect
|
||||
// the direnv environment, such as nushell or elvish.
|
||||
// 1. Load direnv configuration using `direnv export json` directly.
|
||||
// "load_direnv": "direct"
|
||||
"load_direnv": "shell_hook",
|
||||
// 2. Load direnv configuration through the shell hook, works for POSIX shells and fish.
|
||||
// "load_direnv": "shell_hook"
|
||||
"load_direnv": "direct",
|
||||
"inline_completions": {
|
||||
// A list of globs representing files that inline completions should be disabled for.
|
||||
"disabled_globs": [".env"]
|
||||
@@ -671,6 +669,18 @@
|
||||
// 3. Always blink the cursor, ignoring the terminal mode
|
||||
// "blinking": "on",
|
||||
"blinking": "terminal_controlled",
|
||||
// Default cursor shape for the terminal.
|
||||
// 1. A block that surrounds the following character
|
||||
// "block"
|
||||
// 2. A vertical bar
|
||||
// "bar"
|
||||
// 3. An underline that runs along the following character
|
||||
// "underscore"
|
||||
// 4. A box drawn around the following character
|
||||
// "hollow"
|
||||
//
|
||||
// Default: not set, defaults to "block"
|
||||
"cursor_shape": null,
|
||||
// Set whether Alternate Scroll mode (code: ?1007) is active by default.
|
||||
// Alternate Scroll mode converts mouse scroll events into up / down key
|
||||
// presses when in the alternate screen (e.g. when running applications
|
||||
|
||||
@@ -4409,7 +4409,7 @@ async fn test_formatting_buffer(
|
||||
file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
|
||||
vec![Formatter::External {
|
||||
command: "awk".into(),
|
||||
arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(),
|
||||
arguments: Some(vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into()),
|
||||
}]
|
||||
.into(),
|
||||
)));
|
||||
|
||||
@@ -636,11 +636,30 @@ impl EditorElement {
|
||||
cx.stop_propagation();
|
||||
} else if end_selection && pending_nonempty_selections {
|
||||
cx.stop_propagation();
|
||||
} else if cfg!(target_os = "linux")
|
||||
&& event.button == MouseButton::Middle
|
||||
&& (!text_hitbox.is_hovered(cx) || editor.read_only(cx))
|
||||
{
|
||||
return;
|
||||
} else if cfg!(target_os = "linux") && event.button == MouseButton::Middle {
|
||||
if !text_hitbox.is_hovered(cx) || editor.read_only(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
if EditorSettings::get_global(cx).middle_click_paste {
|
||||
if let Some(text) = cx.read_from_primary().and_then(|item| item.text()) {
|
||||
let point_for_position =
|
||||
position_map.point_for_position(text_hitbox.bounds, event.position);
|
||||
let position = point_for_position.previous_valid;
|
||||
|
||||
editor.select(
|
||||
SelectPhase::Begin {
|
||||
position,
|
||||
add: false,
|
||||
click_count: 1,
|
||||
},
|
||||
cx,
|
||||
);
|
||||
editor.insert(&text, cx);
|
||||
}
|
||||
cx.stop_propagation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -966,6 +985,7 @@ impl EditorElement {
|
||||
text_hitbox: &Hitbox,
|
||||
content_origin: gpui::Point<Pixels>,
|
||||
scroll_position: gpui::Point<f32>,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
line_height: Pixels,
|
||||
em_width: Pixels,
|
||||
autoscroll_containing_element: bool,
|
||||
@@ -1030,8 +1050,10 @@ impl EditorElement {
|
||||
None
|
||||
};
|
||||
|
||||
let x = cursor_character_x - scroll_position.x * em_width;
|
||||
let y = (cursor_position.row().as_f32() - scroll_position.y) * line_height;
|
||||
let x = cursor_character_x - scroll_pixel_position.x;
|
||||
let y = (cursor_position.row().as_f32()
|
||||
- scroll_pixel_position.y / line_height)
|
||||
* line_height;
|
||||
if selection.is_newest {
|
||||
editor.pixel_position_of_newest_cursor = Some(point(
|
||||
text_hitbox.origin.x + x + block_width / 2.,
|
||||
@@ -1171,7 +1193,7 @@ impl EditorElement {
|
||||
line_height: Pixels,
|
||||
gutter_dimensions: &GutterDimensions,
|
||||
gutter_settings: crate::editor_settings::Gutter,
|
||||
scroll_position: gpui::Point<f32>,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
gutter_hitbox: &Hitbox,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
@@ -3402,11 +3424,11 @@ impl EditorElement {
|
||||
};
|
||||
|
||||
let start_y = layout.gutter_hitbox.top()
|
||||
+ (start_row.0 as f32 - layout.position_map.scroll_position.y)
|
||||
* layout.position_map.line_height;
|
||||
+ start_row.0 as f32 * layout.position_map.line_height
|
||||
- layout.position_map.scroll_pixel_position.y;
|
||||
let end_y = layout.gutter_hitbox.top()
|
||||
+ ((end_row.0 + 1) as f32 - layout.position_map.scroll_position.y)
|
||||
* layout.position_map.line_height;
|
||||
+ (end_row.0 + 1) as f32 * layout.position_map.line_height
|
||||
- layout.position_map.scroll_pixel_position.y;
|
||||
let bounds = Bounds::from_corners(
|
||||
point(layout.gutter_hitbox.left(), start_y),
|
||||
point(layout.gutter_hitbox.left() + highlight_width, end_y),
|
||||
@@ -3900,9 +3922,8 @@ impl EditorElement {
|
||||
line_height: layout.position_map.line_height,
|
||||
corner_radius,
|
||||
start_y: layout.content_origin.y
|
||||
+ (row_range.start.as_f32() - layout.position_map.scroll_position.y)
|
||||
* layout.position_map.line_height,
|
||||
|
||||
+ row_range.start.as_f32() * layout.position_map.line_height
|
||||
- layout.position_map.scroll_pixel_position.y,
|
||||
lines: row_range
|
||||
.iter_rows()
|
||||
.map(|row| {
|
||||
@@ -3912,17 +3933,18 @@ impl EditorElement {
|
||||
start_x: if row == range.start.row() {
|
||||
layout.content_origin.x
|
||||
+ line_layout.x_for_index(range.start.column() as usize)
|
||||
- layout.position_map.scroll_x_offset()
|
||||
- layout.position_map.scroll_pixel_position.x
|
||||
} else {
|
||||
layout.content_origin.x - layout.position_map.scroll_x_offset()
|
||||
layout.content_origin.x
|
||||
- layout.position_map.scroll_pixel_position.x
|
||||
},
|
||||
end_x: if row == range.end.row() {
|
||||
layout.content_origin.x
|
||||
+ line_layout.x_for_index(range.end.column() as usize)
|
||||
- layout.position_map.scroll_x_offset()
|
||||
- layout.position_map.scroll_pixel_position.x
|
||||
} else {
|
||||
layout.content_origin.x + line_layout.width + line_end_overshoot
|
||||
- layout.position_map.scroll_x_offset()
|
||||
- layout.position_map.scroll_pixel_position.x
|
||||
},
|
||||
}
|
||||
})
|
||||
@@ -4575,10 +4597,11 @@ impl LineWithInvisibles {
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let line_height = layout.position_map.line_height;
|
||||
let line_y = line_height * (row.as_f32() - layout.position_map.scroll_position.y);
|
||||
let line_y = line_height
|
||||
* (row.as_f32() - layout.position_map.scroll_pixel_position.y / line_height);
|
||||
|
||||
let mut fragment_origin =
|
||||
content_origin + gpui::point(-layout.position_map.scroll_x_offset(), line_y);
|
||||
content_origin + gpui::point(-layout.position_map.scroll_pixel_position.x, line_y);
|
||||
|
||||
for fragment in &self.fragments {
|
||||
match fragment {
|
||||
@@ -4632,7 +4655,7 @@ impl LineWithInvisibles {
|
||||
(layout.position_map.em_width - invisible_symbol.width).max(Pixels::ZERO) / 2.0;
|
||||
let origin = content_origin
|
||||
+ gpui::point(
|
||||
x_offset + invisible_offset - layout.position_map.scroll_x_offset(),
|
||||
x_offset + invisible_offset - layout.position_map.scroll_pixel_position.x,
|
||||
line_y,
|
||||
);
|
||||
|
||||
@@ -5244,6 +5267,7 @@ impl Element for EditorElement {
|
||||
scroll_position = snapshot.scroll_position();
|
||||
}
|
||||
});
|
||||
|
||||
let scroll_pixel_position = point(
|
||||
scroll_position.x * em_width,
|
||||
scroll_position.y * line_height,
|
||||
@@ -5568,7 +5592,7 @@ impl Element for EditorElement {
|
||||
mode: snapshot.mode,
|
||||
position_map: Rc::new(PositionMap {
|
||||
size: bounds.size,
|
||||
scroll_position,
|
||||
scroll_pixel_position,
|
||||
scroll_max,
|
||||
line_layouts,
|
||||
line_height,
|
||||
@@ -5875,7 +5899,7 @@ struct CreaseTrailerLayout {
|
||||
struct PositionMap {
|
||||
size: Size<Pixels>,
|
||||
line_height: Pixels,
|
||||
scroll_position: gpui::Point<f32>,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
scroll_max: gpui::Point<f32>,
|
||||
em_width: Pixels,
|
||||
em_advance: Pixels,
|
||||
@@ -5939,9 +5963,6 @@ impl PositionMap {
|
||||
column_overshoot_after_line_end,
|
||||
}
|
||||
}
|
||||
fn scroll_x_offset(&self) -> Pixels {
|
||||
self.em_width * self.scroll_position.x
|
||||
}
|
||||
}
|
||||
|
||||
struct BlockLayout {
|
||||
|
||||
@@ -360,8 +360,11 @@ impl Editor {
|
||||
|
||||
h_flex()
|
||||
.id(cx.block_id)
|
||||
.w_full()
|
||||
.h(cx.line_height())
|
||||
.w_full()
|
||||
.border_t_1()
|
||||
.border_color(border_color)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
div()
|
||||
.id("gutter-strip")
|
||||
@@ -381,12 +384,10 @@ impl Editor {
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.pl_1p5()
|
||||
.pl_2()
|
||||
.pr_6()
|
||||
.size_full()
|
||||
.justify_between()
|
||||
.border_t_1()
|
||||
.border_color(border_color)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
@@ -513,7 +514,7 @@ impl Editor {
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("discard", IconName::RotateCcw)
|
||||
IconButton::new("discard", IconName::Undo)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip({
|
||||
|
||||
@@ -69,25 +69,51 @@ struct ImageShowcase {
|
||||
impl Render for ImageShowcase {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.gap_8()
|
||||
.bg(rgb(0xFFFFFF))
|
||||
.child(ImageContainer::new(
|
||||
"Image loaded from a local file",
|
||||
self.local_resource.clone(),
|
||||
))
|
||||
.child(ImageContainer::new(
|
||||
"Image loaded from a remote resource",
|
||||
self.remote_resource.clone(),
|
||||
))
|
||||
.child(ImageContainer::new(
|
||||
"Image loaded from an asset",
|
||||
self.asset_resource.clone(),
|
||||
))
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.gap_8()
|
||||
.child(ImageContainer::new(
|
||||
"Image loaded from a local file",
|
||||
self.local_resource.clone(),
|
||||
))
|
||||
.child(ImageContainer::new(
|
||||
"Image loaded from a remote resource",
|
||||
self.remote_resource.clone(),
|
||||
))
|
||||
.child(ImageContainer::new(
|
||||
"Image loaded from an asset",
|
||||
self.asset_resource.clone(),
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.gap_8()
|
||||
.child(
|
||||
div()
|
||||
.flex_col()
|
||||
.child("Auto Width")
|
||||
.child(img("https://picsum.photos/800/400").h(px(180.))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_col()
|
||||
.child("Auto Height")
|
||||
.child(img("https://picsum.photos/480/640").w(px(180.))),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
px, AbsoluteLength, AppContext, Asset, Bounds, DefiniteLength, Element, ElementId,
|
||||
GlobalElementId, Hitbox, Image, InteractiveElement, Interactivity, IntoElement, LayoutId,
|
||||
Length, ObjectFit, Pixels, RenderImage, SharedString, SharedUri, Size, StyleRefinement, Styled,
|
||||
Length, ObjectFit, Pixels, RenderImage, SharedString, SharedUri, StyleRefinement, Styled,
|
||||
SvgSize, UriOrPath, WindowContext,
|
||||
};
|
||||
use futures::{AsyncReadExt, Future};
|
||||
@@ -187,16 +187,30 @@ impl Element for Img {
|
||||
|
||||
let image_size = data.size(frame_index);
|
||||
|
||||
if let (Length::Auto, Length::Auto) = (style.size.width, style.size.height)
|
||||
{
|
||||
style.size = Size {
|
||||
width: Length::Definite(DefiniteLength::Absolute(
|
||||
AbsoluteLength::Pixels(px(image_size.width.0 as f32)),
|
||||
)),
|
||||
height: Length::Definite(DefiniteLength::Absolute(
|
||||
AbsoluteLength::Pixels(px(image_size.height.0 as f32)),
|
||||
)),
|
||||
}
|
||||
if let Length::Auto = style.size.width {
|
||||
style.size.width = match style.size.height {
|
||||
Length::Definite(DefiniteLength::Absolute(
|
||||
AbsoluteLength::Pixels(height),
|
||||
)) => Length::Definite(
|
||||
px(image_size.width.0 as f32 * height.0
|
||||
/ image_size.height.0 as f32)
|
||||
.into(),
|
||||
),
|
||||
_ => Length::Definite(px(image_size.width.0 as f32).into()),
|
||||
};
|
||||
}
|
||||
|
||||
if let Length::Auto = style.size.height {
|
||||
style.size.height = match style.size.width {
|
||||
Length::Definite(DefiniteLength::Absolute(
|
||||
AbsoluteLength::Pixels(width),
|
||||
)) => Length::Definite(
|
||||
px(image_size.height.0 as f32 * width.0
|
||||
/ image_size.width.0 as f32)
|
||||
.into(),
|
||||
),
|
||||
_ => Length::Definite(px(image_size.height.0 as f32).into()),
|
||||
};
|
||||
}
|
||||
|
||||
if global_id.is_some() && data.frame_count() > 1 {
|
||||
|
||||
@@ -2612,6 +2612,12 @@ impl From<ScaledPixels> for f64 {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ScaledPixels> for u32 {
|
||||
fn from(pixels: ScaledPixels) -> Self {
|
||||
pixels.0 as u32
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a length in rems, a unit based on the font-size of the window, which can be assigned with [`WindowContext::set_rem_size`][set_rem_size].
|
||||
///
|
||||
/// Rems are used for defining lengths that are scalable and consistent across different UI elements.
|
||||
|
||||
@@ -23,8 +23,8 @@ use crate::{
|
||||
point, Action, AnyWindowHandle, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds,
|
||||
DevicePixels, DispatchEventResult, Font, FontId, FontMetrics, FontRun, ForegroundExecutor,
|
||||
GPUSpecs, GlyphId, ImageSource, Keymap, LineLayout, Pixels, PlatformInput, Point,
|
||||
RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Scene, SharedString, Size,
|
||||
SvgSize, Task, TaskLabel, WindowContext, DEFAULT_WINDOW_SIZE,
|
||||
RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, ScaledPixels, Scene,
|
||||
SharedString, Size, SvgSize, Task, TaskLabel, WindowContext, DEFAULT_WINDOW_SIZE,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use async_task::Runnable;
|
||||
@@ -381,7 +381,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
|
||||
fn set_client_inset(&self, _inset: Pixels) {}
|
||||
fn gpu_specs(&self) -> Option<GPUSpecs>;
|
||||
|
||||
fn update_ime_position(&self, _bounds: Bounds<Pixels>);
|
||||
fn update_ime_position(&self, _bounds: Bounds<ScaledPixels>);
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
fn as_test(&mut self) -> Option<&mut TestWindow> {
|
||||
|
||||
@@ -603,17 +603,11 @@ pub(super) fn get_xkb_compose_state(cx: &xkb::Context) -> Option<xkb::compose::S
|
||||
state
|
||||
}
|
||||
|
||||
pub(super) unsafe fn read_fd(mut fd: FileDescriptor) -> Result<String> {
|
||||
pub(super) unsafe fn read_fd(mut fd: FileDescriptor) -> Result<Vec<u8>> {
|
||||
let mut file = File::from_raw_fd(fd.as_raw_fd());
|
||||
|
||||
let mut buffer = String::new();
|
||||
file.read_to_string(&mut buffer)?;
|
||||
|
||||
// Normalize the text to unix line endings, otherwise
|
||||
// copying from eg: firefox inserts a lot of blank
|
||||
// lines, and that is super annoying.
|
||||
let result = buffer.replace("\r\n", "\n");
|
||||
Ok(result)
|
||||
let mut buffer = Vec::new();
|
||||
file.read_to_end(&mut buffer)?;
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
impl CursorStyle {
|
||||
|
||||
@@ -84,7 +84,7 @@ use crate::{
|
||||
use crate::{
|
||||
AnyWindowHandle, CursorStyle, DisplayId, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers,
|
||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
|
||||
NavigationDirection, Pixels, PlatformDisplay, PlatformInput, Point, ScrollDelta,
|
||||
NavigationDirection, Pixels, PlatformDisplay, PlatformInput, Point, ScaledPixels, ScrollDelta,
|
||||
ScrollWheelEvent, TouchPhase,
|
||||
};
|
||||
use crate::{LinuxCommon, WindowParams};
|
||||
@@ -236,6 +236,7 @@ pub struct DragState {
|
||||
}
|
||||
|
||||
pub struct ClickState {
|
||||
last_mouse_button: Option<MouseButton>,
|
||||
last_click: Instant,
|
||||
last_location: Point<Pixels>,
|
||||
current_count: usize,
|
||||
@@ -313,7 +314,7 @@ impl WaylandClientStatePtr {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_ime_position(&self, bounds: Bounds<Pixels>) {
|
||||
pub fn update_ime_position(&self, bounds: Bounds<ScaledPixels>) {
|
||||
let client = self.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
if state.composing || state.text_input.is_none() || state.pre_edit_text.is_some() {
|
||||
@@ -535,6 +536,7 @@ impl WaylandClient {
|
||||
},
|
||||
click: ClickState {
|
||||
last_click: Instant::now(),
|
||||
last_mouse_button: None,
|
||||
last_location: Point::default(),
|
||||
current_count: 0,
|
||||
},
|
||||
@@ -1524,6 +1526,10 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
let click_elapsed = state.click.last_click.elapsed();
|
||||
|
||||
if click_elapsed < DOUBLE_CLICK_INTERVAL
|
||||
&& state
|
||||
.click
|
||||
.last_mouse_button
|
||||
.is_some_and(|prev_button| prev_button == button)
|
||||
&& is_within_click_distance(
|
||||
state.click.last_location,
|
||||
state.mouse_location.unwrap(),
|
||||
@@ -1535,6 +1541,7 @@ impl Dispatch<wl_pointer::WlPointer, ()> for WaylandClientStatePtr {
|
||||
}
|
||||
|
||||
state.click.last_click = Instant::now();
|
||||
state.click.last_mouse_button = Some(button);
|
||||
state.click.last_location = state.mouse_location.unwrap();
|
||||
|
||||
state.button_pressed = Some(button);
|
||||
@@ -1799,10 +1806,11 @@ impl Dispatch<wl_data_device::WlDataDevice, ()> for WaylandClientStatePtr {
|
||||
let fd = pipe.read;
|
||||
drop(pipe.write);
|
||||
|
||||
let read_task = state
|
||||
.common
|
||||
.background_executor
|
||||
.spawn(async { unsafe { read_fd(fd) } });
|
||||
let read_task = state.common.background_executor.spawn(async {
|
||||
let buffer = unsafe { read_fd(fd)? };
|
||||
let text = String::from_utf8(buffer)?;
|
||||
anyhow::Ok(text)
|
||||
});
|
||||
|
||||
let this = this.clone();
|
||||
state
|
||||
|
||||
@@ -6,10 +6,14 @@ use std::{
|
||||
|
||||
use calloop::{LoopHandle, PostAction};
|
||||
use filedescriptor::Pipe;
|
||||
use strum::IntoEnumIterator;
|
||||
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, ClipboardItem, WaylandClientStatePtr};
|
||||
use crate::{
|
||||
hash, platform::linux::platform::read_fd, ClipboardEntry, ClipboardItem, Image, ImageFormat,
|
||||
WaylandClientStatePtr,
|
||||
};
|
||||
|
||||
pub(crate) const TEXT_MIME_TYPE: &str = "text/plain;charset=utf-8";
|
||||
pub(crate) const FILE_LIST_MIME_TYPE: &str = "text/uri-list";
|
||||
@@ -33,14 +37,30 @@ pub(crate) struct Clipboard {
|
||||
current_primary_offer: Option<DataOffer<ZwpPrimarySelectionOfferV1>>,
|
||||
}
|
||||
|
||||
pub(crate) trait ReceiveData {
|
||||
fn receive_data(&self, mime_type: String, fd: BorrowedFd<'_>);
|
||||
}
|
||||
|
||||
impl ReceiveData for WlDataOffer {
|
||||
fn receive_data(&self, mime_type: String, fd: BorrowedFd<'_>) {
|
||||
self.receive(mime_type, fd);
|
||||
}
|
||||
}
|
||||
|
||||
impl ReceiveData for ZwpPrimarySelectionOfferV1 {
|
||||
fn receive_data(&self, mime_type: String, fd: BorrowedFd<'_>) {
|
||||
self.receive(mime_type, fd);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// Wrapper for `WlDataOffer` and `ZwpPrimarySelectionOfferV1`, used to help track mime types.
|
||||
pub(crate) struct DataOffer<T> {
|
||||
pub(crate) struct DataOffer<T: ReceiveData> {
|
||||
pub inner: T,
|
||||
mime_types: Vec<String>,
|
||||
}
|
||||
|
||||
impl<T> DataOffer<T> {
|
||||
impl<T: ReceiveData> DataOffer<T> {
|
||||
pub fn new(offer: T) -> Self {
|
||||
Self {
|
||||
inner: offer,
|
||||
@@ -52,17 +72,71 @@ impl<T> DataOffer<T> {
|
||||
self.mime_types.push(mime_type)
|
||||
}
|
||||
|
||||
pub fn has_mime_type(&self, mime_type: &str) -> bool {
|
||||
fn has_mime_type(&self, mime_type: &str) -> bool {
|
||||
self.mime_types.iter().any(|t| t == mime_type)
|
||||
}
|
||||
|
||||
pub fn find_text_mime_type(&self) -> Option<String> {
|
||||
for offered_mime_type in &self.mime_types {
|
||||
if let Some(offer_text_mime_type) = ALLOWED_TEXT_MIME_TYPES
|
||||
.into_iter()
|
||||
.find(|text_mime_type| text_mime_type == offered_mime_type)
|
||||
{
|
||||
return Some(offer_text_mime_type.to_owned());
|
||||
fn read_bytes(&self, connection: &Connection, mime_type: &str) -> Option<Vec<u8>> {
|
||||
let pipe = Pipe::new().unwrap();
|
||||
self.inner.receive_data(mime_type.to_string(), unsafe {
|
||||
BorrowedFd::borrow_raw(pipe.write.as_raw_fd())
|
||||
});
|
||||
let fd = pipe.read;
|
||||
drop(pipe.write);
|
||||
|
||||
connection.flush().unwrap();
|
||||
|
||||
match unsafe { read_fd(fd) } {
|
||||
Ok(bytes) => Some(bytes),
|
||||
Err(err) => {
|
||||
log::error!("error reading clipboard pipe: {err:?}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_text(&self, connection: &Connection) -> Option<ClipboardItem> {
|
||||
let mime_type = self.mime_types.iter().find(|&mime_type| {
|
||||
ALLOWED_TEXT_MIME_TYPES
|
||||
.iter()
|
||||
.any(|&allowed| allowed == mime_type)
|
||||
})?;
|
||||
let bytes = self.read_bytes(connection, mime_type)?;
|
||||
let text_content = match String::from_utf8(bytes) {
|
||||
Ok(content) => content,
|
||||
Err(e) => {
|
||||
log::error!("Failed to convert clipboard content to UTF-8: {}", e);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
// Normalize the text to unix line endings, otherwise
|
||||
// copying from eg: firefox inserts a lot of blank
|
||||
// lines, and that is super annoying.
|
||||
let result = text_content.replace("\r\n", "\n");
|
||||
Some(ClipboardItem::new_string(result))
|
||||
}
|
||||
|
||||
fn read_image(&self, connection: &Connection) -> Option<ClipboardItem> {
|
||||
for format in ImageFormat::iter() {
|
||||
let mime_type = match format {
|
||||
ImageFormat::Png => "image/png",
|
||||
ImageFormat::Jpeg => "image/jpeg",
|
||||
ImageFormat::Webp => "image/webp",
|
||||
ImageFormat::Gif => "image/gif",
|
||||
ImageFormat::Svg => "image/svg+xml",
|
||||
ImageFormat::Bmp => "image/bmp",
|
||||
ImageFormat::Tiff => "image/tiff",
|
||||
};
|
||||
if !self.has_mime_type(mime_type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(bytes) = self.read_bytes(connection, mime_type) {
|
||||
let id = hash(&bytes);
|
||||
return Some(ClipboardItem {
|
||||
entries: vec![ClipboardEntry::Image(Image { format, bytes, id })],
|
||||
});
|
||||
}
|
||||
}
|
||||
None
|
||||
@@ -128,7 +202,7 @@ impl Clipboard {
|
||||
}
|
||||
|
||||
pub fn read(&mut self) -> Option<ClipboardItem> {
|
||||
let offer = self.current_offer.clone()?;
|
||||
let offer = self.current_offer.as_ref()?;
|
||||
if let Some(cached) = self.cached_read.clone() {
|
||||
return Some(cached);
|
||||
}
|
||||
@@ -137,30 +211,16 @@ impl Clipboard {
|
||||
return self.contents.clone();
|
||||
}
|
||||
|
||||
let mime_type = offer.find_text_mime_type()?;
|
||||
let pipe = Pipe::new().unwrap();
|
||||
offer.inner.receive(mime_type, unsafe {
|
||||
BorrowedFd::borrow_raw(pipe.write.as_raw_fd())
|
||||
});
|
||||
let fd = pipe.read;
|
||||
drop(pipe.write);
|
||||
let item = offer
|
||||
.read_text(&self.connection)
|
||||
.or_else(|| offer.read_image(&self.connection))?;
|
||||
|
||||
self.connection.flush().unwrap();
|
||||
|
||||
match unsafe { read_fd(fd) } {
|
||||
Ok(v) => {
|
||||
self.cached_read = Some(ClipboardItem::new_string(v));
|
||||
self.cached_read.clone()
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("error reading clipboard pipe: {err:?}");
|
||||
None
|
||||
}
|
||||
}
|
||||
self.cached_read = Some(item.clone());
|
||||
Some(item)
|
||||
}
|
||||
|
||||
pub fn read_primary(&mut self) -> Option<ClipboardItem> {
|
||||
let offer = self.current_primary_offer.clone()?;
|
||||
let offer = self.current_primary_offer.as_ref()?;
|
||||
if let Some(cached) = self.cached_primary_read.clone() {
|
||||
return Some(cached);
|
||||
}
|
||||
@@ -169,26 +229,12 @@ impl Clipboard {
|
||||
return self.primary_contents.clone();
|
||||
}
|
||||
|
||||
let mime_type = offer.find_text_mime_type()?;
|
||||
let pipe = Pipe::new().unwrap();
|
||||
offer.inner.receive(mime_type, unsafe {
|
||||
BorrowedFd::borrow_raw(pipe.write.as_raw_fd())
|
||||
});
|
||||
let fd = pipe.read;
|
||||
drop(pipe.write);
|
||||
let item = offer
|
||||
.read_text(&self.connection)
|
||||
.or_else(|| offer.read_image(&self.connection))?;
|
||||
|
||||
self.connection.flush().unwrap();
|
||||
|
||||
match unsafe { read_fd(fd) } {
|
||||
Ok(v) => {
|
||||
self.cached_primary_read = Some(ClipboardItem::new_string(v.clone()));
|
||||
self.cached_primary_read.clone()
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("error reading clipboard pipe: {err:?}");
|
||||
None
|
||||
}
|
||||
}
|
||||
self.cached_primary_read = Some(item.clone());
|
||||
Some(item)
|
||||
}
|
||||
|
||||
fn send_internal(&self, fd: OwnedFd, bytes: Vec<u8>) {
|
||||
|
||||
@@ -26,7 +26,7 @@ use crate::platform::{PlatformAtlas, PlatformInputHandler, PlatformWindow};
|
||||
use crate::scene::Scene;
|
||||
use crate::{
|
||||
px, size, AnyWindowHandle, Bounds, Decorations, GPUSpecs, Globals, Modifiers, Output, Pixels,
|
||||
PlatformDisplay, PlatformInput, Point, PromptLevel, ResizeEdge, Size, Tiling,
|
||||
PlatformDisplay, PlatformInput, Point, PromptLevel, ResizeEdge, ScaledPixels, Size, Tiling,
|
||||
WaylandClientStatePtr, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
|
||||
WindowControls, WindowDecorations, WindowParams,
|
||||
};
|
||||
@@ -1010,7 +1010,7 @@ impl PlatformWindow for WaylandWindow {
|
||||
}
|
||||
}
|
||||
|
||||
fn update_ime_position(&self, bounds: Bounds<Pixels>) {
|
||||
fn update_ime_position(&self, bounds: Bounds<ScaledPixels>) {
|
||||
let state = self.borrow();
|
||||
state.client.update_ime_position(bounds);
|
||||
}
|
||||
@@ -1046,8 +1046,8 @@ fn update_window(mut state: RefMut<WaylandWindowState>) {
|
||||
&& state.decorations == WindowDecorations::Server
|
||||
{
|
||||
// Promise the compositor that this region of the window surface
|
||||
// contains no transparent pixels. This allows the compositor to
|
||||
// do skip whatever is behind the surface for better performance.
|
||||
// contains no transparent pixels. This allows the compositor to skip
|
||||
// updating whatever is behind the surface for better performance.
|
||||
state.surface.set_opaque_region(Some(®ion));
|
||||
} else {
|
||||
state.surface.set_opaque_region(None);
|
||||
@@ -1057,7 +1057,6 @@ fn update_window(mut state: RefMut<WaylandWindowState>) {
|
||||
if state.background_appearance == WindowBackgroundAppearance::Blurred {
|
||||
if state.blur.is_none() {
|
||||
let blur = blur_manager.create(&state.surface, &state.globals.qh, ());
|
||||
blur.set_region(Some(®ion));
|
||||
state.blur = Some(blur);
|
||||
}
|
||||
state.blur.as_ref().unwrap().commit();
|
||||
|
||||
@@ -37,8 +37,9 @@ use crate::platform::linux::LinuxClient;
|
||||
use crate::platform::{LinuxCommon, PlatformWindow};
|
||||
use crate::{
|
||||
modifiers_from_xinput_info, point, px, AnyWindowHandle, Bounds, ClipboardItem, CursorStyle,
|
||||
DisplayId, FileDropEvent, Keystroke, Modifiers, ModifiersChangedEvent, Pixels, Platform,
|
||||
PlatformDisplay, PlatformInput, Point, ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
|
||||
DisplayId, FileDropEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, Pixels,
|
||||
Platform, PlatformDisplay, PlatformInput, Point, ScaledPixels, ScrollDelta, Size, TouchPhase,
|
||||
WindowParams, X11Window,
|
||||
};
|
||||
|
||||
use super::{button_of_key, modifiers_from_state, pressed_button_from_mask};
|
||||
@@ -121,6 +122,7 @@ pub struct X11ClientState {
|
||||
pub(crate) event_loop: Option<calloop::EventLoop<'static, X11Client>>,
|
||||
|
||||
pub(crate) last_click: Instant,
|
||||
pub(crate) last_mouse_button: Option<MouseButton>,
|
||||
pub(crate) last_location: Point<Pixels>,
|
||||
pub(crate) current_count: usize,
|
||||
|
||||
@@ -188,7 +190,7 @@ impl X11ClientStatePtr {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_ime_position(&self, bounds: Bounds<Pixels>) {
|
||||
pub fn update_ime_position(&self, bounds: Bounds<ScaledPixels>) {
|
||||
let client = self.get_client();
|
||||
let mut state = client.0.borrow_mut();
|
||||
if state.composing || state.ximc.is_none() {
|
||||
@@ -403,6 +405,7 @@ impl X11Client {
|
||||
loop_handle: handle,
|
||||
common,
|
||||
last_click: Instant::now(),
|
||||
last_mouse_button: None,
|
||||
last_location: Point::new(px(0.0), px(0.0)),
|
||||
current_count: 0,
|
||||
scale_factor,
|
||||
@@ -951,6 +954,9 @@ impl X11Client {
|
||||
let click_elapsed = state.last_click.elapsed();
|
||||
|
||||
if click_elapsed < DOUBLE_CLICK_INTERVAL
|
||||
&& state
|
||||
.last_mouse_button
|
||||
.is_some_and(|prev_button| prev_button == button)
|
||||
&& is_within_click_distance(state.last_location, position)
|
||||
{
|
||||
state.current_count += 1;
|
||||
@@ -959,6 +965,7 @@ impl X11Client {
|
||||
}
|
||||
|
||||
state.last_click = Instant::now();
|
||||
state.last_mouse_button = Some(button);
|
||||
state.last_location = position;
|
||||
let current_count = state.current_count;
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@ use crate::{
|
||||
platform::blade::{BladeRenderer, BladeSurfaceConfig},
|
||||
px, size, AnyWindowHandle, Bounds, Decorations, DevicePixels, ForegroundExecutor, GPUSpecs,
|
||||
Modifiers, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler,
|
||||
PlatformWindow, Point, PromptLevel, ResizeEdge, Scene, Size, Tiling, WindowAppearance,
|
||||
WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind, WindowParams,
|
||||
X11ClientStatePtr,
|
||||
PlatformWindow, Point, PromptLevel, ResizeEdge, ScaledPixels, Scene, Size, Tiling,
|
||||
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowDecorations, WindowKind,
|
||||
WindowParams, X11ClientStatePtr,
|
||||
};
|
||||
|
||||
use blade_graphics as gpu;
|
||||
@@ -1412,7 +1412,7 @@ impl PlatformWindow for X11Window {
|
||||
}
|
||||
}
|
||||
|
||||
fn update_ime_position(&self, bounds: Bounds<Pixels>) {
|
||||
fn update_ime_position(&self, bounds: Bounds<ScaledPixels>) {
|
||||
let mut state = self.0.state.borrow_mut();
|
||||
let client = state.client.clone();
|
||||
drop(state);
|
||||
|
||||
@@ -3,8 +3,9 @@ use crate::{
|
||||
platform::PlatformInputHandler, point, px, size, AnyWindowHandle, Bounds, DisplayLink,
|
||||
ExternalPaths, FileDropEvent, ForegroundExecutor, KeyDownEvent, Keystroke, Modifiers,
|
||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
|
||||
PlatformAtlas, PlatformDisplay, PlatformInput, PlatformWindow, Point, PromptLevel, Size, Timer,
|
||||
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowKind, WindowParams,
|
||||
PlatformAtlas, PlatformDisplay, PlatformInput, PlatformWindow, Point, PromptLevel,
|
||||
ScaledPixels, Size, Timer, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
|
||||
WindowKind, WindowParams,
|
||||
};
|
||||
use block::ConcreteBlock;
|
||||
use cocoa::{
|
||||
@@ -1119,7 +1120,7 @@ impl PlatformWindow for MacWindow {
|
||||
None
|
||||
}
|
||||
|
||||
fn update_ime_position(&self, _bounds: Bounds<Pixels>) {
|
||||
fn update_ime_position(&self, _bounds: Bounds<ScaledPixels>) {
|
||||
unsafe {
|
||||
let input_context: id = msg_send![class!(NSTextInputContext), currentInputContext];
|
||||
let _: () = msg_send![input_context, invalidateCharacterCoordinates];
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::{
|
||||
AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, DispatchEventResult, GPUSpecs,
|
||||
Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow,
|
||||
Point, Size, TestPlatform, TileId, WindowAppearance, WindowBackgroundAppearance, WindowBounds,
|
||||
WindowParams,
|
||||
Point, ScaledPixels, Size, TestPlatform, TileId, WindowAppearance, WindowBackgroundAppearance,
|
||||
WindowBounds, WindowParams,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use parking_lot::Mutex;
|
||||
@@ -274,7 +274,7 @@ impl PlatformWindow for TestWindow {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn update_ime_position(&self, _bounds: Bounds<Pixels>) {}
|
||||
fn update_ime_position(&self, _bounds: Bounds<ScaledPixels>) {}
|
||||
|
||||
fn gpu_specs(&self) -> Option<GPUSpecs> {
|
||||
None
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod clipboard;
|
||||
mod direct_write;
|
||||
mod dispatcher;
|
||||
mod display;
|
||||
@@ -8,6 +9,7 @@ mod util;
|
||||
mod window;
|
||||
mod wrapper;
|
||||
|
||||
pub(crate) use clipboard::*;
|
||||
pub(crate) use direct_write::*;
|
||||
pub(crate) use dispatcher::*;
|
||||
pub(crate) use display::*;
|
||||
|
||||
366
crates/gpui/src/platform/windows/clipboard.rs
Normal file
366
crates/gpui/src/platform/windows/clipboard.rs
Normal file
@@ -0,0 +1,366 @@
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use anyhow::Result;
|
||||
use collections::{FxHashMap, FxHashSet};
|
||||
use itertools::Itertools;
|
||||
use util::ResultExt;
|
||||
use windows::Win32::{
|
||||
Foundation::HANDLE,
|
||||
System::{
|
||||
DataExchange::{
|
||||
CloseClipboard, CountClipboardFormats, EmptyClipboard, EnumClipboardFormats,
|
||||
GetClipboardData, GetClipboardFormatNameW, IsClipboardFormatAvailable, OpenClipboard,
|
||||
RegisterClipboardFormatW, SetClipboardData,
|
||||
},
|
||||
Memory::{GlobalAlloc, GlobalLock, GlobalUnlock, GMEM_MOVEABLE},
|
||||
Ole::{CF_HDROP, CF_UNICODETEXT},
|
||||
},
|
||||
UI::Shell::{DragQueryFileW, HDROP},
|
||||
};
|
||||
use windows_core::PCWSTR;
|
||||
|
||||
use crate::{
|
||||
hash, ClipboardEntry, ClipboardItem, ClipboardString, Image, ImageFormat, SmartGlobal,
|
||||
};
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-dragqueryfilew
|
||||
const DRAGDROP_GET_FILES_COUNT: u32 = 0xFFFFFFFF;
|
||||
|
||||
// Clipboard formats
|
||||
static CLIPBOARD_HASH_FORMAT: LazyLock<u32> =
|
||||
LazyLock::new(|| register_clipboard_format(windows::core::w!("GPUI internal text hash")));
|
||||
static CLIPBOARD_METADATA_FORMAT: LazyLock<u32> =
|
||||
LazyLock::new(|| register_clipboard_format(windows::core::w!("GPUI internal metadata")));
|
||||
static CLIPBOARD_SVG_FORMAT: LazyLock<u32> =
|
||||
LazyLock::new(|| register_clipboard_format(windows::core::w!("image/svg+xml")));
|
||||
static CLIPBOARD_GIF_FORMAT: LazyLock<u32> =
|
||||
LazyLock::new(|| register_clipboard_format(windows::core::w!("GIF")));
|
||||
static CLIPBOARD_PNG_FORMAT: LazyLock<u32> =
|
||||
LazyLock::new(|| register_clipboard_format(windows::core::w!("PNG")));
|
||||
static CLIPBOARD_JPG_FORMAT: LazyLock<u32> =
|
||||
LazyLock::new(|| register_clipboard_format(windows::core::w!("JFIF")));
|
||||
|
||||
// Helper maps and sets
|
||||
static FORMATS_MAP: LazyLock<FxHashMap<u32, ClipboardFormatType>> = LazyLock::new(|| {
|
||||
let mut formats_map = FxHashMap::default();
|
||||
formats_map.insert(CF_UNICODETEXT.0 as u32, ClipboardFormatType::Text);
|
||||
formats_map.insert(*CLIPBOARD_PNG_FORMAT, ClipboardFormatType::Image);
|
||||
formats_map.insert(*CLIPBOARD_GIF_FORMAT, ClipboardFormatType::Image);
|
||||
formats_map.insert(*CLIPBOARD_JPG_FORMAT, ClipboardFormatType::Image);
|
||||
formats_map.insert(*CLIPBOARD_SVG_FORMAT, ClipboardFormatType::Image);
|
||||
formats_map.insert(CF_HDROP.0 as u32, ClipboardFormatType::Files);
|
||||
formats_map
|
||||
});
|
||||
static FORMATS_SET: LazyLock<FxHashSet<u32>> = LazyLock::new(|| {
|
||||
let mut formats_map = FxHashSet::default();
|
||||
formats_map.insert(CF_UNICODETEXT.0 as u32);
|
||||
formats_map.insert(*CLIPBOARD_PNG_FORMAT);
|
||||
formats_map.insert(*CLIPBOARD_GIF_FORMAT);
|
||||
formats_map.insert(*CLIPBOARD_JPG_FORMAT);
|
||||
formats_map.insert(*CLIPBOARD_SVG_FORMAT);
|
||||
formats_map.insert(CF_HDROP.0 as u32);
|
||||
formats_map
|
||||
});
|
||||
static IMAGE_FORMATS_MAP: LazyLock<FxHashMap<u32, ImageFormat>> = LazyLock::new(|| {
|
||||
let mut formats_map = FxHashMap::default();
|
||||
formats_map.insert(*CLIPBOARD_PNG_FORMAT, ImageFormat::Png);
|
||||
formats_map.insert(*CLIPBOARD_GIF_FORMAT, ImageFormat::Gif);
|
||||
formats_map.insert(*CLIPBOARD_JPG_FORMAT, ImageFormat::Jpeg);
|
||||
formats_map.insert(*CLIPBOARD_SVG_FORMAT, ImageFormat::Svg);
|
||||
formats_map
|
||||
});
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum ClipboardFormatType {
|
||||
Text,
|
||||
Image,
|
||||
Files,
|
||||
}
|
||||
|
||||
pub(crate) fn write_to_clipboard(item: ClipboardItem) {
|
||||
write_to_clipboard_inner(item).log_err();
|
||||
unsafe { CloseClipboard().log_err() };
|
||||
}
|
||||
|
||||
pub(crate) fn read_from_clipboard() -> Option<ClipboardItem> {
|
||||
let result = read_from_clipboard_inner();
|
||||
unsafe { CloseClipboard().log_err() };
|
||||
result
|
||||
}
|
||||
|
||||
pub(crate) fn with_file_names<F>(hdrop: HDROP, mut f: F)
|
||||
where
|
||||
F: FnMut(String),
|
||||
{
|
||||
let file_count = unsafe { DragQueryFileW(hdrop, DRAGDROP_GET_FILES_COUNT, None) };
|
||||
for file_index in 0..file_count {
|
||||
let filename_length = unsafe { DragQueryFileW(hdrop, file_index, None) } as usize;
|
||||
let mut buffer = vec![0u16; filename_length + 1];
|
||||
let ret = unsafe { DragQueryFileW(hdrop, file_index, Some(buffer.as_mut_slice())) };
|
||||
if ret == 0 {
|
||||
log::error!("unable to read file name");
|
||||
continue;
|
||||
}
|
||||
if let Some(file_name) = String::from_utf16(&buffer[0..filename_length]).log_err() {
|
||||
f(file_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn register_clipboard_format(format: PCWSTR) -> u32 {
|
||||
let ret = unsafe { RegisterClipboardFormatW(format) };
|
||||
if ret == 0 {
|
||||
panic!(
|
||||
"Error when registering clipboard format: {}",
|
||||
std::io::Error::last_os_error()
|
||||
);
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn format_to_type(item_format: u32) -> &'static ClipboardFormatType {
|
||||
FORMATS_MAP.get(&item_format).unwrap()
|
||||
}
|
||||
|
||||
// Currently, we only write the first item.
|
||||
fn write_to_clipboard_inner(item: ClipboardItem) -> Result<()> {
|
||||
unsafe {
|
||||
OpenClipboard(None)?;
|
||||
EmptyClipboard()?;
|
||||
}
|
||||
match item.entries().first() {
|
||||
Some(entry) => match entry {
|
||||
ClipboardEntry::String(string) => {
|
||||
write_string_to_clipboard(string)?;
|
||||
}
|
||||
ClipboardEntry::Image(image) => {
|
||||
write_image_to_clipboard(image)?;
|
||||
}
|
||||
},
|
||||
None => {
|
||||
// Writing an empty list of entries just clears the clipboard.
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_string_to_clipboard(item: &ClipboardString) -> Result<()> {
|
||||
let encode_wide = item.text.encode_utf16().chain(Some(0)).collect_vec();
|
||||
set_data_to_clipboard(&encode_wide, CF_UNICODETEXT.0 as u32)?;
|
||||
|
||||
if let Some(metadata) = item.metadata.as_ref() {
|
||||
let hash_result = {
|
||||
let hash = ClipboardString::text_hash(&item.text);
|
||||
hash.to_ne_bytes()
|
||||
};
|
||||
let encode_wide =
|
||||
unsafe { std::slice::from_raw_parts(hash_result.as_ptr().cast::<u16>(), 4) };
|
||||
set_data_to_clipboard(encode_wide, *CLIPBOARD_HASH_FORMAT)?;
|
||||
|
||||
let metadata_wide = metadata.encode_utf16().chain(Some(0)).collect_vec();
|
||||
set_data_to_clipboard(&metadata_wide, *CLIPBOARD_METADATA_FORMAT)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_data_to_clipboard<T>(data: &[T], format: u32) -> Result<()> {
|
||||
unsafe {
|
||||
let global = GlobalAlloc(GMEM_MOVEABLE, std::mem::size_of_val(data))?;
|
||||
let handle = GlobalLock(global);
|
||||
std::ptr::copy_nonoverlapping(data.as_ptr(), handle as _, data.len());
|
||||
let _ = GlobalUnlock(global);
|
||||
SetClipboardData(format, HANDLE(global.0))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Here writing PNG to the clipboard to better support other apps. For more info, please ref to
|
||||
// the PR.
|
||||
fn write_image_to_clipboard(item: &Image) -> Result<()> {
|
||||
match item.format {
|
||||
ImageFormat::Svg => set_data_to_clipboard(item.bytes(), *CLIPBOARD_SVG_FORMAT)?,
|
||||
ImageFormat::Gif => {
|
||||
set_data_to_clipboard(item.bytes(), *CLIPBOARD_GIF_FORMAT)?;
|
||||
let png_bytes = convert_image_to_png_format(item.bytes(), ImageFormat::Gif)?;
|
||||
set_data_to_clipboard(&png_bytes, *CLIPBOARD_PNG_FORMAT)?;
|
||||
}
|
||||
ImageFormat::Png => {
|
||||
set_data_to_clipboard(item.bytes(), *CLIPBOARD_PNG_FORMAT)?;
|
||||
let png_bytes = convert_image_to_png_format(item.bytes(), ImageFormat::Png)?;
|
||||
set_data_to_clipboard(&png_bytes, *CLIPBOARD_PNG_FORMAT)?;
|
||||
}
|
||||
ImageFormat::Jpeg => {
|
||||
set_data_to_clipboard(item.bytes(), *CLIPBOARD_JPG_FORMAT)?;
|
||||
let png_bytes = convert_image_to_png_format(item.bytes(), ImageFormat::Jpeg)?;
|
||||
set_data_to_clipboard(&png_bytes, *CLIPBOARD_PNG_FORMAT)?;
|
||||
}
|
||||
other => {
|
||||
log::warn!(
|
||||
"Clipboard unsupported image format: {:?}, convert to PNG instead.",
|
||||
item.format
|
||||
);
|
||||
let png_bytes = convert_image_to_png_format(item.bytes(), other)?;
|
||||
set_data_to_clipboard(&png_bytes, *CLIPBOARD_PNG_FORMAT)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn convert_image_to_png_format(bytes: &[u8], image_format: ImageFormat) -> Result<Vec<u8>> {
|
||||
let image = image::load_from_memory_with_format(bytes, image_format.into())?;
|
||||
let mut output_buf = Vec::new();
|
||||
image.write_to(
|
||||
&mut std::io::Cursor::new(&mut output_buf),
|
||||
image::ImageFormat::Png,
|
||||
)?;
|
||||
Ok(output_buf)
|
||||
}
|
||||
|
||||
fn read_from_clipboard_inner() -> Option<ClipboardItem> {
|
||||
unsafe { OpenClipboard(None) }.log_err()?;
|
||||
with_best_match_format(|item_format| match format_to_type(item_format) {
|
||||
ClipboardFormatType::Text => read_string_from_clipboard(),
|
||||
ClipboardFormatType::Image => read_image_from_clipboard(item_format),
|
||||
ClipboardFormatType::Files => read_files_from_clipboard(),
|
||||
})
|
||||
}
|
||||
|
||||
// Here, we enumerate all formats on the clipboard and find the first one that we can process.
|
||||
// The reason we don't use `GetPriorityClipboardFormat` is that it sometimes returns the
|
||||
// wrong format.
|
||||
// For instance, when copying a JPEG image from Microsoft Word, there may be several formats
|
||||
// on the clipboard: Jpeg, Png, Svg.
|
||||
// If we use `GetPriorityClipboardFormat`, it will return Svg, which is not what we want.
|
||||
fn with_best_match_format<F>(f: F) -> Option<ClipboardItem>
|
||||
where
|
||||
F: Fn(u32) -> Option<ClipboardEntry>,
|
||||
{
|
||||
let count = unsafe { CountClipboardFormats() };
|
||||
let mut clipboard_format = 0;
|
||||
for _ in 0..count {
|
||||
clipboard_format = unsafe { EnumClipboardFormats(clipboard_format) };
|
||||
let Some(item_format) = FORMATS_SET.get(&clipboard_format) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(entry) = f(*item_format) {
|
||||
return Some(ClipboardItem {
|
||||
entries: vec![entry],
|
||||
});
|
||||
}
|
||||
}
|
||||
// log the formats that we don't support yet.
|
||||
{
|
||||
clipboard_format = 0;
|
||||
for _ in 0..count {
|
||||
clipboard_format = unsafe { EnumClipboardFormats(clipboard_format) };
|
||||
let mut buffer = [0u16; 64];
|
||||
unsafe { GetClipboardFormatNameW(clipboard_format, &mut buffer) };
|
||||
let format_name = String::from_utf16_lossy(&buffer);
|
||||
log::warn!(
|
||||
"Try to paste with unsupported clipboard format: {}, {}.",
|
||||
clipboard_format,
|
||||
format_name
|
||||
);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn read_string_from_clipboard() -> Option<ClipboardEntry> {
|
||||
let text = {
|
||||
let global = SmartGlobal::from_raw_ptr(
|
||||
unsafe { GetClipboardData(CF_UNICODETEXT.0 as u32).log_err() }?.0,
|
||||
);
|
||||
let text = PCWSTR(global.lock() as *const u16);
|
||||
String::from_utf16_lossy(unsafe { text.as_wide() })
|
||||
};
|
||||
let Some(hash) = read_hash_from_clipboard() else {
|
||||
return Some(ClipboardEntry::String(ClipboardString::new(text)));
|
||||
};
|
||||
let Some(metadata) = read_metadata_from_clipboard() else {
|
||||
return Some(ClipboardEntry::String(ClipboardString::new(text)));
|
||||
};
|
||||
if hash == ClipboardString::text_hash(&text) {
|
||||
Some(ClipboardEntry::String(ClipboardString {
|
||||
text,
|
||||
metadata: Some(metadata),
|
||||
}))
|
||||
} else {
|
||||
Some(ClipboardEntry::String(ClipboardString::new(text)))
|
||||
}
|
||||
}
|
||||
|
||||
fn read_hash_from_clipboard() -> Option<u64> {
|
||||
if unsafe { IsClipboardFormatAvailable(*CLIPBOARD_HASH_FORMAT).is_err() } {
|
||||
return None;
|
||||
}
|
||||
let global =
|
||||
SmartGlobal::from_raw_ptr(unsafe { GetClipboardData(*CLIPBOARD_HASH_FORMAT).log_err() }?.0);
|
||||
let raw_ptr = global.lock() as *const u16;
|
||||
let hash_bytes: [u8; 8] = unsafe {
|
||||
std::slice::from_raw_parts(raw_ptr.cast::<u8>(), 8)
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.log_err()
|
||||
}?;
|
||||
Some(u64::from_ne_bytes(hash_bytes))
|
||||
}
|
||||
|
||||
fn read_metadata_from_clipboard() -> Option<String> {
|
||||
unsafe { IsClipboardFormatAvailable(*CLIPBOARD_METADATA_FORMAT).log_err()? };
|
||||
let global = SmartGlobal::from_raw_ptr(
|
||||
unsafe { GetClipboardData(*CLIPBOARD_METADATA_FORMAT).log_err() }?.0,
|
||||
);
|
||||
let text = PCWSTR(global.lock() as *const u16);
|
||||
Some(String::from_utf16_lossy(unsafe { text.as_wide() }))
|
||||
}
|
||||
|
||||
fn read_image_from_clipboard(format: u32) -> Option<ClipboardEntry> {
|
||||
let image_format = format_number_to_image_format(format)?;
|
||||
read_image_for_type(format, *image_format)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn format_number_to_image_format(format_number: u32) -> Option<&'static ImageFormat> {
|
||||
IMAGE_FORMATS_MAP.get(&format_number)
|
||||
}
|
||||
|
||||
fn read_image_for_type(format_number: u32, format: ImageFormat) -> Option<ClipboardEntry> {
|
||||
let global = SmartGlobal::from_raw_ptr(unsafe { GetClipboardData(format_number).log_err() }?.0);
|
||||
let image_ptr = global.lock();
|
||||
let iamge_size = global.size();
|
||||
let bytes =
|
||||
unsafe { std::slice::from_raw_parts(image_ptr as *mut u8 as _, iamge_size).to_vec() };
|
||||
let id = hash(&bytes);
|
||||
Some(ClipboardEntry::Image(Image { format, bytes, id }))
|
||||
}
|
||||
|
||||
fn read_files_from_clipboard() -> Option<ClipboardEntry> {
|
||||
let global =
|
||||
SmartGlobal::from_raw_ptr(unsafe { GetClipboardData(CF_HDROP.0 as u32).log_err() }?.0);
|
||||
let hdrop = HDROP(global.lock());
|
||||
let mut filenames = String::new();
|
||||
with_file_names(hdrop, |file_name| {
|
||||
filenames.push_str(&file_name);
|
||||
});
|
||||
Some(ClipboardEntry::String(ClipboardString {
|
||||
text: filenames,
|
||||
metadata: None,
|
||||
}))
|
||||
}
|
||||
|
||||
impl From<ImageFormat> for image::ImageFormat {
|
||||
fn from(value: ImageFormat) -> Self {
|
||||
match value {
|
||||
ImageFormat::Png => image::ImageFormat::Png,
|
||||
ImageFormat::Jpeg => image::ImageFormat::Jpeg,
|
||||
ImageFormat::Webp => image::ImageFormat::WebP,
|
||||
ImageFormat::Gif => image::ImageFormat::Gif,
|
||||
// ImageFormat::Svg => todo!(),
|
||||
ImageFormat::Bmp => image::ImageFormat::Bmp,
|
||||
ImageFormat::Tiff => image::ImageFormat::Tiff,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,24 +17,12 @@ use windows::{
|
||||
core::*,
|
||||
Win32::{
|
||||
Foundation::*,
|
||||
Globalization::u_memcpy,
|
||||
Graphics::{
|
||||
Gdi::*,
|
||||
Imaging::{CLSID_WICImagingFactory, IWICImagingFactory},
|
||||
},
|
||||
Security::Credentials::*,
|
||||
System::{
|
||||
Com::*,
|
||||
DataExchange::{
|
||||
CloseClipboard, EmptyClipboard, GetClipboardData, OpenClipboard,
|
||||
RegisterClipboardFormatW, SetClipboardData,
|
||||
},
|
||||
LibraryLoader::*,
|
||||
Memory::{GlobalAlloc, GlobalLock, GlobalUnlock, GMEM_MOVEABLE},
|
||||
Ole::*,
|
||||
SystemInformation::*,
|
||||
Threading::*,
|
||||
},
|
||||
System::{Com::*, LibraryLoader::*, Ole::*, SystemInformation::*, Threading::*},
|
||||
UI::{Input::KeyboardAndMouse::*, Shell::*, WindowsAndMessaging::*},
|
||||
},
|
||||
UI::ViewManagement::UISettings,
|
||||
@@ -52,8 +40,6 @@ pub(crate) struct WindowsPlatform {
|
||||
background_executor: BackgroundExecutor,
|
||||
foreground_executor: ForegroundExecutor,
|
||||
text_system: Arc<DirectWriteTextSystem>,
|
||||
clipboard_hash_format: u32,
|
||||
clipboard_metadata_format: u32,
|
||||
windows_version: WindowsVersion,
|
||||
bitmap_factory: ManuallyDrop<IWICImagingFactory>,
|
||||
validation_number: usize,
|
||||
@@ -108,9 +94,6 @@ impl WindowsPlatform {
|
||||
let icon = load_icon().unwrap_or_default();
|
||||
let state = RefCell::new(WindowsPlatformState::new());
|
||||
let raw_window_handles = RwLock::new(SmallVec::new());
|
||||
let clipboard_hash_format = register_clipboard_format(CLIPBOARD_HASH_FORMAT).unwrap();
|
||||
let clipboard_metadata_format =
|
||||
register_clipboard_format(CLIPBOARD_METADATA_FORMAT).unwrap();
|
||||
let windows_version = WindowsVersion::new().expect("Error retrieve windows version");
|
||||
let validation_number = rand::random::<usize>();
|
||||
|
||||
@@ -123,8 +106,6 @@ impl WindowsPlatform {
|
||||
background_executor,
|
||||
foreground_executor,
|
||||
text_system,
|
||||
clipboard_hash_format,
|
||||
clipboard_metadata_format,
|
||||
windows_version,
|
||||
bitmap_factory,
|
||||
validation_number,
|
||||
@@ -487,15 +468,11 @@ impl Platform for WindowsPlatform {
|
||||
}
|
||||
|
||||
fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||
write_to_clipboard(
|
||||
item,
|
||||
self.clipboard_hash_format,
|
||||
self.clipboard_metadata_format,
|
||||
);
|
||||
write_to_clipboard(item);
|
||||
}
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
read_from_clipboard(self.clipboard_hash_format, self.clipboard_metadata_format)
|
||||
read_from_clipboard()
|
||||
}
|
||||
|
||||
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>> {
|
||||
@@ -664,10 +641,11 @@ fn file_save_dialog(directory: PathBuf) -> Result<Option<PathBuf>> {
|
||||
let dialog: IFileSaveDialog = unsafe { CoCreateInstance(&FileSaveDialog, None, CLSCTX_ALL)? };
|
||||
if !directory.to_string_lossy().is_empty() {
|
||||
if let Some(full_path) = directory.canonicalize().log_err() {
|
||||
let full_path = full_path.to_string_lossy().to_string();
|
||||
if !full_path.is_empty() {
|
||||
let full_path = full_path.to_string_lossy();
|
||||
let full_path_str = full_path.trim_start_matches("\\\\?\\");
|
||||
if !full_path_str.is_empty() {
|
||||
let path_item: IShellItem =
|
||||
unsafe { SHCreateItemFromParsingName(&HSTRING::from(&full_path), None)? };
|
||||
unsafe { SHCreateItemFromParsingName(&HSTRING::from(full_path_str), None)? };
|
||||
unsafe { dialog.SetFolder(&path_item).log_err() };
|
||||
}
|
||||
}
|
||||
@@ -724,117 +702,6 @@ fn should_auto_hide_scrollbars() -> Result<bool> {
|
||||
Ok(ui_settings.AutoHideScrollBars()?)
|
||||
}
|
||||
|
||||
fn register_clipboard_format(format: PCWSTR) -> Result<u32> {
|
||||
let ret = unsafe { RegisterClipboardFormatW(format) };
|
||||
if ret == 0 {
|
||||
Err(anyhow::anyhow!(
|
||||
"Error when registering clipboard format: {}",
|
||||
std::io::Error::last_os_error()
|
||||
))
|
||||
} else {
|
||||
Ok(ret)
|
||||
}
|
||||
}
|
||||
|
||||
fn write_to_clipboard(item: ClipboardItem, hash_format: u32, metadata_format: u32) {
|
||||
write_to_clipboard_inner(item, hash_format, metadata_format).log_err();
|
||||
unsafe { CloseClipboard().log_err() };
|
||||
}
|
||||
|
||||
fn write_to_clipboard_inner(
|
||||
item: ClipboardItem,
|
||||
hash_format: u32,
|
||||
metadata_format: u32,
|
||||
) -> Result<()> {
|
||||
unsafe {
|
||||
OpenClipboard(None)?;
|
||||
EmptyClipboard()?;
|
||||
let encode_wide = item
|
||||
.text()
|
||||
.unwrap_or_default()
|
||||
.encode_utf16()
|
||||
.chain(Some(0))
|
||||
.collect_vec();
|
||||
set_data_to_clipboard(&encode_wide, CF_UNICODETEXT.0 as u32)?;
|
||||
|
||||
if let Some((metadata, text)) = item.metadata().zip(item.text()) {
|
||||
let hash_result = {
|
||||
let hash = ClipboardString::text_hash(&text);
|
||||
hash.to_ne_bytes()
|
||||
};
|
||||
let encode_wide = std::slice::from_raw_parts(hash_result.as_ptr().cast::<u16>(), 4);
|
||||
set_data_to_clipboard(encode_wide, hash_format)?;
|
||||
|
||||
let metadata_wide = metadata.encode_utf16().chain(Some(0)).collect_vec();
|
||||
set_data_to_clipboard(&metadata_wide, metadata_format)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_data_to_clipboard(data: &[u16], format: u32) -> Result<()> {
|
||||
unsafe {
|
||||
let global = GlobalAlloc(GMEM_MOVEABLE, data.len() * 2)?;
|
||||
let handle = GlobalLock(global);
|
||||
u_memcpy(handle as _, data.as_ptr(), data.len() as _);
|
||||
let _ = GlobalUnlock(global);
|
||||
SetClipboardData(format, HANDLE(global.0))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_from_clipboard(hash_format: u32, metadata_format: u32) -> Option<ClipboardItem> {
|
||||
let result = read_from_clipboard_inner(hash_format, metadata_format).log_err();
|
||||
unsafe { CloseClipboard().log_err() };
|
||||
result
|
||||
}
|
||||
|
||||
fn read_from_clipboard_inner(hash_format: u32, metadata_format: u32) -> Result<ClipboardItem> {
|
||||
unsafe {
|
||||
OpenClipboard(None)?;
|
||||
let text = {
|
||||
let handle = GetClipboardData(CF_UNICODETEXT.0 as u32)?;
|
||||
let text = PCWSTR(handle.0 as *const u16);
|
||||
String::from_utf16_lossy(text.as_wide())
|
||||
};
|
||||
let Some(hash) = read_hash_from_clipboard(hash_format) else {
|
||||
return Ok(ClipboardItem::new_string(text));
|
||||
};
|
||||
let Some(metadata) = read_metadata_from_clipboard(metadata_format) else {
|
||||
return Ok(ClipboardItem::new_string(text));
|
||||
};
|
||||
if hash == ClipboardString::text_hash(&text) {
|
||||
Ok(ClipboardItem::new_string_with_metadata(text, metadata))
|
||||
} else {
|
||||
Ok(ClipboardItem::new_string(text))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_hash_from_clipboard(hash_format: u32) -> Option<u64> {
|
||||
unsafe {
|
||||
let handle = GetClipboardData(hash_format).log_err()?;
|
||||
let raw_ptr = handle.0 as *const u16;
|
||||
let hash_bytes: [u8; 8] = std::slice::from_raw_parts(raw_ptr.cast::<u8>(), 8)
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.log_err()?;
|
||||
Some(u64::from_ne_bytes(hash_bytes))
|
||||
}
|
||||
}
|
||||
|
||||
fn read_metadata_from_clipboard(metadata_format: u32) -> Option<String> {
|
||||
unsafe {
|
||||
let handle = GetClipboardData(metadata_format).log_err()?;
|
||||
let text = PCWSTR(handle.0 as *const u16);
|
||||
Some(String::from_utf16_lossy(text.as_wide()))
|
||||
}
|
||||
}
|
||||
|
||||
// clipboard
|
||||
pub const CLIPBOARD_HASH_FORMAT: PCWSTR = windows::core::w!("zed-text-hash");
|
||||
pub const CLIPBOARD_METADATA_FORMAT: PCWSTR = windows::core::w!("zed-metadata");
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{ClipboardItem, Platform, WindowsPlatform};
|
||||
|
||||
@@ -685,7 +685,7 @@ impl PlatformWindow for WindowsWindow {
|
||||
Some(self.0.state.borrow().renderer.gpu_specs())
|
||||
}
|
||||
|
||||
fn update_ime_position(&self, _bounds: Bounds<Pixels>) {
|
||||
fn update_ime_position(&self, _bounds: Bounds<ScaledPixels>) {
|
||||
// todo(windows)
|
||||
}
|
||||
}
|
||||
@@ -735,23 +735,11 @@ impl IDropTarget_Impl for WindowsDragDropHandler_Impl {
|
||||
}
|
||||
let hdrop = idata.u.hGlobal.0 as *mut HDROP;
|
||||
let mut paths = SmallVec::<[PathBuf; 2]>::new();
|
||||
let file_count = DragQueryFileW(*hdrop, DRAGDROP_GET_FILES_COUNT, None);
|
||||
for file_index in 0..file_count {
|
||||
let filename_length = DragQueryFileW(*hdrop, file_index, None) as usize;
|
||||
let mut buffer = vec![0u16; filename_length + 1];
|
||||
let ret = DragQueryFileW(*hdrop, file_index, Some(buffer.as_mut_slice()));
|
||||
if ret == 0 {
|
||||
log::error!("unable to read file name");
|
||||
continue;
|
||||
with_file_names(*hdrop, |file_name| {
|
||||
if let Some(path) = PathBuf::from_str(&file_name).log_err() {
|
||||
paths.push(path);
|
||||
}
|
||||
if let Some(file_name) =
|
||||
String::from_utf16(&buffer[0..filename_length]).log_err()
|
||||
{
|
||||
if let Some(path) = PathBuf::from_str(&file_name).log_err() {
|
||||
paths.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
ReleaseStgMedium(&mut idata);
|
||||
let mut cursor_position = POINT { x: pt.x, y: pt.y };
|
||||
ScreenToClient(self.0.hwnd, &mut cursor_position)
|
||||
@@ -1069,9 +1057,6 @@ fn calculate_client_rect(
|
||||
}
|
||||
}
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-dragqueryfilew
|
||||
const DRAGDROP_GET_FILES_COUNT: u32 = 0xFFFFFFFF;
|
||||
|
||||
mod windows_renderer {
|
||||
use std::{num::NonZeroIsize, sync::Arc};
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
use std::ops::Deref;
|
||||
|
||||
use windows::Win32::{Foundation::HANDLE, UI::WindowsAndMessaging::HCURSOR};
|
||||
use util::ResultExt;
|
||||
use windows::Win32::{
|
||||
Foundation::{HANDLE, HGLOBAL},
|
||||
System::Memory::{GlobalLock, GlobalSize, GlobalUnlock},
|
||||
UI::WindowsAndMessaging::HCURSOR,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct SafeHandle {
|
||||
@@ -45,3 +50,30 @@ impl Deref for SafeCursor {
|
||||
&self.raw
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct SmartGlobal {
|
||||
raw: HGLOBAL,
|
||||
}
|
||||
|
||||
impl SmartGlobal {
|
||||
pub(crate) fn from_raw_ptr(ptr: *mut std::ffi::c_void) -> Self {
|
||||
Self { raw: HGLOBAL(ptr) }
|
||||
}
|
||||
|
||||
pub(crate) fn lock(&self) -> *mut std::ffi::c_void {
|
||||
unsafe { GlobalLock(self.raw) }
|
||||
}
|
||||
|
||||
pub(crate) fn size(&self) -> usize {
|
||||
unsafe { GlobalSize(self.raw) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SmartGlobal {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
GlobalUnlock(self.raw).log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3610,7 +3610,9 @@ impl<'a> WindowContext<'a> {
|
||||
self.on_next_frame(|cx| {
|
||||
if let Some(mut input_handler) = cx.window.platform_window.take_input_handler() {
|
||||
if let Some(bounds) = input_handler.selected_bounds(cx) {
|
||||
cx.window.platform_window.update_ime_position(bounds);
|
||||
cx.window
|
||||
.platform_window
|
||||
.update_ime_position(bounds.scale(cx.scale_factor()));
|
||||
}
|
||||
cx.window.platform_window.set_input_handler(input_handler);
|
||||
}
|
||||
|
||||
@@ -661,7 +661,7 @@ pub enum Formatter {
|
||||
/// The external program to run.
|
||||
command: Arc<str>,
|
||||
/// The arguments to pass to the program.
|
||||
arguments: Arc<[String]>,
|
||||
arguments: Option<Arc<[String]>>,
|
||||
},
|
||||
/// Files should be formatted using code actions executed by language servers.
|
||||
CodeActions(HashMap<String, bool>),
|
||||
|
||||
@@ -17,7 +17,7 @@ use ui::{prelude::*, Button, Checkbox, ContextMenu, Label, PopoverMenu, Selectio
|
||||
use workspace::{
|
||||
item::{Item, ItemHandle},
|
||||
searchable::{SearchEvent, SearchableItem, SearchableItemHandle},
|
||||
ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
|
||||
SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,
|
||||
};
|
||||
|
||||
const SEND_LINE: &str = "// Send:";
|
||||
@@ -194,12 +194,11 @@ pub fn init(cx: &mut AppContext) {
|
||||
workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, cx| {
|
||||
let project = workspace.project().read(cx);
|
||||
if project.is_local() {
|
||||
workspace.add_item_to_active_pane(
|
||||
workspace.split_item(
|
||||
SplitDirection::Right,
|
||||
Box::new(cx.new_view(|cx| {
|
||||
LspLogView::new(workspace.project().clone(), log_store.clone(), cx)
|
||||
})),
|
||||
None,
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -912,6 +911,27 @@ impl Item for LspLogView {
|
||||
fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
Some(Box::new(handle.clone()))
|
||||
}
|
||||
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
_workspace_id: Option<WorkspaceId>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<View<Self>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Some(cx.new_view(|cx| {
|
||||
let mut new_view = Self::new(self.project.clone(), self.log_store.clone(), cx);
|
||||
if let Some(server_id) = self.current_server_id {
|
||||
match self.active_entry_kind {
|
||||
LogKind::Rpc => new_view.show_rpc_trace_for_server(server_id, cx),
|
||||
LogKind::Trace => new_view.show_trace_for_server(server_id, cx),
|
||||
LogKind::Logs => new_view.show_logs_for_server(server_id, cx),
|
||||
}
|
||||
}
|
||||
new_view
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl SearchableItem for LspLogView {
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
(field_identifier) @property
|
||||
(namespace_identifier) @namespace
|
||||
|
||||
(concept_definition
|
||||
(identifier) @concept)
|
||||
|
||||
|
||||
(call_expression
|
||||
function: (qualified_identifier
|
||||
name: (identifier) @function))
|
||||
@@ -64,6 +68,14 @@
|
||||
|
||||
(auto) @type
|
||||
(type_identifier) @type
|
||||
type :(primitive_type) @type.primitive
|
||||
|
||||
(requires_clause
|
||||
constraint: (template_type
|
||||
name: (type_identifier) @concept))
|
||||
|
||||
(attribute
|
||||
name: (identifier) @keyword)
|
||||
|
||||
((identifier) @constant
|
||||
(#match? @constant "^_*[A-Z][A-Z\\d_]*$"))
|
||||
@@ -119,7 +131,6 @@
|
||||
"using"
|
||||
"virtual"
|
||||
"while"
|
||||
(primitive_type)
|
||||
(sized_type_specifier)
|
||||
(storage_class_specifier)
|
||||
(type_qualifier)
|
||||
|
||||
@@ -6,8 +6,8 @@ use crate::markdown_elements::{
|
||||
};
|
||||
use gpui::{
|
||||
div, px, rems, AbsoluteLength, AnyElement, DefiniteLength, Div, Element, ElementId,
|
||||
HighlightStyle, Hsla, InteractiveText, IntoElement, Keystroke, Modifiers, ParentElement,
|
||||
SharedString, Styled, StyledText, TextStyle, WeakView, WindowContext,
|
||||
HighlightStyle, Hsla, InteractiveText, IntoElement, Keystroke, Length, Modifiers,
|
||||
ParentElement, SharedString, Styled, StyledText, TextStyle, WeakView, WindowContext,
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
@@ -16,7 +16,7 @@ use std::{
|
||||
};
|
||||
use theme::{ActiveTheme, SyntaxTheme, ThemeSettings};
|
||||
use ui::{
|
||||
h_flex, v_flex, Checkbox, FluentBuilder, InteractiveElement, LinkPreview, Selection,
|
||||
h_flex, relative, v_flex, Checkbox, FluentBuilder, InteractiveElement, LinkPreview, Selection,
|
||||
StatefulInteractiveElement, Tooltip,
|
||||
};
|
||||
use workspace::Workspace;
|
||||
@@ -231,12 +231,48 @@ fn render_markdown_list_item(
|
||||
}
|
||||
|
||||
fn render_markdown_table(parsed: &ParsedMarkdownTable, cx: &mut RenderContext) -> AnyElement {
|
||||
let header = render_markdown_table_row(&parsed.header, &parsed.column_alignments, true, cx);
|
||||
let mut max_lengths: Vec<usize> = vec![0; parsed.header.children.len()];
|
||||
|
||||
for (index, cell) in parsed.header.children.iter().enumerate() {
|
||||
let length = cell.contents.len();
|
||||
max_lengths[index] = length;
|
||||
}
|
||||
|
||||
for row in &parsed.body {
|
||||
for (index, cell) in row.children.iter().enumerate() {
|
||||
let length = cell.contents.len();
|
||||
if length > max_lengths[index] {
|
||||
max_lengths[index] = length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let total_max_length: usize = max_lengths.iter().sum();
|
||||
let max_column_widths: Vec<f32> = max_lengths
|
||||
.iter()
|
||||
.map(|&length| length as f32 / total_max_length as f32)
|
||||
.collect();
|
||||
|
||||
let header = render_markdown_table_row(
|
||||
&parsed.header,
|
||||
&parsed.column_alignments,
|
||||
&max_column_widths,
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
|
||||
let body: Vec<AnyElement> = parsed
|
||||
.body
|
||||
.iter()
|
||||
.map(|row| render_markdown_table_row(row, &parsed.column_alignments, false, cx))
|
||||
.map(|row| {
|
||||
render_markdown_table_row(
|
||||
row,
|
||||
&parsed.column_alignments,
|
||||
&max_column_widths,
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
cx.with_common_p(v_flex())
|
||||
@@ -249,14 +285,15 @@ fn render_markdown_table(parsed: &ParsedMarkdownTable, cx: &mut RenderContext) -
|
||||
fn render_markdown_table_row(
|
||||
parsed: &ParsedMarkdownTableRow,
|
||||
alignments: &Vec<ParsedMarkdownTableAlignment>,
|
||||
max_column_widths: &Vec<f32>,
|
||||
is_header: bool,
|
||||
cx: &mut RenderContext,
|
||||
) -> AnyElement {
|
||||
let mut items = vec![];
|
||||
|
||||
for cell in &parsed.children {
|
||||
for (index, cell) in parsed.children.iter().enumerate() {
|
||||
let alignment = alignments
|
||||
.get(items.len())
|
||||
.get(index)
|
||||
.copied()
|
||||
.unwrap_or(ParsedMarkdownTableAlignment::None);
|
||||
|
||||
@@ -268,8 +305,11 @@ fn render_markdown_table_row(
|
||||
ParsedMarkdownTableAlignment::Right => v_flex().items_end(),
|
||||
};
|
||||
|
||||
let max_width = max_column_widths.get(index).unwrap_or(&0.0);
|
||||
|
||||
let mut cell = container
|
||||
.w_full()
|
||||
.w(Length::Definite(relative(*max_width)))
|
||||
.h_full()
|
||||
.child(contents)
|
||||
.px_2()
|
||||
.py_1()
|
||||
|
||||
@@ -198,8 +198,9 @@ async fn load_shell_environment(
|
||||
|
||||
anyhow::ensure!(
|
||||
direnv_output.status.success(),
|
||||
"direnv exited with error {:?}",
|
||||
direnv_output.status
|
||||
"direnv exited with error {:?}. Stderr:\n{}",
|
||||
direnv_output.status,
|
||||
String::from_utf8_lossy(&direnv_output.stderr)
|
||||
);
|
||||
|
||||
let output = String::from_utf8_lossy(&direnv_output.stdout);
|
||||
@@ -214,7 +215,7 @@ async fn load_shell_environment(
|
||||
|
||||
let direnv_environment = match load_direnv {
|
||||
DirenvSettings::ShellHook => None,
|
||||
DirenvSettings::Direct => load_direnv_environment(dir).await?,
|
||||
DirenvSettings::Direct => load_direnv_environment(dir).await.log_err().flatten(),
|
||||
}
|
||||
.unwrap_or(HashMap::default());
|
||||
|
||||
|
||||
@@ -539,13 +539,19 @@ impl LocalLspStore {
|
||||
}
|
||||
Formatter::External { command, arguments } => {
|
||||
let buffer_abs_path = buffer_abs_path.as_ref().map(|path| path.as_path());
|
||||
Self::format_via_external_command(buffer, buffer_abs_path, command, arguments, cx)
|
||||
.await
|
||||
.context(format!(
|
||||
"failed to format via external command {:?}",
|
||||
command
|
||||
))?
|
||||
.map(FormatOperation::External)
|
||||
Self::format_via_external_command(
|
||||
buffer,
|
||||
buffer_abs_path,
|
||||
command,
|
||||
arguments.as_deref(),
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.context(format!(
|
||||
"failed to format via external command {:?}",
|
||||
command
|
||||
))?
|
||||
.map(FormatOperation::External)
|
||||
}
|
||||
Formatter::CodeActions(code_actions) => {
|
||||
let code_actions = deserialize_code_actions(code_actions);
|
||||
@@ -571,7 +577,7 @@ impl LocalLspStore {
|
||||
buffer: &Model<Buffer>,
|
||||
buffer_abs_path: Option<&Path>,
|
||||
command: &str,
|
||||
arguments: &[String],
|
||||
arguments: Option<&[String]>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Option<Diff>> {
|
||||
let working_dir_path = buffer.update(cx, |buffer, cx| {
|
||||
@@ -595,14 +601,17 @@ impl LocalLspStore {
|
||||
child.current_dir(working_dir_path);
|
||||
}
|
||||
|
||||
let mut child = child
|
||||
.args(arguments.iter().map(|arg| {
|
||||
if let Some(arguments) = arguments {
|
||||
child.args(arguments.iter().map(|arg| {
|
||||
if let Some(buffer_abs_path) = buffer_abs_path {
|
||||
arg.replace("{buffer_path}", &buffer_abs_path.to_string_lossy())
|
||||
} else {
|
||||
arg.replace("{buffer_path}", "Untitled")
|
||||
}
|
||||
}))
|
||||
}));
|
||||
}
|
||||
|
||||
let mut child = child
|
||||
.stdin(smol::process::Stdio::piped())
|
||||
.stdout(smol::process::Stdio::piped())
|
||||
.stderr(smol::process::Stdio::piped())
|
||||
|
||||
@@ -62,12 +62,9 @@ pub struct NodeBinarySettings {
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DirenvSettings {
|
||||
/// Load direnv configuration through a shell hook
|
||||
#[default]
|
||||
ShellHook,
|
||||
/// Load direnv configuration directly using `direnv export json`
|
||||
///
|
||||
/// Warning: This option is experimental and might cause some inconsistent behavior compared to using the shell hook.
|
||||
/// If it does, please report it to GitHub
|
||||
#[default]
|
||||
Direct,
|
||||
}
|
||||
|
||||
|
||||
@@ -215,7 +215,7 @@ impl Project {
|
||||
spawn_task,
|
||||
shell,
|
||||
env,
|
||||
Some(settings.blinking),
|
||||
settings.cursor_shape.unwrap_or_default(),
|
||||
settings.alternate_scroll,
|
||||
settings.max_scroll_history_lines,
|
||||
window,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,9 @@ use alacritty_terminal::{
|
||||
Config, RenderableCursor, TermMode,
|
||||
},
|
||||
tty::{self},
|
||||
vte::ansi::{ClearMode, Handler, NamedPrivateMode, PrivateMode},
|
||||
vte::ansi::{
|
||||
ClearMode, CursorStyle as AlacCursorStyle, Handler, NamedPrivateMode, PrivateMode,
|
||||
},
|
||||
Term,
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
@@ -40,7 +42,7 @@ use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use smol::channel::{Receiver, Sender};
|
||||
use task::{HideStrategy, Shell, TaskId};
|
||||
use terminal_settings::{AlternateScroll, TerminalBlink, TerminalSettings};
|
||||
use terminal_settings::{AlternateScroll, CursorShape, TerminalSettings};
|
||||
use theme::{ActiveTheme, Theme};
|
||||
use util::truncate_and_trailoff;
|
||||
|
||||
@@ -100,7 +102,7 @@ pub enum Event {
|
||||
CloseTerminal,
|
||||
Bell,
|
||||
Wakeup,
|
||||
BlinkChanged,
|
||||
BlinkChanged(bool),
|
||||
SelectionsChanged,
|
||||
NewNavigationTarget(Option<MaybeNavigationTarget>),
|
||||
Open(MaybeNavigationTarget),
|
||||
@@ -313,7 +315,7 @@ impl TerminalBuilder {
|
||||
task: Option<TaskState>,
|
||||
shell: Shell,
|
||||
mut env: HashMap<String, String>,
|
||||
blink_settings: Option<TerminalBlink>,
|
||||
cursor_shape: CursorShape,
|
||||
alternate_scroll: AlternateScroll,
|
||||
max_scroll_history_lines: Option<usize>,
|
||||
window: AnyWindowHandle,
|
||||
@@ -353,6 +355,7 @@ impl TerminalBuilder {
|
||||
// Setup Alacritty's env, which modifies the current process's environment
|
||||
alacritty_terminal::tty::setup_env();
|
||||
|
||||
let default_cursor_style = AlacCursorStyle::from(cursor_shape);
|
||||
let scrolling_history = if task.is_some() {
|
||||
// Tasks like `cargo build --all` may produce a lot of output, ergo allow maximum scrolling.
|
||||
// After the task finishes, we do not allow appending to that terminal, so small tasks output should not
|
||||
@@ -365,6 +368,7 @@ impl TerminalBuilder {
|
||||
};
|
||||
let config = Config {
|
||||
scrolling_history,
|
||||
default_cursor_style,
|
||||
..Config::default()
|
||||
};
|
||||
|
||||
@@ -373,16 +377,11 @@ impl TerminalBuilder {
|
||||
let (events_tx, events_rx) = unbounded();
|
||||
//Set up the terminal...
|
||||
let mut term = Term::new(
|
||||
config,
|
||||
config.clone(),
|
||||
&TerminalSize::default(),
|
||||
ZedListener(events_tx.clone()),
|
||||
);
|
||||
|
||||
//Start off blinking if we need to
|
||||
if let Some(TerminalBlink::On) = blink_settings {
|
||||
term.set_private_mode(PrivateMode::Named(NamedPrivateMode::BlinkingCursor));
|
||||
}
|
||||
|
||||
//Alacritty defaults to alternate scrolling being on, so we just need to turn it off.
|
||||
if let AlternateScroll::Off = alternate_scroll {
|
||||
term.unset_private_mode(PrivateMode::Named(NamedPrivateMode::AlternateScroll));
|
||||
@@ -432,6 +431,7 @@ impl TerminalBuilder {
|
||||
pty_tx: Notifier(pty_tx),
|
||||
completion_tx,
|
||||
term,
|
||||
term_config: config,
|
||||
events: VecDeque::with_capacity(10), //Should never get this high.
|
||||
last_content: Default::default(),
|
||||
last_mouse: None,
|
||||
@@ -583,6 +583,7 @@ pub struct Terminal {
|
||||
pty_tx: Notifier,
|
||||
completion_tx: Sender<()>,
|
||||
term: Arc<FairMutex<Term<ZedListener>>>,
|
||||
term_config: Config,
|
||||
events: VecDeque<InternalEvent>,
|
||||
/// This is only used for mouse mode cell change detection
|
||||
last_mouse: Option<(AlacPoint, AlacDirection)>,
|
||||
@@ -667,7 +668,9 @@ impl Terminal {
|
||||
self.write_to_pty(format(self.last_content.size.into()))
|
||||
}
|
||||
AlacTermEvent::CursorBlinkingChange => {
|
||||
cx.emit(Event::BlinkChanged);
|
||||
let terminal = self.term.lock();
|
||||
let blinking = terminal.cursor_style().blinking;
|
||||
cx.emit(Event::BlinkChanged(blinking));
|
||||
}
|
||||
AlacTermEvent::Bell => {
|
||||
cx.emit(Event::Bell);
|
||||
@@ -951,6 +954,11 @@ impl Terminal {
|
||||
&self.last_content
|
||||
}
|
||||
|
||||
pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape) {
|
||||
self.term_config.default_cursor_style = cursor_shape.into();
|
||||
self.term.lock().set_options(self.term_config.clone());
|
||||
}
|
||||
|
||||
pub fn total_lines(&self) -> usize {
|
||||
let term = self.term.clone();
|
||||
let terminal = term.lock_unfair();
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
use alacritty_terminal::vte::ansi::{
|
||||
CursorShape as AlacCursorShape, CursorStyle as AlacCursorStyle,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use gpui::{
|
||||
px, AbsoluteLength, AppContext, FontFallbacks, FontFeatures, FontWeight, Pixels, SharedString,
|
||||
@@ -32,6 +35,7 @@ pub struct TerminalSettings {
|
||||
pub font_weight: Option<FontWeight>,
|
||||
pub line_height: TerminalLineHeight,
|
||||
pub env: HashMap<String, String>,
|
||||
pub cursor_shape: Option<CursorShape>,
|
||||
pub blinking: TerminalBlink,
|
||||
pub alternate_scroll: AlternateScroll,
|
||||
pub option_as_meta: bool,
|
||||
@@ -129,6 +133,11 @@ pub struct TerminalSettingsContent {
|
||||
///
|
||||
/// Default: {}
|
||||
pub env: Option<HashMap<String, String>>,
|
||||
/// Default cursor shape for the terminal.
|
||||
/// Can be "bar", "block", "underscore", or "hollow".
|
||||
///
|
||||
/// Default: None
|
||||
pub cursor_shape: Option<CursorShape>,
|
||||
/// Sets the cursor blinking behavior in the terminal.
|
||||
///
|
||||
/// Default: terminal_controlled
|
||||
@@ -282,3 +291,37 @@ pub struct ToolbarContent {
|
||||
/// Default: true
|
||||
pub title: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CursorShape {
|
||||
/// Cursor is a block like `█`.
|
||||
#[default]
|
||||
Block,
|
||||
/// Cursor is an underscore like `_`.
|
||||
Underline,
|
||||
/// Cursor is a vertical bar like `⎸`.
|
||||
Bar,
|
||||
/// Cursor is a hollow box like `▯`.
|
||||
Hollow,
|
||||
}
|
||||
|
||||
impl From<CursorShape> for AlacCursorShape {
|
||||
fn from(value: CursorShape) -> Self {
|
||||
match value {
|
||||
CursorShape::Block => AlacCursorShape::Block,
|
||||
CursorShape::Underline => AlacCursorShape::Underline,
|
||||
CursorShape::Bar => AlacCursorShape::Beam,
|
||||
CursorShape::Hollow => AlacCursorShape::HollowBlock,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CursorShape> for AlacCursorStyle {
|
||||
fn from(value: CursorShape) -> Self {
|
||||
AlacCursorStyle {
|
||||
shape: value.into(),
|
||||
blinking: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ use terminal::{
|
||||
index::Point,
|
||||
term::{search::RegexSearch, TermMode},
|
||||
},
|
||||
terminal_settings::{TerminalBlink, TerminalSettings, WorkingDirectory},
|
||||
terminal_settings::{CursorShape, TerminalBlink, TerminalSettings, WorkingDirectory},
|
||||
Clear, Copy, Event, MaybeNavigationTarget, Paste, ScrollLineDown, ScrollLineUp, ScrollPageDown,
|
||||
ScrollPageUp, ScrollToBottom, ScrollToTop, ShowCharacterPalette, TaskStatus, Terminal,
|
||||
TerminalSize,
|
||||
@@ -102,8 +102,9 @@ pub struct TerminalView {
|
||||
//Currently using iTerm bell, show bell emoji in tab until input is received
|
||||
has_bell: bool,
|
||||
context_menu: Option<(View<ContextMenu>, gpui::Point<Pixels>, Subscription)>,
|
||||
cursor_shape: CursorShape,
|
||||
blink_state: bool,
|
||||
blinking_on: bool,
|
||||
blinking_terminal_enabled: bool,
|
||||
blinking_paused: bool,
|
||||
blink_epoch: usize,
|
||||
can_navigate_to_selected_word: bool,
|
||||
@@ -171,6 +172,9 @@ impl TerminalView {
|
||||
let focus_out = cx.on_focus_out(&focus_handle, |terminal_view, _event, cx| {
|
||||
terminal_view.focus_out(cx);
|
||||
});
|
||||
let cursor_shape = TerminalSettings::get_global(cx)
|
||||
.cursor_shape
|
||||
.unwrap_or_default();
|
||||
|
||||
Self {
|
||||
terminal,
|
||||
@@ -178,8 +182,9 @@ impl TerminalView {
|
||||
has_bell: false,
|
||||
focus_handle,
|
||||
context_menu: None,
|
||||
cursor_shape,
|
||||
blink_state: true,
|
||||
blinking_on: false,
|
||||
blinking_terminal_enabled: false,
|
||||
blinking_paused: false,
|
||||
blink_epoch: 0,
|
||||
can_navigate_to_selected_word: false,
|
||||
@@ -255,6 +260,16 @@ impl TerminalView {
|
||||
fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
|
||||
let settings = TerminalSettings::get_global(cx);
|
||||
self.show_title = settings.toolbar.title;
|
||||
|
||||
let new_cursor_shape = settings.cursor_shape.unwrap_or_default();
|
||||
let old_cursor_shape = self.cursor_shape;
|
||||
if old_cursor_shape != new_cursor_shape {
|
||||
self.cursor_shape = new_cursor_shape;
|
||||
self.terminal.update(cx, |term, _| {
|
||||
term.set_cursor_shape(self.cursor_shape);
|
||||
});
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -419,7 +434,6 @@ impl TerminalView {
|
||||
pub fn should_show_cursor(&self, focused: bool, cx: &mut gpui::ViewContext<Self>) -> bool {
|
||||
//Don't blink the cursor when not focused, blinking is disabled, or paused
|
||||
if !focused
|
||||
|| !self.blinking_on
|
||||
|| self.blinking_paused
|
||||
|| self
|
||||
.terminal
|
||||
@@ -435,7 +449,10 @@ impl TerminalView {
|
||||
//If the user requested to never blink, don't blink it.
|
||||
TerminalBlink::Off => true,
|
||||
//If the terminal is controlling it, check terminal mode
|
||||
TerminalBlink::TerminalControlled | TerminalBlink::On => self.blink_state,
|
||||
TerminalBlink::TerminalControlled => {
|
||||
!self.blinking_terminal_enabled || self.blink_state
|
||||
}
|
||||
TerminalBlink::On => self.blink_state,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -627,7 +644,14 @@ fn subscribe_for_terminal_events(
|
||||
cx.emit(Event::Wakeup);
|
||||
}
|
||||
|
||||
Event::BlinkChanged => this.blinking_on = !this.blinking_on,
|
||||
Event::BlinkChanged(blinking) => {
|
||||
if matches!(
|
||||
TerminalSettings::get_global(cx).blinking,
|
||||
TerminalBlink::TerminalControlled
|
||||
) {
|
||||
this.blinking_terminal_enabled = *blinking;
|
||||
}
|
||||
}
|
||||
|
||||
Event::TitleChanged => {
|
||||
cx.emit(ItemEvent::UpdateTab);
|
||||
@@ -903,7 +927,10 @@ impl TerminalView {
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.terminal.read(cx).focus_in();
|
||||
self.terminal.update(cx, |terminal, _| {
|
||||
terminal.set_cursor_shape(self.cursor_shape);
|
||||
terminal.focus_in();
|
||||
});
|
||||
self.blink_cursors(self.blink_epoch, cx);
|
||||
cx.invalidate_character_coordinates();
|
||||
cx.notify();
|
||||
@@ -912,6 +939,7 @@ impl TerminalView {
|
||||
fn focus_out(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.terminal.update(cx, |terminal, _| {
|
||||
terminal.focus_out();
|
||||
terminal.set_cursor_shape(CursorShape::Hollow);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ impl Render for TitleBar {
|
||||
let supported_controls = cx.window_controls();
|
||||
let decorations = cx.window_decorations();
|
||||
let titlebar_color = if cfg!(target_os = "linux") {
|
||||
if cx.is_window_active() {
|
||||
if cx.is_window_active() && !self.should_move {
|
||||
cx.theme().colors().title_bar_background
|
||||
} else {
|
||||
cx.theme().colors().title_bar_inactive_background
|
||||
|
||||
@@ -56,6 +56,13 @@ fn main() {
|
||||
println!("cargo:rerun-if-changed={}", icon.display());
|
||||
|
||||
let mut res = winresource::WindowsResource::new();
|
||||
|
||||
// Depending on the security applied to the computer, winresource might fail
|
||||
// fetching the RC path. Therefore, we add a way to explicitly specify the
|
||||
// toolkit path, allowing winresource to use a valid RC path.
|
||||
if let Some(explicit_rc_toolkit_path) = std::env::var("ZED_RC_TOOLKIT_PATH").ok() {
|
||||
res.set_toolkit_path(explicit_rc_toolkit_path.as_str());
|
||||
}
|
||||
res.set_icon(icon.to_str().unwrap());
|
||||
res.set("FileDescription", "Zed");
|
||||
res.set("ProductName", "Zed");
|
||||
|
||||
@@ -267,12 +267,14 @@ left and right padding of the central pane from the workspace when the centered
|
||||
|
||||
## Direnv Integration
|
||||
|
||||
- Description: Settings for [direnv](https://direnv.net/) integration. Requires `direnv` to be installed. `direnv` integration currently only means that the environment variables set by a `direnv` configuration can be used to detect some language servers in `$PATH` instead of installing them.
|
||||
- Description: Settings for [direnv](https://direnv.net/) integration. Requires `direnv` to be installed.
|
||||
`direnv` integration make it possible to use the environment variables set by a `direnv` configuration to detect some language servers in `$PATH` instead of installing them.
|
||||
It also allows for those environment variables to be used in tasks.
|
||||
- Setting: `load_direnv`
|
||||
- Default:
|
||||
|
||||
```json
|
||||
"load_direnv": "shell_hook"
|
||||
"load_direnv": "direct"
|
||||
```
|
||||
|
||||
**Options**
|
||||
|
||||
@@ -35,6 +35,12 @@ Clone down the [Zed repository](https://github.com/zed-industries/zed).
|
||||
brew install cmake
|
||||
```
|
||||
|
||||
- (Optional) Install `mold` to speed up link times
|
||||
|
||||
```sh
|
||||
brew install mold
|
||||
```
|
||||
|
||||
## Backend Dependencies
|
||||
|
||||
If you are developing collaborative features of Zed, you'll need to install the dependencies of zed's `collab` server:
|
||||
|
||||
@@ -63,16 +63,16 @@ cargo test --workspace
|
||||
|
||||
## Installing from msys2
|
||||
|
||||
[MSYS2](https://msys2.org/) distribution provides Zed as a package [mingw-w64-zed](https://packages.msys2.org/base/mingw-w64-zed). To download the prebuilt binary, run
|
||||
[MSYS2](https://msys2.org/) distribution provides Zed as a package [mingw-w64-zed](https://packages.msys2.org/base/mingw-w64-zed). The package is available for UCRT64 and CLANG64. To download it, run
|
||||
|
||||
```sh
|
||||
pacman -Syu
|
||||
pacman -S mingw-w64-ucrt-x86_64-zed
|
||||
pacman -S $MINGW_PACKAGE_PREFIX-zed
|
||||
```
|
||||
|
||||
then you can run `zed` in a UCRT64 shell.
|
||||
then you can run `zed` in a shell.
|
||||
|
||||
You can see the [build script](https://github.com/msys2/MINGW-packages/blob/master/mingw-w64-zed/PKGBUILD) for more details.
|
||||
You can see the [build script](https://github.com/msys2/MINGW-packages/blob/master/mingw-w64-zed/PKGBUILD) for more details on build process.
|
||||
|
||||
> Please, report any issue in [msys2/MINGW-packages/issues](https://github.com/msys2/MINGW-packages/issues?q=is%3Aissue+is%3Aopen+zed) first.
|
||||
|
||||
@@ -93,3 +93,30 @@ This error can happen if you are using the "rust-lld.exe" linker. Consider tryin
|
||||
If you are using a global config, consider moving the Zed repository to a nested directory and add a `.cargo/config.toml` with a custom linker config in the parent directory.
|
||||
|
||||
See this issue for more information [#12041](https://github.com/zed-industries/zed/issues/12041)
|
||||
|
||||
### Invalid RC path selected
|
||||
|
||||
Sometimes, depending on the security rules applied to your laptop, you may get the following error while compiling Zed:
|
||||
|
||||
```
|
||||
error: failed to run custom build command for `zed(C:\Users\USER\src\zed\crates\zed)`
|
||||
|
||||
Caused by:
|
||||
process didn't exit successfully: `C:\Users\USER\src\zed\target\debug\build\zed-b24f1e9300107efc\build-script-build` (exit code: 1)
|
||||
--- stdout
|
||||
cargo:rerun-if-changed=../../.git/logs/HEAD
|
||||
cargo:rustc-env=ZED_COMMIT_SHA=25e2e9c6727ba9b77415588cfa11fd969612adb7
|
||||
cargo:rustc-link-arg=/stack:8388608
|
||||
cargo:rerun-if-changed=resources/windows/app-icon.ico
|
||||
package.metadata.winresource does not exist
|
||||
Selected RC path: 'bin\x64\rc.exe'
|
||||
|
||||
--- stderr
|
||||
The system cannot find the path specified. (os error 3)
|
||||
warning: build failed, waiting for other jobs to finish...
|
||||
```
|
||||
|
||||
In order to fix this issue, you can manually set the `ZED_RC_TOOLKIT_PATH` environment variable to the RC toolkit path. Usually, you can set it to:
|
||||
`C:\Program Files (x86)\Windows Kits\10\bin\<SDK_version>\x64`.
|
||||
|
||||
See this [issue](https://github.com/zed-industries/zed/issues/18393) for more information.
|
||||
|
||||
@@ -16,7 +16,7 @@ The Zed installed by the script works best on systems that:
|
||||
|
||||
- have a Vulkan compatible GPU available (for example Linux on an M-series macBook)
|
||||
- have a system-wide glibc (NixOS and Alpine do not by default)
|
||||
- x86_64 (Intel/AMD): glibc version >= 2.35 (Ubuntu 22 and newer)
|
||||
- x86_64 (Intel/AMD): glibc version >= 2.31 (Ubuntu 20 and newer)
|
||||
- aarch64 (ARM): glibc version >= 2.35 (Ubuntu 22 and newer)
|
||||
|
||||
Both Nix and Alpine have third-party Zed packages available (though they are currently a few weeks out of date). If you'd like to use our builds they do work if you install a glibc compatibility layer. On NixOS you can try [nix-ld](https://github.com/Mic92/nix-ld), and on Alpine [gcompat](https://wiki.alpinelinux.org/wiki/Running_glibc_programs).
|
||||
@@ -24,8 +24,8 @@ Both Nix and Alpine have third-party Zed packages available (though they are cur
|
||||
You will need to build from source for:
|
||||
|
||||
- architectures other than 64-bit Intel or 64-bit ARM (for example a 32-bit or RISC-V machine)
|
||||
- Amazon Linux
|
||||
- Rocky Linux 9.3
|
||||
- Redhat Enterprise Linux 8.x, Rocky Linux 8, AlmaLinux 8, Amazon Linux 2 on all architectures
|
||||
- Redhat Enterprise Linux 9.x, Rocky Linux 9.3, AlmaLinux 8, Amazon Linux 2023 on aarch64 (x86_x64 OK)
|
||||
|
||||
## Other ways to install Zed on Linux
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ To start a search run the `pane: Toggle Search` command (`cmd-shift-f` on macOS,
|
||||
|
||||
## Diagnostics
|
||||
|
||||
If you have a language server installed, the diagnostics pane can show you all errors across your project. You can open it by clicking on the icon in the status bar, or running the `diagnostcs: Deploy` command` ('cmd-shift-m` on macOS, `ctrl-shift-m` on Windows/Linux, or `:clist` in Vim mode).
|
||||
If you have a language server installed, the diagnostics pane can show you all errors across your project. You can open it by clicking on the icon in the status bar, or running the `diagnostics: Deploy` command` ('cmd-shift-m` on macOS, `ctrl-shift-m` on Windows/Linux, or `:clist` in Vim mode).
|
||||
|
||||
## Find References
|
||||
|
||||
|
||||
@@ -5,3 +5,5 @@
|
||||
((comment) @content
|
||||
(#match? @content "^/\\*\\*[^*]")
|
||||
(#set! "language" "phpdoc"))
|
||||
|
||||
((heredoc_body) (heredoc_end) @language) @content
|
||||
|
||||
25
script/build-docker
Executable file
25
script/build-docker
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Use a docker BASE_IMAGE to test building Zed.
|
||||
# e.g: ./script/bundle-docker ubuntu:20.04
|
||||
#
|
||||
# Increasing resources available to podman may speed this up:
|
||||
# podman machine stop
|
||||
# podman machine set --memory 16384 --cpus 8 --disk-size 200
|
||||
# podman machine start
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BASE_IMAGE=${BASE_IMAGE:-${1:-}}
|
||||
if [ -z "$BASE_IMAGE" ]; then
|
||||
echo "Usage: $0 BASE_IMAGE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export DOCKER_BUILDKIT=1
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
podman build . \
|
||||
-f Dockerfile-distros \
|
||||
-t many \
|
||||
--build-arg BASE_IMAGE="$BASE_IMAGE"
|
||||
@@ -9,11 +9,8 @@ case $channel in
|
||||
preview)
|
||||
tag_suffix="-pre"
|
||||
;;
|
||||
nightly)
|
||||
tag_suffix="-nightly"
|
||||
;;
|
||||
*)
|
||||
echo "this must be run on either of stable|preview|nightly release branches" >&2
|
||||
echo "this must be run on either of stable|preview release branches" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
32
script/determine-release-channel
Executable file
32
script/determine-release-channel
Executable file
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z "${GITHUB_ACTIONS-}" ]; then
|
||||
echo "Error: This script must be run in a GitHub Actions environment"
|
||||
exit 1
|
||||
elif [ -z "${GITHUB_REF-}" ]; then
|
||||
# This should be the release tag 'v0.x.x'
|
||||
echo "Error: GITHUB_REF is not set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
version=$(script/get-crate-version zed)
|
||||
channel=$(cat crates/zed/RELEASE_CHANNEL)
|
||||
echo "Publishing version: ${version} on release channel ${channel}"
|
||||
echo "RELEASE_CHANNEL=${channel}" >> $GITHUB_ENV
|
||||
|
||||
expected_tag_name=""
|
||||
case ${channel} in
|
||||
stable)
|
||||
expected_tag_name="v${version}";;
|
||||
preview)
|
||||
expected_tag_name="v${version}-pre";;
|
||||
*)
|
||||
echo "can't publish a release on channel ${channel}"
|
||||
exit 1;;
|
||||
esac
|
||||
if [[ $GITHUB_REF_NAME != $expected_tag_name ]]; then
|
||||
echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}"
|
||||
exit 1
|
||||
fi
|
||||
77
script/install-cmake
Executable file
77
script/install-cmake
Executable file
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# This script installs an up-to-date version of CMake.
|
||||
#
|
||||
# For MacOS use Homebrew to install the latest version.
|
||||
#
|
||||
# For Ubuntu use the official KitWare Apt repository with backports.
|
||||
# See: https://apt.kitware.com/
|
||||
#
|
||||
# For other systems (RHEL 8.x, 9.x, AmazonLinux, SUSE, Fedora, Arch, etc)
|
||||
# use the official CMake installer script from KitWare.
|
||||
#
|
||||
# Note this is similar to how GitHub Actions runners install cmake:
|
||||
# https://github.com/actions/runner-images/blob/main/images/ubuntu/scripts/build/install-cmake.sh
|
||||
#
|
||||
# Upstream: 3.30.4 (2024-09-27)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
|
||||
if [[ "$(uname -s)" == "darwin" ]]; then
|
||||
brew --version >/dev/null \
|
||||
|| echo "Error: Homebrew is required to install cmake on MacOS." && exit 1
|
||||
echo "Installing cmake via Homebrew (can't pin to old versions)."
|
||||
brew install cmake
|
||||
exit 0
|
||||
elif [ "$(uname -s)" != "Linux" ]; then
|
||||
echo "Error: This script is intended for MacOS/Linux systems only."
|
||||
exit 1
|
||||
elif [ -z "${1:-}" ]; then
|
||||
echo "Usage: $0 [3.30.4]"
|
||||
exit 1
|
||||
fi
|
||||
CMAKE_VERSION="${CMAKE_VERSION:-${1:-3.30.4}}"
|
||||
|
||||
if [ "$(whoami)" = root ]; then SUDO=; else SUDO="$(command -v sudo || command -v doas || true)"; fi
|
||||
|
||||
if cmake --version | grep -q "$CMAKE_VERSION"; then
|
||||
echo "CMake $CMAKE_VERSION is already installed."
|
||||
exit 0
|
||||
elif [ -e /usr/local/bin/cmake ]; then
|
||||
echo "Warning: existing cmake found at /usr/local/bin/cmake. Skipping installation."
|
||||
exit 0
|
||||
elif [ -e /etc/apt/sources.list.d/kitware.list ]; then
|
||||
echo "Warning: existing KitWare repository found. Skipping installation."
|
||||
exit 0
|
||||
elif [ -e /etc/lsb-release ] && grep -qP 'DISTRIB_ID=Ubuntu' /etc/lsb-release; then
|
||||
curl -fsSL https://apt.kitware.com/keys/kitware-archive-latest.asc \
|
||||
| $SUDO gpg --dearmor - \
|
||||
| $SUDO tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null
|
||||
echo "deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ $(lsb_release -cs) main" \
|
||||
| $SUDO tee /etc/apt/sources.list.d/kitware.list >/dev/null
|
||||
$SUDO apt-get update
|
||||
$SUDO apt-get install -y kitware-archive-keyring cmake==$CMAKE_VERSION
|
||||
else
|
||||
arch="$(uname -m)"
|
||||
if [ "$arch" != "x86_64" ] && [ "$arch" != "aarch64" ]; then
|
||||
echo "Error. Only x86_64 and aarch64 are supported."
|
||||
exit 1
|
||||
fi
|
||||
tempdir=$(mktemp -d)
|
||||
pushd "$tempdir"
|
||||
CMAKE_REPO="https://github.com/Kitware/CMake"
|
||||
CMAKE_INSTALLER="cmake-$CMAKE_VERSION-linux-$arch.sh"
|
||||
curl -fsSL --output cmake-$CMAKE_VERSION-SHA-256.txt \
|
||||
"$CMAKE_REPO/releases/download/v$CMAKE_VERSION/cmake-$CMAKE_VERSION-SHA-256.txt"
|
||||
curl -fsSL --output $CMAKE_INSTALLER \
|
||||
"$CMAKE_REPO/releases/download/v$CMAKE_VERSION/cmake-$CMAKE_VERSION-linux-$arch.sh"
|
||||
# workaround for old versions of sha256sum not having --ignore-missing
|
||||
grep -F "cmake-$CMAKE_VERSION-linux-$arch.sh" "cmake-$CMAKE_VERSION-SHA-256.txt" \
|
||||
| sha256sum -c \
|
||||
| grep -qP "^${CMAKE_INSTALLER}: OK"
|
||||
chmod +x cmake-$CMAKE_VERSION-linux-$arch.sh
|
||||
$SUDO ./cmake-$CMAKE_VERSION-linux-$arch.sh --prefix=/usr/local --skip-license
|
||||
popd
|
||||
rm -rf "$tempdir"
|
||||
fi
|
||||
37
script/install-mold
Executable file
37
script/install-mold
Executable file
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Install `mold` official binaries from GitHub Releases.
|
||||
#
|
||||
# Adapted from the official rui314/setup-mold@v1 action to:
|
||||
# * use environment variables instead of action inputs
|
||||
# * remove make-default support
|
||||
# * use curl instead of wget
|
||||
# * support doas for sudo
|
||||
# * support redhat systems
|
||||
# See: https://github.com/rui314/setup-mold/blob/main/action.yml
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
MOLD_VERSION="${MOLD_VERSION:-${1:-}}"
|
||||
if [ "$(uname -s)" != "Linux" ]; then
|
||||
echo "Error: This script is intended for Linux systems only."
|
||||
exit 1
|
||||
elif [ -z "$MOLD_VERSION" ]; then
|
||||
echo "Usage: $0 2.34.0"
|
||||
exit 1
|
||||
elif [ -e /usr/local/bin/mold ]; then
|
||||
echo "Warning: existing mold found at /usr/local/bin/mold. Skipping installation."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$(whoami)" = root ]; then SUDO=; else SUDO="$(command -v sudo || command -v doas || true)"; fi
|
||||
|
||||
MOLD_REPO="${MOLD_REPO:-https://github.com/rui314/mold}"
|
||||
MOLD_URL="${MOLD_URL:-$MOLD_REPO}/releases/download/v$MOLD_VERSION/mold-$MOLD_VERSION-$(uname -m)-linux.tar.gz"
|
||||
|
||||
echo "Downloading from $MOLD_URL"
|
||||
curl -fsSL --output - "$MOLD_URL" \
|
||||
| $SUDO tar -C /usr/local --strip-components=1 --no-overwrite-dir -xzf -
|
||||
|
||||
# Note this binary depends on the system libatomic.so.1 which is usually
|
||||
# provided as a dependency of gcc so it should be available on most systems.
|
||||
112
script/linux
112
script/linux
@@ -1,15 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
set -xeuo pipefail
|
||||
|
||||
# install the wasm toolchain
|
||||
which rustup > /dev/null 2>&1 || curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
# if root or if sudo/unavailable, define an empty variable
|
||||
if [ "$(id -u)" -eq 0 ]
|
||||
then maysudo=''
|
||||
else maysudo="$(command -v sudo || command -v doas || true)"
|
||||
fi
|
||||
|
||||
# if sudo is not installed, define an empty alias
|
||||
maysudo=$(command -v sudo || command -v doas || true)
|
||||
function finalize {
|
||||
# after packages install (curl, etc), get the rust toolchain
|
||||
which rustup > /dev/null 2>&1 || curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
# verify the mold situation
|
||||
if ! command -v mold >/dev/null 2>&1; then
|
||||
echo "Warning: Mold binaries are unavailable on your system." >&2
|
||||
echo " Builds will be slower without mold. Try: scripts/install-mold" >&2
|
||||
fi
|
||||
echo "Finished installing Linux dependencies with script/linux"
|
||||
}
|
||||
|
||||
# Ubuntu, Debian, etc.
|
||||
# https://packages.ubuntu.com/
|
||||
# Ubuntu, Debian, Mint, Kali, Pop!_OS, Raspbian, etc.
|
||||
apt=$(command -v apt-get || true)
|
||||
if [[ -n $apt ]]; then
|
||||
deps=(
|
||||
@@ -20,55 +30,95 @@ if [[ -n $apt ]]; then
|
||||
libwayland-dev
|
||||
libxkbcommon-x11-dev
|
||||
libssl-dev
|
||||
libstdc++-12-dev
|
||||
libzstd-dev
|
||||
libvulkan1
|
||||
libgit2-dev
|
||||
make
|
||||
cmake
|
||||
clang
|
||||
mold
|
||||
jq
|
||||
git
|
||||
curl
|
||||
gettext-base
|
||||
elfutils
|
||||
libsqlite3-dev
|
||||
)
|
||||
if (grep -qP 'PRETTY_NAME="(.+24\.04)' /etc/os-release); then
|
||||
deps+=( mold libstdc++-14-dev )
|
||||
elif (grep -qP 'PRETTY_NAME="((Debian|Raspbian).+12|.+22\.04)' /etc/os-release); then
|
||||
deps+=( mold libstdc++-12-dev )
|
||||
elif (grep -qP 'PRETTY_NAME="((Debian|Raspbian).+11|.+20\.04)' /etc/os-release); then
|
||||
deps+=( libstdc++-10-dev )
|
||||
fi
|
||||
|
||||
$maysudo "$apt" update
|
||||
$maysudo "$apt" install -y "${deps[@]}"
|
||||
finalize
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Fedora, CentOS, RHEL, etc.
|
||||
# https://packages.fedoraproject.org/
|
||||
# Fedora, CentOS, RHEL, Alma, Amazon 2023, Oracle, etc.
|
||||
dnf=$(command -v dnf || true)
|
||||
if [[ -n $dnf ]]; then
|
||||
# Old Redhat (yum only): Amazon Linux 2, Oracle Linux 7, etc.
|
||||
yum=$(command -v yum || true)
|
||||
|
||||
if [[ -n $dnf ]] || [[ -n $yum ]]; then
|
||||
pkg_cmd="${dnf:-${yum}}"
|
||||
deps=(
|
||||
gcc
|
||||
g++
|
||||
clang
|
||||
cmake
|
||||
mold
|
||||
alsa-lib-devel
|
||||
fontconfig-devel
|
||||
wayland-devel
|
||||
libxkbcommon-x11-devel
|
||||
openssl-devel
|
||||
libzstd-devel
|
||||
# Perl dependencies are needed for openssl-sys crate see https://docs.rs/openssl/latest/openssl/
|
||||
perl-FindBin
|
||||
perl-IPC-Cmd
|
||||
perl-File-Compare
|
||||
perl-File-Copy
|
||||
vulkan-loader
|
||||
sqlite-devel
|
||||
jq
|
||||
git
|
||||
tar
|
||||
)
|
||||
|
||||
# libxkbcommon-x11-devel is in the crb repo on RHEL and CentOS, not needed for Fedora
|
||||
if ! grep -q "Fedora" /etc/redhat-release; then
|
||||
$maysudo "$dnf" config-manager --set-enabled crb
|
||||
# perl used for building openssl-sys crate. See: https://docs.rs/openssl/latest/openssl/
|
||||
if grep -qP '^ID="(fedora)' /etc/os-release; then
|
||||
deps+=(
|
||||
perl-FindBin
|
||||
perl-IPC-Cmd
|
||||
perl-File-Compare
|
||||
perl-File-Copy
|
||||
mold
|
||||
)
|
||||
elif grep grep -qP '^ID="(rhel|rocky|alma|centos|ol)' /etc/os-release; then
|
||||
deps+=( perl-interpreter )
|
||||
fi
|
||||
|
||||
$maysudo "$dnf" install -y "${deps[@]}"
|
||||
# gcc-c++ is g++ on RHEL8 and 8.x clones
|
||||
if grep -qP '^ID="(rhel|rocky|alma|centos|ol)' /etc/os-release \
|
||||
&& grep -qP '^VERSION_ID="8' /etc/os-release; then
|
||||
deps+=( gcc-c++ )
|
||||
else
|
||||
deps+=( g++ )
|
||||
fi
|
||||
|
||||
# libxkbcommon-x11-devel is in a non-default repo on RHEL 8.x/9.x (except on AmazonLinux)
|
||||
if grep -qP '^VERSION_ID="(8|9)' && grep -qP '^ID="(rhel|rocky|centos|alma|ol)' /etc/os-release; then
|
||||
$maysudo dnf install -y 'dnf-command(config-manager)'
|
||||
if grep -qP '^PRETTY_NAME="(AlmaLinux 8|Rocky Linux 8)' /etc/os-release; then
|
||||
$maysudo dnf config-manager --set-enabled powertools
|
||||
elif grep -qP '^PRETTY_NAME="((AlmaLinux|Rocky|CentOS Stream) 9|Red Hat.+(8|9))' /etc/os-release; then
|
||||
$maysudo dnf config-manager --set-enabled crb
|
||||
elif grep -qP '^PRETTY_NAME="Oracle Linux Server 8' /etc/os-release; then
|
||||
$maysudo dnf config-manager --set-enabled ol8_codeready_builder
|
||||
elif grep -qP '^PRETTY_NAME="Oracle Linux Server 9' /etc/os-release; then
|
||||
$maysudo dnf config-manager --set-enabled ol9_codeready_builder
|
||||
else
|
||||
echo "Unexpected distro" && grep 'PRETTY_NAME' /etc/os-release && exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
$maysudo $pkg_cmd install -y "${deps[@]}"
|
||||
finalize
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -89,10 +139,14 @@ if [[ -n $zyp ]]; then
|
||||
openssl-devel
|
||||
libzstd-devel
|
||||
libvulkan1
|
||||
mold
|
||||
sqlite3-devel
|
||||
jq
|
||||
git
|
||||
tar
|
||||
gzip
|
||||
)
|
||||
$maysudo "$zyp" install -y "${deps[@]}"
|
||||
finalize
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -115,8 +169,10 @@ if [[ -n $pacman ]]; then
|
||||
mold
|
||||
sqlite
|
||||
jq
|
||||
git
|
||||
)
|
||||
$maysudo "$pacman" -S --needed --noconfirm "${deps[@]}"
|
||||
$maysudo "$pacman" -Syu --needed --noconfirm "${deps[@]}"
|
||||
finalize
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -143,6 +199,7 @@ if [[ -n $xbps ]]; then
|
||||
sqlite-devel
|
||||
)
|
||||
$maysudo "$xbps" -Syu "${deps[@]}"
|
||||
finalize
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -152,6 +209,7 @@ emerge=$(command -v emerge || true)
|
||||
if [[ -n $emerge ]]; then
|
||||
deps=(
|
||||
app-arch/zstd
|
||||
app-misc/jq
|
||||
dev-libs/openssl
|
||||
dev-libs/wayland
|
||||
dev-util/cmake
|
||||
@@ -164,7 +222,9 @@ if [[ -n $emerge ]]; then
|
||||
dev-db/sqlite
|
||||
)
|
||||
$maysudo "$emerge" -u "${deps[@]}"
|
||||
finalize
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Unsupported Linux distribution in script/linux"
|
||||
exit 1
|
||||
|
||||
Reference in New Issue
Block a user