Compare commits
30 Commits
overhaul-m
...
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 | ||
|
|
250f2e76eb | ||
|
|
5f35fa5d92 |
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
Cargo.lock
generated
26
Cargo.lock
generated
@@ -7065,32 +7065,6 @@ dependencies = [
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multi_buffer2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clock",
|
||||
"collections",
|
||||
"ctor",
|
||||
"env_logger",
|
||||
"futures 0.3.30",
|
||||
"gpui",
|
||||
"itertools 0.13.0",
|
||||
"language",
|
||||
"log",
|
||||
"parking_lot",
|
||||
"rand 0.8.5",
|
||||
"rpc",
|
||||
"serde",
|
||||
"settings",
|
||||
"smallvec",
|
||||
"sum_tree",
|
||||
"text",
|
||||
"theme",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "multimap"
|
||||
version = "0.8.3"
|
||||
|
||||
@@ -67,7 +67,6 @@ members = [
|
||||
"crates/media",
|
||||
"crates/menu",
|
||||
"crates/multi_buffer",
|
||||
"crates/multi_buffer2",
|
||||
"crates/node_runtime",
|
||||
"crates/notifications",
|
||||
"crates/ollama",
|
||||
|
||||
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
|
||||
@@ -769,7 +779,8 @@
|
||||
"**/Zed/**/*.json",
|
||||
"tsconfig.json",
|
||||
"pyrightconfig.json"
|
||||
]
|
||||
],
|
||||
"TOML": ["uv.lock"]
|
||||
},
|
||||
/// By default use a recent system version of node, or install our own.
|
||||
/// You can override this to use a version of node that is not in $PATH with:
|
||||
|
||||
@@ -599,7 +599,7 @@ impl Context {
|
||||
});
|
||||
let message = MessageAnchor {
|
||||
id: first_message_id,
|
||||
start: this.buffer.read(cx).min_anchor(),
|
||||
start: language::Anchor::MIN,
|
||||
};
|
||||
this.messages_metadata.insert(
|
||||
first_message_id,
|
||||
@@ -862,9 +862,11 @@ impl Context {
|
||||
}
|
||||
|
||||
match op {
|
||||
ContextOperation::InsertMessage { anchor, .. } => {
|
||||
self.buffer.read(cx).can_resolve(&anchor.start)
|
||||
}
|
||||
ContextOperation::InsertMessage { anchor, .. } => self
|
||||
.buffer
|
||||
.read(cx)
|
||||
.version
|
||||
.observed(anchor.start.timestamp),
|
||||
ContextOperation::UpdateMessage { message_id, .. } => {
|
||||
self.messages_metadata.contains_key(message_id)
|
||||
}
|
||||
@@ -874,12 +876,20 @@ impl Context {
|
||||
sections,
|
||||
..
|
||||
} => {
|
||||
let buffer = self.buffer.read(cx);
|
||||
let version = &self.buffer.read(cx).version;
|
||||
sections
|
||||
.iter()
|
||||
.map(|section| §ion.range)
|
||||
.chain([output_range])
|
||||
.all(|range| buffer.can_resolve(&range.start) && buffer.can_resolve(&range.end))
|
||||
.all(|range| {
|
||||
let observed_start = range.start == language::Anchor::MIN
|
||||
|| range.start == language::Anchor::MAX
|
||||
|| version.observed(range.start.timestamp);
|
||||
let observed_end = range.end == language::Anchor::MIN
|
||||
|| range.end == language::Anchor::MAX
|
||||
|| version.observed(range.end.timestamp);
|
||||
observed_start && observed_end
|
||||
})
|
||||
}
|
||||
ContextOperation::BufferOperation(_) => {
|
||||
panic!("buffer operations should always be applied")
|
||||
@@ -1553,7 +1563,7 @@ impl Context {
|
||||
}
|
||||
|
||||
if let Some(mut pending_step) = pending_step {
|
||||
pending_step.range.end = buffer.max_anchor();
|
||||
pending_step.range.end = text::Anchor::MAX;
|
||||
new_steps.push(pending_step);
|
||||
}
|
||||
|
||||
@@ -2697,7 +2707,7 @@ impl Context {
|
||||
messages.next();
|
||||
}
|
||||
}
|
||||
let message_end_anchor = message_end.unwrap_or(buffer.max_anchor());
|
||||
let message_end_anchor = message_end.unwrap_or(language::Anchor::MAX);
|
||||
let message_end = message_end_anchor.to_offset(buffer);
|
||||
|
||||
return Some(Message {
|
||||
@@ -2922,7 +2932,6 @@ impl SavedContext {
|
||||
buffer: &Model<Buffer>,
|
||||
cx: &mut ModelContext<Context>,
|
||||
) -> Vec<ContextOperation> {
|
||||
let buffer = buffer.read(cx);
|
||||
let mut operations = Vec::new();
|
||||
let mut version = clock::Global::new();
|
||||
let mut next_timestamp = clock::Lamport::new(ReplicaId::default());
|
||||
@@ -2935,7 +2944,7 @@ impl SavedContext {
|
||||
operations.push(ContextOperation::InsertMessage {
|
||||
anchor: MessageAnchor {
|
||||
id: message.id,
|
||||
start: buffer.anchor_before(message.start),
|
||||
start: buffer.read(cx).anchor_before(message.start),
|
||||
},
|
||||
metadata: MessageMetadata {
|
||||
role: message.metadata.role,
|
||||
@@ -2968,16 +2977,19 @@ impl SavedContext {
|
||||
let timestamp = next_timestamp.tick();
|
||||
operations.push(ContextOperation::SlashCommandFinished {
|
||||
id: SlashCommandId(timestamp),
|
||||
output_range: buffer.min_anchor()..buffer.max_anchor(),
|
||||
output_range: language::Anchor::MIN..language::Anchor::MAX,
|
||||
sections: self
|
||||
.slash_command_output_sections
|
||||
.into_iter()
|
||||
.map(|section| SlashCommandOutputSection {
|
||||
range: buffer.anchor_after(section.range.start)
|
||||
..buffer.anchor_before(section.range.end),
|
||||
icon: section.icon,
|
||||
label: section.label,
|
||||
metadata: section.metadata,
|
||||
.map(|section| {
|
||||
let buffer = buffer.read(cx);
|
||||
SlashCommandOutputSection {
|
||||
range: buffer.anchor_after(section.range.start)
|
||||
..buffer.anchor_before(section.range.end),
|
||||
icon: section.icon,
|
||||
label: section.label,
|
||||
metadata: section.metadata,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
version: version.clone(),
|
||||
|
||||
@@ -64,15 +64,14 @@ pub enum WorkflowSuggestion {
|
||||
|
||||
impl WorkflowSuggestion {
|
||||
pub fn range(&self) -> Range<language::Anchor> {
|
||||
todo!()
|
||||
// match self {
|
||||
// Self::Update { range, .. } => range.clone(),
|
||||
// Self::CreateFile { .. } => language::Anchor::MIN..language::Anchor::MAX,
|
||||
// Self::InsertBefore { position, .. } | Self::InsertAfter { position, .. } => {
|
||||
// *position..*position
|
||||
// }
|
||||
// Self::Delete { range, .. } => range.clone(),
|
||||
// }
|
||||
match self {
|
||||
Self::Update { range, .. } => range.clone(),
|
||||
Self::CreateFile { .. } => language::Anchor::MIN..language::Anchor::MAX,
|
||||
Self::InsertBefore { position, .. } | Self::InsertAfter { position, .. } => {
|
||||
*position..*position
|
||||
}
|
||||
Self::Delete { range, .. } => range.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn description(&self) -> Option<&str> {
|
||||
|
||||
@@ -1198,7 +1198,7 @@ async fn test_share_project(
|
||||
buffer_a.read_with(cx_a, |buffer, _| {
|
||||
buffer
|
||||
.snapshot()
|
||||
.selections_in_range(buffer.min_anchor()..buffer.max_anchor(), false)
|
||||
.selections_in_range(text::Anchor::MIN..text::Anchor::MAX, false)
|
||||
.count()
|
||||
== 1
|
||||
});
|
||||
@@ -1237,7 +1237,7 @@ async fn test_share_project(
|
||||
buffer_a.read_with(cx_a, |buffer, _| {
|
||||
buffer
|
||||
.snapshot()
|
||||
.selections_in_range(buffer.min_anchor()..buffer.max_anchor(), false)
|
||||
.selections_in_range(text::Anchor::MIN..text::Anchor::MAX, false)
|
||||
.count()
|
||||
== 0
|
||||
});
|
||||
|
||||
@@ -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(),
|
||||
)));
|
||||
|
||||
@@ -429,7 +429,7 @@ impl ProjectDiagnosticsEditor {
|
||||
prev_excerpt_id = excerpt_id;
|
||||
first_excerpt_id.get_or_insert(prev_excerpt_id);
|
||||
group_state.excerpts.push(excerpt_id);
|
||||
let header_position = (excerpt_id, snapshot.min_anchor());
|
||||
let header_position = (excerpt_id, language::Anchor::MIN);
|
||||
|
||||
if is_first_excerpt_for_group {
|
||||
is_first_excerpt_for_group = false;
|
||||
|
||||
@@ -1194,7 +1194,7 @@ mod tests {
|
||||
use rand::prelude::*;
|
||||
use settings::SettingsStore;
|
||||
use std::{cmp::Reverse, env, sync::Arc};
|
||||
use text::{BufferId, Patch};
|
||||
use text::Patch;
|
||||
use util::post_inc;
|
||||
|
||||
#[test]
|
||||
@@ -1205,9 +1205,7 @@ mod tests {
|
||||
Anchor::min(),
|
||||
&InlayHint {
|
||||
label: InlayHintLabel::String("a".to_string()),
|
||||
position: text::Anchor::Start {
|
||||
buffer_id: BufferId::new(1).unwrap()
|
||||
},
|
||||
position: text::Anchor::default(),
|
||||
padding_left: false,
|
||||
padding_right: false,
|
||||
tooltip: None,
|
||||
@@ -1227,9 +1225,7 @@ mod tests {
|
||||
Anchor::min(),
|
||||
&InlayHint {
|
||||
label: InlayHintLabel::String("a".to_string()),
|
||||
position: text::Anchor::Start {
|
||||
buffer_id: BufferId::new(1).unwrap()
|
||||
},
|
||||
position: text::Anchor::default(),
|
||||
padding_left: true,
|
||||
padding_right: true,
|
||||
tooltip: None,
|
||||
@@ -1249,9 +1245,7 @@ mod tests {
|
||||
Anchor::min(),
|
||||
&InlayHint {
|
||||
label: InlayHintLabel::String(" a ".to_string()),
|
||||
position: text::Anchor::Start {
|
||||
buffer_id: BufferId::new(1).unwrap()
|
||||
},
|
||||
position: text::Anchor::default(),
|
||||
padding_left: false,
|
||||
padding_right: false,
|
||||
tooltip: None,
|
||||
@@ -1271,9 +1265,7 @@ mod tests {
|
||||
Anchor::min(),
|
||||
&InlayHint {
|
||||
label: InlayHintLabel::String(" a ".to_string()),
|
||||
position: text::Anchor::Start {
|
||||
buffer_id: BufferId::new(1).unwrap()
|
||||
},
|
||||
position: text::Anchor::default(),
|
||||
padding_left: true,
|
||||
padding_right: true,
|
||||
tooltip: None,
|
||||
|
||||
@@ -3119,60 +3119,59 @@ impl Editor {
|
||||
|
||||
fn linked_editing_ranges_for(
|
||||
&self,
|
||||
_selection: Range<text::Anchor>,
|
||||
_cx: &AppContext,
|
||||
selection: Range<text::Anchor>,
|
||||
cx: &AppContext,
|
||||
) -> Option<HashMap<Model<Buffer>, Vec<Range<text::Anchor>>>> {
|
||||
todo!()
|
||||
// if self.linked_edit_ranges.is_empty() {
|
||||
// return None;
|
||||
// }
|
||||
// let ((base_range, linked_ranges), buffer_snapshot, buffer) =
|
||||
// selection.end.buffer_id.and_then(|end_buffer_id| {
|
||||
// if selection.start.buffer_id != Some(end_buffer_id) {
|
||||
// return None;
|
||||
// }
|
||||
// let buffer = self.buffer.read(cx).buffer(end_buffer_id)?;
|
||||
// let snapshot = buffer.read(cx).snapshot();
|
||||
// self.linked_edit_ranges
|
||||
// .get(end_buffer_id, selection.start..selection.end, &snapshot)
|
||||
// .map(|ranges| (ranges, snapshot, buffer))
|
||||
// })?;
|
||||
// use text::ToOffset as TO;
|
||||
// // find offset from the start of current range to current cursor position
|
||||
// let start_byte_offset = TO::to_offset(&base_range.start, &buffer_snapshot);
|
||||
if self.linked_edit_ranges.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let ((base_range, linked_ranges), buffer_snapshot, buffer) =
|
||||
selection.end.buffer_id.and_then(|end_buffer_id| {
|
||||
if selection.start.buffer_id != Some(end_buffer_id) {
|
||||
return None;
|
||||
}
|
||||
let buffer = self.buffer.read(cx).buffer(end_buffer_id)?;
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
self.linked_edit_ranges
|
||||
.get(end_buffer_id, selection.start..selection.end, &snapshot)
|
||||
.map(|ranges| (ranges, snapshot, buffer))
|
||||
})?;
|
||||
use text::ToOffset as TO;
|
||||
// find offset from the start of current range to current cursor position
|
||||
let start_byte_offset = TO::to_offset(&base_range.start, &buffer_snapshot);
|
||||
|
||||
// let start_offset = TO::to_offset(&selection.start, &buffer_snapshot);
|
||||
// let start_difference = start_offset - start_byte_offset;
|
||||
// let end_offset = TO::to_offset(&selection.end, &buffer_snapshot);
|
||||
// let end_difference = end_offset - start_byte_offset;
|
||||
// // Current range has associated linked ranges.
|
||||
// let mut linked_edits = HashMap::<_, Vec<_>>::default();
|
||||
// for range in linked_ranges.iter() {
|
||||
// let start_offset = TO::to_offset(&range.start, &buffer_snapshot);
|
||||
// let end_offset = start_offset + end_difference;
|
||||
// let start_offset = start_offset + start_difference;
|
||||
// if start_offset > buffer_snapshot.len() || end_offset > buffer_snapshot.len() {
|
||||
// continue;
|
||||
// }
|
||||
// if self.selections.disjoint_anchor_ranges().iter().any(|s| {
|
||||
// if s.start.buffer_id != selection.start.buffer_id
|
||||
// || s.end.buffer_id != selection.end.buffer_id
|
||||
// {
|
||||
// return false;
|
||||
// }
|
||||
// TO::to_offset(&s.start.text_anchor, &buffer_snapshot) <= end_offset
|
||||
// && TO::to_offset(&s.end.text_anchor, &buffer_snapshot) >= start_offset
|
||||
// }) {
|
||||
// continue;
|
||||
// }
|
||||
// let start = buffer_snapshot.anchor_after(start_offset);
|
||||
// let end = buffer_snapshot.anchor_after(end_offset);
|
||||
// linked_edits
|
||||
// .entry(buffer.clone())
|
||||
// .or_default()
|
||||
// .push(start..end);
|
||||
// }
|
||||
// Some(linked_edits)
|
||||
let start_offset = TO::to_offset(&selection.start, &buffer_snapshot);
|
||||
let start_difference = start_offset - start_byte_offset;
|
||||
let end_offset = TO::to_offset(&selection.end, &buffer_snapshot);
|
||||
let end_difference = end_offset - start_byte_offset;
|
||||
// Current range has associated linked ranges.
|
||||
let mut linked_edits = HashMap::<_, Vec<_>>::default();
|
||||
for range in linked_ranges.iter() {
|
||||
let start_offset = TO::to_offset(&range.start, &buffer_snapshot);
|
||||
let end_offset = start_offset + end_difference;
|
||||
let start_offset = start_offset + start_difference;
|
||||
if start_offset > buffer_snapshot.len() || end_offset > buffer_snapshot.len() {
|
||||
continue;
|
||||
}
|
||||
if self.selections.disjoint_anchor_ranges().iter().any(|s| {
|
||||
if s.start.buffer_id != selection.start.buffer_id
|
||||
|| s.end.buffer_id != selection.end.buffer_id
|
||||
{
|
||||
return false;
|
||||
}
|
||||
TO::to_offset(&s.start.text_anchor, &buffer_snapshot) <= end_offset
|
||||
&& TO::to_offset(&s.end.text_anchor, &buffer_snapshot) >= start_offset
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
let start = buffer_snapshot.anchor_after(start_offset);
|
||||
let end = buffer_snapshot.anchor_after(end_offset);
|
||||
linked_edits
|
||||
.entry(buffer.clone())
|
||||
.or_default()
|
||||
.push(start..end);
|
||||
}
|
||||
Some(linked_edits)
|
||||
}
|
||||
|
||||
pub fn handle_input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
|
||||
@@ -5641,7 +5640,7 @@ impl Editor {
|
||||
for selection in selections.iter() {
|
||||
let selection_start = snapshot.anchor_before(selection.start).text_anchor;
|
||||
let selection_end = snapshot.anchor_after(selection.end).text_anchor;
|
||||
if selection_start.buffer_id() != selection_end.buffer_id() {
|
||||
if selection_start.buffer_id != selection_end.buffer_id {
|
||||
continue;
|
||||
}
|
||||
if let Some(ranges) =
|
||||
@@ -11658,14 +11657,13 @@ impl Editor {
|
||||
let start = highlight.range.start.to_display_point(&snapshot);
|
||||
let end = highlight.range.end.to_display_point(&snapshot);
|
||||
let start_row = start.row().0;
|
||||
let end_row =
|
||||
if !matches!(highlight.range.end.text_anchor, text::Anchor::End { .. })
|
||||
&& end.column() == 0
|
||||
{
|
||||
end.row().0.saturating_sub(1)
|
||||
} else {
|
||||
end.row().0
|
||||
};
|
||||
let end_row = if highlight.range.end.text_anchor != text::Anchor::MAX
|
||||
&& end.column() == 0
|
||||
{
|
||||
end.row().0.saturating_sub(1)
|
||||
} else {
|
||||
end.row().0
|
||||
};
|
||||
for row in start_row..=end_row {
|
||||
let used_index =
|
||||
used_highlight_orders.entry(row).or_insert(highlight.index);
|
||||
@@ -12969,7 +12967,7 @@ fn snippet_completions(
|
||||
return vec![];
|
||||
}
|
||||
let snapshot = buffer.read(cx).text_snapshot();
|
||||
let chunks = snapshot.reversed_chunks_in_range(snapshot.min_anchor()..buffer_position);
|
||||
let chunks = snapshot.reversed_chunks_in_range(text::Anchor::MIN..buffer_position);
|
||||
|
||||
let mut lines = chunks.lines();
|
||||
let Some(line_at) = lines.next().filter(|line| !line.is_empty()) else {
|
||||
|
||||
@@ -738,12 +738,7 @@ async fn test_navigation_history(cx: &mut TestAppContext) {
|
||||
|
||||
// Ensure we don't panic when navigation data contains invalid anchors *and* points.
|
||||
let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
|
||||
invalid_anchor.text_anchor = text::Anchor::Character {
|
||||
buffer_id: invalid_anchor.text_anchor.buffer_id(),
|
||||
insertion_id: clock::Lamport::MAX,
|
||||
offset: 42,
|
||||
bias: Bias::Left,
|
||||
};
|
||||
invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
|
||||
let invalid_point = Point::new(9999, 0);
|
||||
editor.navigate(
|
||||
Box::new(NavigationData {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1646,7 +1665,16 @@ impl EditorElement {
|
||||
return None;
|
||||
}
|
||||
if snapshot.is_line_folded(multibuffer_row) {
|
||||
return None;
|
||||
// Skip folded indicators, unless it's the starting line of a fold.
|
||||
if multibuffer_row
|
||||
.0
|
||||
.checked_sub(1)
|
||||
.map_or(false, |previous_row| {
|
||||
snapshot.is_line_folded(MultiBufferRow(previous_row))
|
||||
})
|
||||
{
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let button = editor.render_run_indicator(
|
||||
&self.style,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -169,66 +169,65 @@ impl TasksForRanges {
|
||||
|
||||
fn remove_cached_ranges_from_query(
|
||||
&mut self,
|
||||
_buffer_snapshot: &BufferSnapshot,
|
||||
_query_range: Range<language::Anchor>,
|
||||
buffer_snapshot: &BufferSnapshot,
|
||||
query_range: Range<language::Anchor>,
|
||||
) -> Vec<Range<language::Anchor>> {
|
||||
todo!()
|
||||
// let mut ranges_to_query = Vec::new();
|
||||
// let mut latest_cached_range = None::<&mut Range<language::Anchor>>;
|
||||
// for cached_range in self
|
||||
// .sorted_ranges
|
||||
// .iter_mut()
|
||||
// .skip_while(|cached_range| {
|
||||
// cached_range
|
||||
// .end
|
||||
// .cmp(&query_range.start, buffer_snapshot)
|
||||
// .is_lt()
|
||||
// })
|
||||
// .take_while(|cached_range| {
|
||||
// cached_range
|
||||
// .start
|
||||
// .cmp(&query_range.end, buffer_snapshot)
|
||||
// .is_le()
|
||||
// })
|
||||
// {
|
||||
// match latest_cached_range {
|
||||
// Some(latest_cached_range) => {
|
||||
// if latest_cached_range.end.offset.saturating_add(1) < cached_range.start.offset
|
||||
// {
|
||||
// ranges_to_query.push(latest_cached_range.end..cached_range.start);
|
||||
// cached_range.start = latest_cached_range.end;
|
||||
// }
|
||||
// }
|
||||
// None => {
|
||||
// if query_range
|
||||
// .start
|
||||
// .cmp(&cached_range.start, buffer_snapshot)
|
||||
// .is_lt()
|
||||
// {
|
||||
// ranges_to_query.push(query_range.start..cached_range.start);
|
||||
// cached_range.start = query_range.start;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// latest_cached_range = Some(cached_range);
|
||||
// }
|
||||
let mut ranges_to_query = Vec::new();
|
||||
let mut latest_cached_range = None::<&mut Range<language::Anchor>>;
|
||||
for cached_range in self
|
||||
.sorted_ranges
|
||||
.iter_mut()
|
||||
.skip_while(|cached_range| {
|
||||
cached_range
|
||||
.end
|
||||
.cmp(&query_range.start, buffer_snapshot)
|
||||
.is_lt()
|
||||
})
|
||||
.take_while(|cached_range| {
|
||||
cached_range
|
||||
.start
|
||||
.cmp(&query_range.end, buffer_snapshot)
|
||||
.is_le()
|
||||
})
|
||||
{
|
||||
match latest_cached_range {
|
||||
Some(latest_cached_range) => {
|
||||
if latest_cached_range.end.offset.saturating_add(1) < cached_range.start.offset
|
||||
{
|
||||
ranges_to_query.push(latest_cached_range.end..cached_range.start);
|
||||
cached_range.start = latest_cached_range.end;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if query_range
|
||||
.start
|
||||
.cmp(&cached_range.start, buffer_snapshot)
|
||||
.is_lt()
|
||||
{
|
||||
ranges_to_query.push(query_range.start..cached_range.start);
|
||||
cached_range.start = query_range.start;
|
||||
}
|
||||
}
|
||||
}
|
||||
latest_cached_range = Some(cached_range);
|
||||
}
|
||||
|
||||
// match latest_cached_range {
|
||||
// Some(latest_cached_range) => {
|
||||
// if latest_cached_range.end.offset.saturating_add(1) < query_range.end.offset {
|
||||
// ranges_to_query.push(latest_cached_range.end..query_range.end);
|
||||
// latest_cached_range.end = query_range.end;
|
||||
// }
|
||||
// }
|
||||
// None => {
|
||||
// ranges_to_query.push(query_range.clone());
|
||||
// self.sorted_ranges.push(query_range);
|
||||
// self.sorted_ranges
|
||||
// .sort_by(|range_a, range_b| range_a.start.cmp(&range_b.start, buffer_snapshot));
|
||||
// }
|
||||
// }
|
||||
match latest_cached_range {
|
||||
Some(latest_cached_range) => {
|
||||
if latest_cached_range.end.offset.saturating_add(1) < query_range.end.offset {
|
||||
ranges_to_query.push(latest_cached_range.end..query_range.end);
|
||||
latest_cached_range.end = query_range.end;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
ranges_to_query.push(query_range.clone());
|
||||
self.sorted_ranges.push(query_range);
|
||||
self.sorted_ranges
|
||||
.sort_by(|range_a, range_b| range_a.start.cmp(&range_b.start, buffer_snapshot));
|
||||
}
|
||||
}
|
||||
|
||||
// ranges_to_query
|
||||
ranges_to_query
|
||||
}
|
||||
|
||||
fn invalidate_range(&mut self, buffer: &BufferSnapshot, range: &Range<language::Anchor>) {
|
||||
|
||||
@@ -15,7 +15,6 @@ pub(super) struct LinkedEditingRanges(
|
||||
);
|
||||
|
||||
impl LinkedEditingRanges {
|
||||
#[allow(dead_code)] // todo!()
|
||||
pub(super) fn get(
|
||||
&self,
|
||||
id: BufferId,
|
||||
|
||||
@@ -41,7 +41,7 @@ impl sum_tree::Item for InternalDiffHunk {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct DiffHunkSummary {
|
||||
buffer_range: Range<Anchor>,
|
||||
}
|
||||
@@ -49,10 +49,8 @@ pub struct DiffHunkSummary {
|
||||
impl sum_tree::Summary for DiffHunkSummary {
|
||||
type Context = text::BufferSnapshot;
|
||||
|
||||
fn zero(buffer: &Self::Context) -> Self {
|
||||
Self {
|
||||
buffer_range: buffer.min_anchor()..buffer.min_anchor(),
|
||||
}
|
||||
fn zero(_cx: &Self::Context) -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn add_summary(&mut self, other: &Self, buffer: &Self::Context) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -835,7 +835,7 @@ impl Buffer {
|
||||
branch
|
||||
.edits_since_in_range::<usize>(
|
||||
&self.version,
|
||||
range.unwrap_or(self.min_anchor()..self.max_anchor()),
|
||||
range.unwrap_or(Anchor::MIN..Anchor::MAX),
|
||||
)
|
||||
.map(|edit| {
|
||||
(
|
||||
|
||||
@@ -2496,17 +2496,15 @@ fn assert_diff_hunks(
|
||||
cx: &mut TestAppContext,
|
||||
expected_hunks: &[(Range<u32>, &str, &str)],
|
||||
) {
|
||||
buffer.read_with(cx, |buffer, _| {
|
||||
let snapshot = buffer.snapshot();
|
||||
let diff_base = buffer.diff_base().unwrap().to_string();
|
||||
assert_hunks(
|
||||
snapshot
|
||||
.git_diff_hunks_intersecting_range(snapshot.min_anchor()..snapshot.max_anchor()),
|
||||
&snapshot,
|
||||
&diff_base,
|
||||
expected_hunks,
|
||||
);
|
||||
let (snapshot, diff_base) = buffer.read_with(cx, |buffer, _| {
|
||||
(buffer.snapshot(), buffer.diff_base().unwrap().to_string())
|
||||
});
|
||||
assert_hunks(
|
||||
snapshot.git_diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX),
|
||||
&snapshot,
|
||||
&diff_base,
|
||||
expected_hunks,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
@@ -2788,7 +2786,7 @@ fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) {
|
||||
for buffer in &buffers {
|
||||
let buffer = buffer.read(cx).snapshot();
|
||||
let actual_remote_selections = buffer
|
||||
.selections_in_range(buffer.min_anchor()..buffer.max_anchor(), false)
|
||||
.selections_in_range(Anchor::MIN..Anchor::MAX, false)
|
||||
.map(|(replica_id, _, _, selections)| (replica_id, selections.collect::<Vec<_>>()))
|
||||
.collect::<Vec<_>>();
|
||||
let expected_remote_selections = active_selections
|
||||
|
||||
@@ -246,17 +246,23 @@ impl DiagnosticEntry<Anchor> {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Summary {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
start: Anchor::MIN,
|
||||
end: Anchor::MAX,
|
||||
min_start: Anchor::MAX,
|
||||
max_end: Anchor::MIN,
|
||||
count: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Summary for Summary {
|
||||
type Context = text::BufferSnapshot;
|
||||
|
||||
fn zero(buffer: &Self::Context) -> Self {
|
||||
Self {
|
||||
start: buffer.min_anchor(),
|
||||
end: buffer.max_anchor(),
|
||||
min_start: buffer.max_anchor(),
|
||||
max_end: buffer.min_anchor(),
|
||||
count: 0,
|
||||
}
|
||||
fn zero(_cx: &Self::Context) -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn add_summary(&mut self, other: &Self, buffer: &Self::Context) {
|
||||
|
||||
@@ -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>),
|
||||
|
||||
@@ -6,7 +6,7 @@ use clock::ReplicaId;
|
||||
use lsp::{DiagnosticSeverity, LanguageServerId};
|
||||
use rpc::proto;
|
||||
use serde_json::Value;
|
||||
use std::{ops::Range, str::FromStr, sync::Arc, u32};
|
||||
use std::{ops::Range, str::FromStr, sync::Arc};
|
||||
use text::*;
|
||||
|
||||
pub use proto::{BufferState, Operation};
|
||||
@@ -221,36 +221,15 @@ pub fn serialize_diagnostics<'a>(
|
||||
|
||||
/// Serializes an [`Anchor`] to be sent over RPC.
|
||||
pub fn serialize_anchor(anchor: &Anchor) -> proto::Anchor {
|
||||
match *anchor {
|
||||
Anchor::Start { buffer_id } => proto::Anchor {
|
||||
replica_id: 0,
|
||||
timestamp: 0,
|
||||
offset: 0,
|
||||
bias: proto::Bias::Left as i32,
|
||||
buffer_id: Some(buffer_id.into()),
|
||||
},
|
||||
Anchor::End { buffer_id } => proto::Anchor {
|
||||
replica_id: u32::MAX,
|
||||
timestamp: u32::MAX,
|
||||
offset: u64::MAX,
|
||||
bias: proto::Bias::Right as i32,
|
||||
buffer_id: Some(buffer_id.into()),
|
||||
},
|
||||
Anchor::Character {
|
||||
buffer_id,
|
||||
insertion_id,
|
||||
offset,
|
||||
bias,
|
||||
} => proto::Anchor {
|
||||
replica_id: insertion_id.replica_id as u32,
|
||||
timestamp: insertion_id.value,
|
||||
offset: offset as u64,
|
||||
bias: match bias {
|
||||
Bias::Left => proto::Bias::Left as i32,
|
||||
Bias::Right => proto::Bias::Right as i32,
|
||||
},
|
||||
buffer_id: Some(buffer_id.into()),
|
||||
proto::Anchor {
|
||||
replica_id: anchor.timestamp.replica_id as u32,
|
||||
timestamp: anchor.timestamp.value,
|
||||
offset: anchor.offset as u64,
|
||||
bias: match anchor.bias {
|
||||
Bias::Left => proto::Bias::Left as i32,
|
||||
Bias::Right => proto::Bias::Right as i32,
|
||||
},
|
||||
buffer_id: anchor.buffer_id.map(Into::into),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,33 +429,23 @@ pub fn deserialize_diagnostics(
|
||||
|
||||
/// Deserializes an [`Anchor`] from the RPC representation.
|
||||
pub fn deserialize_anchor(anchor: proto::Anchor) -> Option<Anchor> {
|
||||
let buffer_id = BufferId::new(anchor.buffer_id?).ok()?;
|
||||
if anchor.replica_id == 0
|
||||
&& anchor.timestamp == 0
|
||||
&& anchor.offset == 0
|
||||
&& anchor.bias == proto::Bias::Left as i32
|
||||
{
|
||||
Some(Anchor::Start { buffer_id })
|
||||
} else if anchor.replica_id == u32::MAX
|
||||
&& anchor.timestamp == u32::MAX
|
||||
&& anchor.offset == u64::MAX
|
||||
&& anchor.bias == proto::Bias::Right as i32
|
||||
{
|
||||
Some(Anchor::End { buffer_id })
|
||||
let buffer_id = if let Some(id) = anchor.buffer_id {
|
||||
Some(BufferId::new(id).ok()?)
|
||||
} else {
|
||||
Some(Anchor::Character {
|
||||
insertion_id: clock::Lamport {
|
||||
replica_id: anchor.replica_id as ReplicaId,
|
||||
value: anchor.timestamp,
|
||||
},
|
||||
offset: anchor.offset as usize,
|
||||
bias: match proto::Bias::from_i32(anchor.bias)? {
|
||||
proto::Bias::Left => Bias::Left,
|
||||
proto::Bias::Right => Bias::Right,
|
||||
},
|
||||
buffer_id,
|
||||
})
|
||||
}
|
||||
None
|
||||
};
|
||||
Some(Anchor {
|
||||
timestamp: clock::Lamport {
|
||||
replica_id: anchor.replica_id as ReplicaId,
|
||||
value: anchor.timestamp,
|
||||
},
|
||||
offset: anchor.offset as usize,
|
||||
bias: match proto::Bias::from_i32(anchor.bias)? {
|
||||
proto::Bias::Left => Bias::Left,
|
||||
proto::Bias::Right => Bias::Right,
|
||||
},
|
||||
buffer_id,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a `[clock::Lamport`] timestamp for the given [`proto::Operation`].
|
||||
|
||||
@@ -303,7 +303,7 @@ impl SyntaxSnapshot {
|
||||
let slice = cursor.slice(
|
||||
&SyntaxLayerPosition {
|
||||
depth: depth + 1,
|
||||
range: text.min_anchor()..text.max_anchor(),
|
||||
range: Anchor::MIN..Anchor::MAX,
|
||||
language: None,
|
||||
},
|
||||
Bias::Left,
|
||||
@@ -459,7 +459,7 @@ impl SyntaxSnapshot {
|
||||
start_point: Point::zero().to_ts_point(),
|
||||
end_point: text.max_point().to_ts_point(),
|
||||
}],
|
||||
range: text.min_anchor()..text.max_anchor(),
|
||||
range: Anchor::MIN..Anchor::MAX,
|
||||
mode: ParseMode::Single,
|
||||
});
|
||||
|
||||
@@ -474,7 +474,7 @@ impl SyntaxSnapshot {
|
||||
} else {
|
||||
SyntaxLayerPosition {
|
||||
depth: max_depth + 1,
|
||||
range: text.max_anchor()..text.max_anchor(),
|
||||
range: Anchor::MAX..Anchor::MAX,
|
||||
language: None,
|
||||
}
|
||||
};
|
||||
@@ -485,7 +485,7 @@ impl SyntaxSnapshot {
|
||||
|
||||
let bounded_position = SyntaxLayerPositionBeforeChange {
|
||||
position: position.clone(),
|
||||
change: changed_regions.start_position(text),
|
||||
change: changed_regions.start_position(),
|
||||
};
|
||||
if bounded_position.cmp(cursor.start(), text).is_gt() {
|
||||
let slice = cursor.slice(&bounded_position, Bias::Left, text);
|
||||
@@ -1608,11 +1608,11 @@ impl ChangedRegion {
|
||||
}
|
||||
|
||||
impl ChangeRegionSet {
|
||||
fn start_position(&self, text: &BufferSnapshot) -> ChangeStartPosition {
|
||||
fn start_position(&self) -> ChangeStartPosition {
|
||||
self.0.first().map_or(
|
||||
ChangeStartPosition {
|
||||
depth: usize::MAX,
|
||||
position: text.max_anchor(),
|
||||
position: Anchor::MAX,
|
||||
},
|
||||
|region| ChangeStartPosition {
|
||||
depth: region.depth,
|
||||
@@ -1661,26 +1661,32 @@ impl ChangeRegionSet {
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Summary for SyntaxLayerSummary {
|
||||
type Context = BufferSnapshot;
|
||||
|
||||
fn zero(buffer: &BufferSnapshot) -> Self {
|
||||
impl Default for SyntaxLayerSummary {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_depth: 0,
|
||||
min_depth: 0,
|
||||
range: buffer.max_anchor()..buffer.min_anchor(),
|
||||
last_layer_range: buffer.min_anchor()..buffer.max_anchor(),
|
||||
range: Anchor::MAX..Anchor::MIN,
|
||||
last_layer_range: Anchor::MIN..Anchor::MAX,
|
||||
last_layer_language: None,
|
||||
contains_unknown_injections: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl sum_tree::Summary for SyntaxLayerSummary {
|
||||
type Context = BufferSnapshot;
|
||||
|
||||
fn zero(_cx: &BufferSnapshot) -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn add_summary(&mut self, other: &Self, buffer: &Self::Context) {
|
||||
if other.max_depth > self.max_depth {
|
||||
self.max_depth = other.max_depth;
|
||||
self.range = other.range.clone();
|
||||
} else {
|
||||
if self.range == (buffer.max_anchor()..buffer.max_anchor()) {
|
||||
if self.range == (Anchor::MAX..Anchor::MAX) {
|
||||
self.range.start = other.range.start;
|
||||
}
|
||||
if other.range.end.cmp(&self.range.end, buffer).is_gt() {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -16,21 +16,19 @@ pub struct Anchor {
|
||||
|
||||
impl Anchor {
|
||||
pub fn min() -> Self {
|
||||
todo!()
|
||||
// Self {
|
||||
// buffer_id: None,
|
||||
// excerpt_id: ExcerptId::min(),
|
||||
// text_anchor: text::Anchor::MIN,
|
||||
// }
|
||||
Self {
|
||||
buffer_id: None,
|
||||
excerpt_id: ExcerptId::min(),
|
||||
text_anchor: text::Anchor::MIN,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max() -> Self {
|
||||
todo!()
|
||||
// Self {
|
||||
// buffer_id: None,
|
||||
// excerpt_id: ExcerptId::max(),
|
||||
// text_anchor: text::Anchor::MAX,
|
||||
// }
|
||||
Self {
|
||||
buffer_id: None,
|
||||
excerpt_id: ExcerptId::max(),
|
||||
text_anchor: text::Anchor::MAX,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cmp(&self, other: &Anchor, snapshot: &MultiBufferSnapshot) -> Ordering {
|
||||
@@ -49,36 +47,33 @@ impl Anchor {
|
||||
}
|
||||
|
||||
pub fn bias(&self) -> Bias {
|
||||
todo!()
|
||||
// self.text_anchor.bias
|
||||
self.text_anchor.bias
|
||||
}
|
||||
|
||||
pub fn bias_left(&self, _snapshot: &MultiBufferSnapshot) -> Anchor {
|
||||
todo!()
|
||||
// if self.text_anchor.bias != Bias::Left {
|
||||
// if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
|
||||
// return Self {
|
||||
// buffer_id: self.buffer_id,
|
||||
// excerpt_id: self.excerpt_id,
|
||||
// text_anchor: self.text_anchor.bias_left(&excerpt.buffer),
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
// *self
|
||||
pub fn bias_left(&self, snapshot: &MultiBufferSnapshot) -> Anchor {
|
||||
if self.text_anchor.bias != Bias::Left {
|
||||
if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
|
||||
return Self {
|
||||
buffer_id: self.buffer_id,
|
||||
excerpt_id: self.excerpt_id,
|
||||
text_anchor: self.text_anchor.bias_left(&excerpt.buffer),
|
||||
};
|
||||
}
|
||||
}
|
||||
*self
|
||||
}
|
||||
|
||||
pub fn bias_right(&self, _snapshot: &MultiBufferSnapshot) -> Anchor {
|
||||
todo!()
|
||||
// if self.text_anchor.bias != Bias::Right {
|
||||
// if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
|
||||
// return Self {
|
||||
// buffer_id: self.buffer_id,
|
||||
// excerpt_id: self.excerpt_id,
|
||||
// text_anchor: self.text_anchor.bias_right(&excerpt.buffer),
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
// *self
|
||||
pub fn bias_right(&self, snapshot: &MultiBufferSnapshot) -> Anchor {
|
||||
if self.text_anchor.bias != Bias::Right {
|
||||
if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
|
||||
return Self {
|
||||
buffer_id: self.buffer_id,
|
||||
excerpt_id: self.excerpt_id,
|
||||
text_anchor: self.text_anchor.bias_right(&excerpt.buffer),
|
||||
};
|
||||
}
|
||||
}
|
||||
*self
|
||||
}
|
||||
|
||||
pub fn summary<D>(&self, snapshot: &MultiBufferSnapshot) -> D
|
||||
|
||||
@@ -437,20 +437,19 @@ impl MultiBuffer {
|
||||
self.capability == Capability::ReadOnly
|
||||
}
|
||||
|
||||
pub fn singleton(_buffer: Model<Buffer>, _cx: &mut ModelContext<Self>) -> Self {
|
||||
todo!()
|
||||
// let mut this = Self::new(buffer.read(cx).capability());
|
||||
// this.singleton = true;
|
||||
// this.push_excerpts(
|
||||
// buffer,
|
||||
// [ExcerptRange {
|
||||
// context: text::Anchor::MIN..text::Anchor::MAX,
|
||||
// primary: None,
|
||||
// }],
|
||||
// cx,
|
||||
// );
|
||||
// this.snapshot.borrow_mut().singleton = true;
|
||||
// this
|
||||
pub fn singleton(buffer: Model<Buffer>, cx: &mut ModelContext<Self>) -> Self {
|
||||
let mut this = Self::new(buffer.read(cx).capability());
|
||||
this.singleton = true;
|
||||
this.push_excerpts(
|
||||
buffer,
|
||||
[ExcerptRange {
|
||||
context: text::Anchor::MIN..text::Anchor::MAX,
|
||||
primary: None,
|
||||
}],
|
||||
cx,
|
||||
);
|
||||
this.snapshot.borrow_mut().singleton = true;
|
||||
this
|
||||
}
|
||||
|
||||
/// Returns an up-to-date snapshot of the MultiBuffer.
|
||||
@@ -3057,107 +3056,106 @@ impl MultiBufferSnapshot {
|
||||
summaries
|
||||
}
|
||||
|
||||
pub fn refresh_anchors<'a, I>(&'a self, _anchors: I) -> Vec<(usize, Anchor, bool)>
|
||||
pub fn refresh_anchors<'a, I>(&'a self, anchors: I) -> Vec<(usize, Anchor, bool)>
|
||||
where
|
||||
I: 'a + IntoIterator<Item = &'a Anchor>,
|
||||
{
|
||||
todo!()
|
||||
// let mut anchors = anchors.into_iter().enumerate().peekable();
|
||||
// let mut cursor = self.excerpts.cursor::<Option<&Locator>>(&());
|
||||
// cursor.next(&());
|
||||
let mut anchors = anchors.into_iter().enumerate().peekable();
|
||||
let mut cursor = self.excerpts.cursor::<Option<&Locator>>(&());
|
||||
cursor.next(&());
|
||||
|
||||
// let mut result = Vec::new();
|
||||
let mut result = Vec::new();
|
||||
|
||||
// while let Some((_, anchor)) = anchors.peek() {
|
||||
// let old_excerpt_id = anchor.excerpt_id;
|
||||
while let Some((_, anchor)) = anchors.peek() {
|
||||
let old_excerpt_id = anchor.excerpt_id;
|
||||
|
||||
// // Find the location where this anchor's excerpt should be.
|
||||
// let old_locator = self.excerpt_locator_for_id(old_excerpt_id);
|
||||
// cursor.seek_forward(&Some(old_locator), Bias::Left, &());
|
||||
// Find the location where this anchor's excerpt should be.
|
||||
let old_locator = self.excerpt_locator_for_id(old_excerpt_id);
|
||||
cursor.seek_forward(&Some(old_locator), Bias::Left, &());
|
||||
|
||||
// if cursor.item().is_none() {
|
||||
// cursor.next(&());
|
||||
// }
|
||||
if cursor.item().is_none() {
|
||||
cursor.next(&());
|
||||
}
|
||||
|
||||
// let next_excerpt = cursor.item();
|
||||
// let prev_excerpt = cursor.prev_item();
|
||||
let next_excerpt = cursor.item();
|
||||
let prev_excerpt = cursor.prev_item();
|
||||
|
||||
// // Process all of the anchors for this excerpt.
|
||||
// while let Some((_, anchor)) = anchors.peek() {
|
||||
// if anchor.excerpt_id != old_excerpt_id {
|
||||
// break;
|
||||
// }
|
||||
// let (anchor_ix, anchor) = anchors.next().unwrap();
|
||||
// let mut anchor = *anchor;
|
||||
// Process all of the anchors for this excerpt.
|
||||
while let Some((_, anchor)) = anchors.peek() {
|
||||
if anchor.excerpt_id != old_excerpt_id {
|
||||
break;
|
||||
}
|
||||
let (anchor_ix, anchor) = anchors.next().unwrap();
|
||||
let mut anchor = *anchor;
|
||||
|
||||
// // Leave min and max anchors unchanged if invalid or
|
||||
// // if the old excerpt still exists at this location
|
||||
// let mut kept_position = next_excerpt
|
||||
// .map_or(false, |e| e.id == old_excerpt_id && e.contains(&anchor))
|
||||
// || old_excerpt_id == ExcerptId::max()
|
||||
// || old_excerpt_id == ExcerptId::min();
|
||||
// Leave min and max anchors unchanged if invalid or
|
||||
// if the old excerpt still exists at this location
|
||||
let mut kept_position = next_excerpt
|
||||
.map_or(false, |e| e.id == old_excerpt_id && e.contains(&anchor))
|
||||
|| old_excerpt_id == ExcerptId::max()
|
||||
|| old_excerpt_id == ExcerptId::min();
|
||||
|
||||
// // If the old excerpt no longer exists at this location, then attempt to
|
||||
// // find an equivalent position for this anchor in an adjacent excerpt.
|
||||
// if !kept_position {
|
||||
// for excerpt in [next_excerpt, prev_excerpt].iter().filter_map(|e| *e) {
|
||||
// if excerpt.contains(&anchor) {
|
||||
// anchor.excerpt_id = excerpt.id;
|
||||
// kept_position = true;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// If the old excerpt no longer exists at this location, then attempt to
|
||||
// find an equivalent position for this anchor in an adjacent excerpt.
|
||||
if !kept_position {
|
||||
for excerpt in [next_excerpt, prev_excerpt].iter().filter_map(|e| *e) {
|
||||
if excerpt.contains(&anchor) {
|
||||
anchor.excerpt_id = excerpt.id;
|
||||
kept_position = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// // If there's no adjacent excerpt that contains the anchor's position,
|
||||
// // then report that the anchor has lost its position.
|
||||
// if !kept_position {
|
||||
// anchor = if let Some(excerpt) = next_excerpt {
|
||||
// let mut text_anchor = excerpt
|
||||
// .range
|
||||
// .context
|
||||
// .start
|
||||
// .bias(anchor.text_anchor.bias, &excerpt.buffer);
|
||||
// if text_anchor
|
||||
// .cmp(&excerpt.range.context.end, &excerpt.buffer)
|
||||
// .is_gt()
|
||||
// {
|
||||
// text_anchor = excerpt.range.context.end;
|
||||
// }
|
||||
// Anchor {
|
||||
// buffer_id: Some(excerpt.buffer_id),
|
||||
// excerpt_id: excerpt.id,
|
||||
// text_anchor,
|
||||
// }
|
||||
// } else if let Some(excerpt) = prev_excerpt {
|
||||
// let mut text_anchor = excerpt
|
||||
// .range
|
||||
// .context
|
||||
// .end
|
||||
// .bias(anchor.text_anchor.bias, &excerpt.buffer);
|
||||
// if text_anchor
|
||||
// .cmp(&excerpt.range.context.start, &excerpt.buffer)
|
||||
// .is_lt()
|
||||
// {
|
||||
// text_anchor = excerpt.range.context.start;
|
||||
// }
|
||||
// Anchor {
|
||||
// buffer_id: Some(excerpt.buffer_id),
|
||||
// excerpt_id: excerpt.id,
|
||||
// text_anchor,
|
||||
// }
|
||||
// } else if anchor.text_anchor.bias == Bias::Left {
|
||||
// Anchor::min()
|
||||
// } else {
|
||||
// Anchor::max()
|
||||
// };
|
||||
// }
|
||||
// If there's no adjacent excerpt that contains the anchor's position,
|
||||
// then report that the anchor has lost its position.
|
||||
if !kept_position {
|
||||
anchor = if let Some(excerpt) = next_excerpt {
|
||||
let mut text_anchor = excerpt
|
||||
.range
|
||||
.context
|
||||
.start
|
||||
.bias(anchor.text_anchor.bias, &excerpt.buffer);
|
||||
if text_anchor
|
||||
.cmp(&excerpt.range.context.end, &excerpt.buffer)
|
||||
.is_gt()
|
||||
{
|
||||
text_anchor = excerpt.range.context.end;
|
||||
}
|
||||
Anchor {
|
||||
buffer_id: Some(excerpt.buffer_id),
|
||||
excerpt_id: excerpt.id,
|
||||
text_anchor,
|
||||
}
|
||||
} else if let Some(excerpt) = prev_excerpt {
|
||||
let mut text_anchor = excerpt
|
||||
.range
|
||||
.context
|
||||
.end
|
||||
.bias(anchor.text_anchor.bias, &excerpt.buffer);
|
||||
if text_anchor
|
||||
.cmp(&excerpt.range.context.start, &excerpt.buffer)
|
||||
.is_lt()
|
||||
{
|
||||
text_anchor = excerpt.range.context.start;
|
||||
}
|
||||
Anchor {
|
||||
buffer_id: Some(excerpt.buffer_id),
|
||||
excerpt_id: excerpt.id,
|
||||
text_anchor,
|
||||
}
|
||||
} else if anchor.text_anchor.bias == Bias::Left {
|
||||
Anchor::min()
|
||||
} else {
|
||||
Anchor::max()
|
||||
};
|
||||
}
|
||||
|
||||
// result.push((anchor_ix, anchor, kept_position));
|
||||
// }
|
||||
// }
|
||||
// result.sort_unstable_by(|a, b| a.1.cmp(&b.1, self));
|
||||
// result
|
||||
result.push((anchor_ix, anchor, kept_position));
|
||||
}
|
||||
}
|
||||
result.sort_unstable_by(|a, b| a.1.cmp(&b.1, self));
|
||||
result
|
||||
}
|
||||
|
||||
pub fn anchor_before<T: ToOffset>(&self, position: T) -> Anchor {
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
[package]
|
||||
name = "multi_buffer2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/multi_buffer2.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = [
|
||||
"text/test-support",
|
||||
"language/test-support",
|
||||
"gpui/test-support",
|
||||
"util/test-support",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
clock.workspace = true
|
||||
collections.workspace = true
|
||||
ctor.workspace = true
|
||||
env_logger.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
parking_lot.workspace = true
|
||||
rand.workspace = true
|
||||
settings.workspace = true
|
||||
serde.workspace = true
|
||||
smallvec.workspace = true
|
||||
sum_tree.workspace = true
|
||||
text.workspace = true
|
||||
theme.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
rand.workspace = true
|
||||
rpc = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
text = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
@@ -1 +0,0 @@
|
||||
../../LICENSE-GPL
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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())
|
||||
@@ -1451,9 +1460,8 @@ impl LspStore {
|
||||
|
||||
let actions = this
|
||||
.update(cx, move |this, cx| {
|
||||
let range = buffer.read(cx).min_anchor()..buffer.read(cx).max_anchor();
|
||||
let request = GetCodeActions {
|
||||
range,
|
||||
range: text::Anchor::MIN..text::Anchor::MAX,
|
||||
kinds: Some(code_actions),
|
||||
};
|
||||
let server = LanguageServerToQuery::Other(language_server.server_id());
|
||||
@@ -1770,7 +1778,9 @@ impl LspStore {
|
||||
});
|
||||
|
||||
buffer
|
||||
.update(&mut cx, |buffer, _| buffer.wait_for_anchors(Some(position)))?
|
||||
.update(&mut cx, |buffer, _| {
|
||||
buffer.wait_for_edits(Some(position.timestamp))
|
||||
})?
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let position = position.to_point_utf16(buffer.read(cx));
|
||||
@@ -2416,7 +2426,7 @@ impl LspStore {
|
||||
cx.spawn(move |_, mut cx| async move {
|
||||
buffer_handle
|
||||
.update(&mut cx, |buffer, _| {
|
||||
buffer.wait_for_anchors(vec![range_start, range_end])
|
||||
buffer.wait_for_edits(vec![range_start.timestamp, range_end.timestamp])
|
||||
})?
|
||||
.await
|
||||
.context("waiting for inlay hint request range edits")?;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -576,7 +576,7 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
|
||||
DiagnosticSet::from_sorted_entries(
|
||||
vec![DiagnosticEntry {
|
||||
diagnostic: Default::default(),
|
||||
range: buffer.min_anchor()..buffer.max_anchor(),
|
||||
range: Anchor::MIN..Anchor::MAX,
|
||||
}],
|
||||
&buffer.snapshot(),
|
||||
),
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -6,68 +6,43 @@ use std::{cmp::Ordering, fmt::Debug, ops::Range};
|
||||
use sum_tree::Bias;
|
||||
|
||||
/// A timestamped position in a buffer
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
|
||||
pub enum Anchor {
|
||||
Start {
|
||||
buffer_id: BufferId,
|
||||
},
|
||||
End {
|
||||
buffer_id: BufferId,
|
||||
},
|
||||
Character {
|
||||
buffer_id: BufferId,
|
||||
insertion_id: clock::Lamport,
|
||||
offset: usize,
|
||||
bias: Bias,
|
||||
},
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, Default)]
|
||||
pub struct Anchor {
|
||||
pub timestamp: clock::Lamport,
|
||||
/// The byte offset in the buffer
|
||||
pub offset: usize,
|
||||
/// Describes which character the anchor is biased towards
|
||||
pub bias: Bias,
|
||||
pub buffer_id: Option<BufferId>,
|
||||
}
|
||||
|
||||
impl Anchor {
|
||||
pub fn buffer_id(&self) -> BufferId {
|
||||
match self {
|
||||
Anchor::Start { buffer_id } => *buffer_id,
|
||||
Anchor::End { buffer_id } => *buffer_id,
|
||||
Anchor::Character { buffer_id, .. } => *buffer_id,
|
||||
}
|
||||
}
|
||||
pub const MIN: Self = Self {
|
||||
timestamp: clock::Lamport::MIN,
|
||||
offset: usize::MIN,
|
||||
bias: Bias::Left,
|
||||
buffer_id: None,
|
||||
};
|
||||
|
||||
pub const MAX: Self = Self {
|
||||
timestamp: clock::Lamport::MAX,
|
||||
offset: usize::MAX,
|
||||
bias: Bias::Right,
|
||||
buffer_id: None,
|
||||
};
|
||||
|
||||
pub fn cmp(&self, other: &Anchor, buffer: &BufferSnapshot) -> Ordering {
|
||||
debug_assert_eq!(
|
||||
self.buffer_id(),
|
||||
other.buffer_id(),
|
||||
"anchors belong to different buffers"
|
||||
);
|
||||
let fragment_id_comparison = if self.timestamp == other.timestamp {
|
||||
Ordering::Equal
|
||||
} else {
|
||||
buffer
|
||||
.fragment_id_for_anchor(self)
|
||||
.cmp(buffer.fragment_id_for_anchor(other))
|
||||
};
|
||||
|
||||
match (self, other) {
|
||||
(Anchor::Start { .. }, Anchor::Start { .. }) => Ordering::Equal,
|
||||
(Anchor::End { .. }, Anchor::End { .. }) => Ordering::Equal,
|
||||
(Anchor::Start { .. }, _) | (_, Anchor::End { .. }) => Ordering::Less,
|
||||
(_, Anchor::Start { .. }) | (Anchor::End { .. }, _) => Ordering::Greater,
|
||||
(
|
||||
Anchor::Character {
|
||||
insertion_id,
|
||||
offset,
|
||||
bias,
|
||||
..
|
||||
},
|
||||
Anchor::Character {
|
||||
insertion_id: other_insertion_id,
|
||||
offset: other_offset,
|
||||
bias: other_bias,
|
||||
..
|
||||
},
|
||||
) => {
|
||||
if insertion_id == other_insertion_id {
|
||||
offset
|
||||
.cmp(&other_offset)
|
||||
.then_with(|| bias.cmp(&other_bias))
|
||||
} else {
|
||||
buffer
|
||||
.fragment_id_for_anchor(self)
|
||||
.cmp(buffer.fragment_id_for_anchor(other))
|
||||
}
|
||||
}
|
||||
}
|
||||
fragment_id_comparison
|
||||
.then_with(|| self.offset.cmp(&other.offset))
|
||||
.then_with(|| self.bias.cmp(&other.bias))
|
||||
}
|
||||
|
||||
pub fn min(&self, other: &Self, buffer: &BufferSnapshot) -> Self {
|
||||
@@ -95,42 +70,18 @@ impl Anchor {
|
||||
}
|
||||
|
||||
pub fn bias_left(&self, buffer: &BufferSnapshot) -> Anchor {
|
||||
match self {
|
||||
Anchor::Start { buffer_id } => Anchor::Start {
|
||||
buffer_id: *buffer_id,
|
||||
},
|
||||
Anchor::End { .. } => buffer.anchor_before(buffer.len()),
|
||||
Anchor::Character {
|
||||
buffer_id,
|
||||
insertion_id,
|
||||
offset,
|
||||
..
|
||||
} => Anchor::Character {
|
||||
buffer_id: *buffer_id,
|
||||
insertion_id: *insertion_id,
|
||||
offset: *offset,
|
||||
bias: Bias::Left,
|
||||
},
|
||||
if self.bias == Bias::Left {
|
||||
*self
|
||||
} else {
|
||||
buffer.anchor_before(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bias_right(&self, buffer: &BufferSnapshot) -> Anchor {
|
||||
match self {
|
||||
Anchor::Start { .. } => buffer.anchor_after(0),
|
||||
Anchor::End { buffer_id } => Anchor::End {
|
||||
buffer_id: *buffer_id,
|
||||
},
|
||||
Anchor::Character {
|
||||
buffer_id,
|
||||
insertion_id,
|
||||
offset,
|
||||
..
|
||||
} => Anchor::Character {
|
||||
buffer_id: *buffer_id,
|
||||
insertion_id: *insertion_id,
|
||||
offset: *offset,
|
||||
bias: Bias::Right,
|
||||
},
|
||||
if self.bias == Bias::Right {
|
||||
*self
|
||||
} else {
|
||||
buffer.anchor_after(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,23 +94,17 @@ impl Anchor {
|
||||
|
||||
/// Returns true when the [`Anchor`] is located inside a visible fragment.
|
||||
pub fn is_valid(&self, buffer: &BufferSnapshot) -> bool {
|
||||
match self {
|
||||
Anchor::Start { buffer_id } | Anchor::End { buffer_id } => {
|
||||
*buffer_id == buffer.remote_id
|
||||
}
|
||||
Anchor::Character { buffer_id, .. } => {
|
||||
if *buffer_id == buffer.remote_id {
|
||||
let fragment_id = buffer.fragment_id_for_anchor(self);
|
||||
let mut fragment_cursor =
|
||||
buffer.fragments.cursor::<(Option<&Locator>, usize)>(&None);
|
||||
fragment_cursor.seek(&Some(fragment_id), Bias::Left, &None);
|
||||
fragment_cursor
|
||||
.item()
|
||||
.map_or(false, |fragment| fragment.visible)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
if *self == Anchor::MIN || *self == Anchor::MAX {
|
||||
true
|
||||
} else if self.buffer_id != Some(buffer.remote_id) {
|
||||
false
|
||||
} else {
|
||||
let fragment_id = buffer.fragment_id_for_anchor(self);
|
||||
let mut fragment_cursor = buffer.fragments.cursor::<(Option<&Locator>, usize)>(&None);
|
||||
fragment_cursor.seek(&Some(fragment_id), Bias::Left, &None);
|
||||
fragment_cursor
|
||||
.item()
|
||||
.map_or(false, |fragment| fragment.visible)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1542,15 +1542,16 @@ impl Buffer {
|
||||
) -> impl 'static + Future<Output = Result<()>> {
|
||||
let mut futures = Vec::new();
|
||||
for anchor in anchors {
|
||||
if let Anchor::Character { insertion_id, .. } = anchor {
|
||||
if !self.version.observed(insertion_id) {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.edit_id_resolvers
|
||||
.entry(insertion_id)
|
||||
.or_default()
|
||||
.push(tx);
|
||||
futures.push(rx);
|
||||
}
|
||||
if !self.version.observed(anchor.timestamp)
|
||||
&& anchor != Anchor::MAX
|
||||
&& anchor != Anchor::MIN
|
||||
{
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.edit_id_resolvers
|
||||
.entry(anchor.timestamp)
|
||||
.or_default()
|
||||
.push(tx);
|
||||
futures.push(rx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1927,18 +1928,6 @@ impl BufferSnapshot {
|
||||
self.visible_text.summary()
|
||||
}
|
||||
|
||||
pub fn min_anchor(&self) -> Anchor {
|
||||
Anchor::Start {
|
||||
buffer_id: self.remote_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max_anchor(&self) -> Anchor {
|
||||
Anchor::End {
|
||||
buffer_id: self.remote_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max_point(&self) -> Point {
|
||||
self.visible_text.max_point()
|
||||
}
|
||||
@@ -2140,50 +2129,41 @@ impl BufferSnapshot {
|
||||
let mut position = D::zero(&());
|
||||
|
||||
anchors.map(move |(anchor, payload)| {
|
||||
debug_assert_eq!(
|
||||
anchor.buffer_id(),
|
||||
self.remote_id,
|
||||
"anchor belongs to a different buffer"
|
||||
);
|
||||
|
||||
match *anchor {
|
||||
Anchor::Start { .. } => (D::zero(&()), payload),
|
||||
Anchor::End { .. } => (D::from_text_summary(&self.visible_text.summary()), payload),
|
||||
Anchor::Character {
|
||||
insertion_id,
|
||||
offset,
|
||||
bias,
|
||||
..
|
||||
} => {
|
||||
let anchor_key = InsertionFragmentKey {
|
||||
timestamp: insertion_id,
|
||||
split_offset: offset,
|
||||
};
|
||||
insertion_cursor.seek(&anchor_key, bias, &());
|
||||
if let Some(insertion) = insertion_cursor.item() {
|
||||
let comparison = sum_tree::KeyedItem::key(insertion).cmp(&anchor_key);
|
||||
if comparison == Ordering::Greater
|
||||
|| (bias == Bias::Left && comparison == Ordering::Equal && offset > 0)
|
||||
{
|
||||
insertion_cursor.prev(&());
|
||||
}
|
||||
} else {
|
||||
insertion_cursor.prev(&());
|
||||
}
|
||||
let insertion = insertion_cursor.item().expect("invalid insertion");
|
||||
assert_eq!(insertion.timestamp, insertion_id, "invalid insertion");
|
||||
|
||||
fragment_cursor.seek_forward(&Some(&insertion.fragment_id), Bias::Left, &None);
|
||||
let fragment = fragment_cursor.item().unwrap();
|
||||
let mut fragment_offset = fragment_cursor.start().1;
|
||||
if fragment.visible {
|
||||
fragment_offset += offset - insertion.split_offset;
|
||||
}
|
||||
|
||||
position.add_assign(&text_cursor.summary(fragment_offset));
|
||||
(position.clone(), payload)
|
||||
}
|
||||
if *anchor == Anchor::MIN {
|
||||
return (D::zero(&()), payload);
|
||||
} else if *anchor == Anchor::MAX {
|
||||
return (D::from_text_summary(&self.visible_text.summary()), payload);
|
||||
}
|
||||
|
||||
let anchor_key = InsertionFragmentKey {
|
||||
timestamp: anchor.timestamp,
|
||||
split_offset: anchor.offset,
|
||||
};
|
||||
insertion_cursor.seek(&anchor_key, anchor.bias, &());
|
||||
if let Some(insertion) = insertion_cursor.item() {
|
||||
let comparison = sum_tree::KeyedItem::key(insertion).cmp(&anchor_key);
|
||||
if comparison == Ordering::Greater
|
||||
|| (anchor.bias == Bias::Left
|
||||
&& comparison == Ordering::Equal
|
||||
&& anchor.offset > 0)
|
||||
{
|
||||
insertion_cursor.prev(&());
|
||||
}
|
||||
} else {
|
||||
insertion_cursor.prev(&());
|
||||
}
|
||||
let insertion = insertion_cursor.item().expect("invalid insertion");
|
||||
assert_eq!(insertion.timestamp, anchor.timestamp, "invalid insertion");
|
||||
|
||||
fragment_cursor.seek_forward(&Some(&insertion.fragment_id), Bias::Left, &None);
|
||||
let fragment = fragment_cursor.item().unwrap();
|
||||
let mut fragment_offset = fragment_cursor.start().1;
|
||||
if fragment.visible {
|
||||
fragment_offset += anchor.offset - insertion.split_offset;
|
||||
}
|
||||
|
||||
position.add_assign(&text_cursor.summary(fragment_offset));
|
||||
(position.clone(), payload)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2191,57 +2171,90 @@ impl BufferSnapshot {
|
||||
where
|
||||
D: TextDimension,
|
||||
{
|
||||
self.summaries_for_anchors([anchor]).next().unwrap()
|
||||
if *anchor == Anchor::MIN {
|
||||
D::zero(&())
|
||||
} else if *anchor == Anchor::MAX {
|
||||
D::from_text_summary(&self.visible_text.summary())
|
||||
} else {
|
||||
let anchor_key = InsertionFragmentKey {
|
||||
timestamp: anchor.timestamp,
|
||||
split_offset: anchor.offset,
|
||||
};
|
||||
let mut insertion_cursor = self.insertions.cursor::<InsertionFragmentKey>(&());
|
||||
insertion_cursor.seek(&anchor_key, anchor.bias, &());
|
||||
if let Some(insertion) = insertion_cursor.item() {
|
||||
let comparison = sum_tree::KeyedItem::key(insertion).cmp(&anchor_key);
|
||||
if comparison == Ordering::Greater
|
||||
|| (anchor.bias == Bias::Left
|
||||
&& comparison == Ordering::Equal
|
||||
&& anchor.offset > 0)
|
||||
{
|
||||
insertion_cursor.prev(&());
|
||||
}
|
||||
} else {
|
||||
insertion_cursor.prev(&());
|
||||
}
|
||||
|
||||
let Some(insertion) = insertion_cursor
|
||||
.item()
|
||||
.filter(|insertion| insertion.timestamp == anchor.timestamp)
|
||||
else {
|
||||
panic!(
|
||||
"invalid anchor {:?}. buffer id: {}, version: {:?}",
|
||||
anchor, self.remote_id, self.version
|
||||
);
|
||||
};
|
||||
|
||||
let mut fragment_cursor = self.fragments.cursor::<(Option<&Locator>, usize)>(&None);
|
||||
fragment_cursor.seek(&Some(&insertion.fragment_id), Bias::Left, &None);
|
||||
let fragment = fragment_cursor.item().unwrap();
|
||||
let mut fragment_offset = fragment_cursor.start().1;
|
||||
if fragment.visible {
|
||||
fragment_offset += anchor.offset - insertion.split_offset;
|
||||
}
|
||||
self.text_summary_for_range(0..fragment_offset)
|
||||
}
|
||||
}
|
||||
|
||||
fn fragment_id_for_anchor(&self, anchor: &Anchor) -> &Locator {
|
||||
debug_assert_eq!(
|
||||
anchor.buffer_id(),
|
||||
self.remote_id,
|
||||
"anchor belongs to a different buffer"
|
||||
);
|
||||
|
||||
match *anchor {
|
||||
Anchor::Start { .. } => Locator::min_ref(),
|
||||
Anchor::End { .. } => Locator::max_ref(),
|
||||
Anchor::Character {
|
||||
insertion_id,
|
||||
offset,
|
||||
bias,
|
||||
..
|
||||
} => {
|
||||
let anchor_key = InsertionFragmentKey {
|
||||
timestamp: insertion_id,
|
||||
split_offset: offset,
|
||||
};
|
||||
let mut insertion_cursor = self.insertions.cursor::<InsertionFragmentKey>(&());
|
||||
insertion_cursor.seek(&anchor_key, bias, &());
|
||||
if let Some(insertion) = insertion_cursor.item() {
|
||||
let comparison = sum_tree::KeyedItem::key(insertion).cmp(&anchor_key);
|
||||
if comparison == Ordering::Greater
|
||||
|| (bias == Bias::Left && comparison == Ordering::Equal && offset > 0)
|
||||
{
|
||||
insertion_cursor.prev(&());
|
||||
}
|
||||
} else {
|
||||
if *anchor == Anchor::MIN {
|
||||
Locator::min_ref()
|
||||
} else if *anchor == Anchor::MAX {
|
||||
Locator::max_ref()
|
||||
} else {
|
||||
let anchor_key = InsertionFragmentKey {
|
||||
timestamp: anchor.timestamp,
|
||||
split_offset: anchor.offset,
|
||||
};
|
||||
let mut insertion_cursor = self.insertions.cursor::<InsertionFragmentKey>(&());
|
||||
insertion_cursor.seek(&anchor_key, anchor.bias, &());
|
||||
if let Some(insertion) = insertion_cursor.item() {
|
||||
let comparison = sum_tree::KeyedItem::key(insertion).cmp(&anchor_key);
|
||||
if comparison == Ordering::Greater
|
||||
|| (anchor.bias == Bias::Left
|
||||
&& comparison == Ordering::Equal
|
||||
&& anchor.offset > 0)
|
||||
{
|
||||
insertion_cursor.prev(&());
|
||||
}
|
||||
|
||||
let Some(insertion) = insertion_cursor.item().filter(|insertion| {
|
||||
if cfg!(debug_assertions) {
|
||||
insertion.timestamp == insertion_id
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}) else {
|
||||
panic!(
|
||||
"invalid anchor {:?}. buffer id: {}, version: {:?}",
|
||||
anchor, self.remote_id, self.version
|
||||
);
|
||||
};
|
||||
|
||||
&insertion.fragment_id
|
||||
} else {
|
||||
insertion_cursor.prev(&());
|
||||
}
|
||||
|
||||
let Some(insertion) = insertion_cursor.item().filter(|insertion| {
|
||||
if cfg!(debug_assertions) {
|
||||
insertion.timestamp == anchor.timestamp
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}) else {
|
||||
panic!(
|
||||
"invalid anchor {:?}. buffer id: {}, version: {:?}",
|
||||
anchor, self.remote_id, self.version
|
||||
);
|
||||
};
|
||||
|
||||
&insertion.fragment_id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2259,33 +2272,27 @@ impl BufferSnapshot {
|
||||
|
||||
fn anchor_at_offset(&self, offset: usize, bias: Bias) -> Anchor {
|
||||
if bias == Bias::Left && offset == 0 {
|
||||
self.min_anchor()
|
||||
Anchor::MIN
|
||||
} else if bias == Bias::Right && offset == self.len() {
|
||||
self.max_anchor()
|
||||
Anchor::MAX
|
||||
} else {
|
||||
let mut fragment_cursor = self.fragments.cursor::<usize>(&None);
|
||||
fragment_cursor.seek(&offset, bias, &None);
|
||||
let fragment = fragment_cursor.item().unwrap();
|
||||
let overshoot = offset - *fragment_cursor.start();
|
||||
Anchor::Character {
|
||||
insertion_id: fragment.timestamp,
|
||||
Anchor {
|
||||
timestamp: fragment.timestamp,
|
||||
offset: fragment.insertion_offset + overshoot,
|
||||
bias,
|
||||
buffer_id: self.remote_id,
|
||||
buffer_id: Some(self.remote_id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn can_resolve(&self, anchor: &Anchor) -> bool {
|
||||
match *anchor {
|
||||
Anchor::Start { buffer_id } => self.remote_id == buffer_id,
|
||||
Anchor::End { buffer_id } => self.remote_id == buffer_id,
|
||||
Anchor::Character {
|
||||
buffer_id,
|
||||
insertion_id,
|
||||
..
|
||||
} => self.remote_id == buffer_id && self.version.observed(insertion_id),
|
||||
}
|
||||
*anchor == Anchor::MIN
|
||||
|| *anchor == Anchor::MAX
|
||||
|| (Some(self.remote_id) == anchor.buffer_id && self.version.observed(anchor.timestamp))
|
||||
}
|
||||
|
||||
pub fn clip_offset(&self, offset: usize, bias: Bias) -> usize {
|
||||
@@ -2311,7 +2318,7 @@ impl BufferSnapshot {
|
||||
where
|
||||
D: TextDimension + Ord,
|
||||
{
|
||||
self.edits_since_in_range(since, self.min_anchor()..self.max_anchor())
|
||||
self.edits_since_in_range(since, Anchor::MIN..Anchor::MAX)
|
||||
}
|
||||
|
||||
pub fn anchored_edits_since<'a, D>(
|
||||
@@ -2321,7 +2328,7 @@ impl BufferSnapshot {
|
||||
where
|
||||
D: TextDimension + Ord,
|
||||
{
|
||||
self.anchored_edits_since_in_range(since, self.min_anchor()..self.max_anchor())
|
||||
self.anchored_edits_since_in_range(since, Anchor::MIN..Anchor::MAX)
|
||||
}
|
||||
|
||||
pub fn edits_since_in_range<'a, D>(
|
||||
@@ -2359,20 +2366,10 @@ impl BufferSnapshot {
|
||||
|
||||
let start_fragment_id = self.fragment_id_for_anchor(&range.start);
|
||||
cursor.seek(&Some(start_fragment_id), Bias::Left, &None);
|
||||
let start_fragment_offset = if let Anchor::Character { offset, .. } = range.start {
|
||||
offset
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let mut visible_start = cursor.start().1.visible;
|
||||
let mut deleted_start = cursor.start().1.deleted;
|
||||
if let Some(fragment) = cursor.item() {
|
||||
let overshoot = if let Anchor::Character { offset, .. } = range.start {
|
||||
offset - fragment.insertion_offset
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let overshoot = range.start.offset - fragment.insertion_offset;
|
||||
if fragment.visible {
|
||||
visible_start += overshoot;
|
||||
} else {
|
||||
@@ -2380,11 +2377,6 @@ impl BufferSnapshot {
|
||||
}
|
||||
}
|
||||
let end_fragment_id = self.fragment_id_for_anchor(&range.end);
|
||||
let end_fragment_offset = if let Anchor::Character { offset, .. } = range.end {
|
||||
offset
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
Edits {
|
||||
visible_cursor: self.visible_text.cursor(visible_start),
|
||||
@@ -2394,8 +2386,7 @@ impl BufferSnapshot {
|
||||
since,
|
||||
old_end: D::zero(&()),
|
||||
new_end: D::zero(&()),
|
||||
range: (start_fragment_id, start_fragment_offset)
|
||||
..(end_fragment_id, end_fragment_offset),
|
||||
range: (start_fragment_id, range.start.offset)..(end_fragment_id, range.end.offset),
|
||||
buffer_id: self.remote_id,
|
||||
}
|
||||
}
|
||||
@@ -2525,17 +2516,17 @@ impl<'a, D: TextDimension + Ord, F: FnMut(&FragmentSummary) -> bool> Iterator fo
|
||||
break;
|
||||
}
|
||||
|
||||
let start_anchor = Anchor::Character {
|
||||
insertion_id: fragment.timestamp,
|
||||
let start_anchor = Anchor {
|
||||
timestamp: fragment.timestamp,
|
||||
offset: fragment.insertion_offset,
|
||||
bias: Bias::Right,
|
||||
buffer_id: self.buffer_id,
|
||||
buffer_id: Some(self.buffer_id),
|
||||
};
|
||||
let end_anchor = Anchor::Character {
|
||||
insertion_id: fragment.timestamp,
|
||||
let end_anchor = Anchor {
|
||||
timestamp: fragment.timestamp,
|
||||
offset: fragment.insertion_offset + fragment.len,
|
||||
bias: Bias::Left,
|
||||
buffer_id: self.buffer_id,
|
||||
buffer_id: Some(self.buffer_id),
|
||||
};
|
||||
|
||||
if !fragment.was_visible(self.since, self.undos) && fragment.visible {
|
||||
|
||||
@@ -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