Merge remote-tracking branch 'origin' into tab-map-bitmask

This commit is contained in:
Anthony 2025-06-30 13:54:15 -04:00
commit 8c29d98afb
225 changed files with 9976 additions and 4245 deletions

View file

@ -460,8 +460,10 @@ jobs:
RET_CODE=0
# Always check style
[[ "${{ needs.style.result }}" != 'success' ]] && { RET_CODE=1; echo "style tests failed"; }
[[ "${{ needs.check_docs.result }}" != 'success' ]] && { RET_CODE=1; echo "docs checks failed"; }
if [[ "${{ needs.job_spec.outputs.run_docs }}" == "true" ]]; then
[[ "${{ needs.check_docs.result }}" != 'success' ]] && { RET_CODE=1; echo "docs checks failed"; }
fi
# Only check test jobs if they were supposed to run
if [[ "${{ needs.job_spec.outputs.run_tests }}" == "true" ]]; then
[[ "${{ needs.workspace_hack.result }}" != 'success' ]] && { RET_CODE=1; echo "Workspace Hack failed"; }

View file

@ -7,7 +7,7 @@ on:
pull_request:
branches:
- "**"
types: [opened, synchronize, reopened, labeled]
types: [synchronize, reopened, labeled]
workflow_dispatch:
@ -25,16 +25,6 @@ env:
ZED_EVAL_TELEMETRY: 1
jobs:
# This is a no-op job that we run to prevent GitHub from marking the workflow
# as failed for PRs that don't have the `run-eval` label.
noop:
name: No-op
runs-on: ubuntu-latest
if: github.repository_owner == 'zed-industries'
steps:
- name: No-op
run: echo "Nothing to do"
run_eval:
timeout-minutes: 60
name: Run Agent Eval

37
Cargo.lock generated
View file

@ -78,6 +78,7 @@ dependencies = [
"language",
"language_model",
"log",
"parking_lot",
"paths",
"postage",
"pretty_assertions",
@ -4155,6 +4156,7 @@ dependencies = [
"paths",
"serde",
"serde_json",
"shlex",
"task",
"util",
"workspace-hack",
@ -4308,6 +4310,7 @@ version = "0.1.0"
dependencies = [
"alacritty_terminal",
"anyhow",
"bitflags 2.9.0",
"client",
"collections",
"command_palette_hooks",
@ -9015,6 +9018,7 @@ dependencies = [
"collections",
"copilot",
"editor",
"feature_flags",
"futures 0.3.31",
"gpui",
"itertools 0.14.0",
@ -9238,7 +9242,7 @@ dependencies = [
[[package]]
name = "libwebrtc"
version = "0.3.10"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=80bb8f4c9112789f7c24cc98d8423010977806a6#80bb8f4c9112789f7c24cc98d8423010977806a6"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4"
dependencies = [
"cxx",
"jni",
@ -9318,7 +9322,7 @@ checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856"
[[package]]
name = "livekit"
version = "0.7.8"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=80bb8f4c9112789f7c24cc98d8423010977806a6#80bb8f4c9112789f7c24cc98d8423010977806a6"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4"
dependencies = [
"chrono",
"futures-util",
@ -9341,7 +9345,7 @@ dependencies = [
[[package]]
name = "livekit-api"
version = "0.4.2"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=80bb8f4c9112789f7c24cc98d8423010977806a6#80bb8f4c9112789f7c24cc98d8423010977806a6"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4"
dependencies = [
"futures-util",
"http 0.2.12",
@ -9365,7 +9369,7 @@ dependencies = [
[[package]]
name = "livekit-protocol"
version = "0.3.9"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=80bb8f4c9112789f7c24cc98d8423010977806a6#80bb8f4c9112789f7c24cc98d8423010977806a6"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4"
dependencies = [
"futures-util",
"livekit-runtime",
@ -9382,7 +9386,7 @@ dependencies = [
[[package]]
name = "livekit-runtime"
version = "0.4.0"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=80bb8f4c9112789f7c24cc98d8423010977806a6#80bb8f4c9112789f7c24cc98d8423010977806a6"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4"
dependencies = [
"tokio",
"tokio-stream",
@ -14552,12 +14556,12 @@ dependencies = [
"serde_json",
"serde_json_lenient",
"smallvec",
"streaming-iterator",
"tree-sitter",
"tree-sitter-json",
"unindent",
"util",
"workspace-hack",
"zlog",
]
[[package]]
@ -15524,6 +15528,18 @@ version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb"
[[package]]
name = "svg_preview"
version = "0.1.0"
dependencies = [
"editor",
"file_icons",
"gpui",
"ui",
"workspace",
"workspace-hack",
]
[[package]]
name = "svgtypes"
version = "0.15.3"
@ -16035,7 +16051,6 @@ dependencies = [
"indexmap",
"log",
"palette",
"rust-embed",
"serde",
"serde_json",
"serde_json_lenient",
@ -16866,8 +16881,7 @@ dependencies = [
[[package]]
name = "tree-sitter-python"
version = "0.23.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d065aaa27f3aaceaf60c1f0e0ac09e1cb9eb8ed28e7bcdaa52129cffc7f4b04"
source = "git+https://github.com/zed-industries/tree-sitter-python?rev=218fcbf3fda3d029225f3dec005cb497d111b35e#218fcbf3fda3d029225f3dec005cb497d111b35e"
dependencies = [
"cc",
"tree-sitter-language",
@ -18283,7 +18297,7 @@ dependencies = [
[[package]]
name = "webrtc-sys"
version = "0.3.7"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=80bb8f4c9112789f7c24cc98d8423010977806a6#80bb8f4c9112789f7c24cc98d8423010977806a6"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4"
dependencies = [
"cc",
"cxx",
@ -18296,7 +18310,7 @@ dependencies = [
[[package]]
name = "webrtc-sys-build"
version = "0.3.6"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=80bb8f4c9112789f7c24cc98d8423010977806a6#80bb8f4c9112789f7c24cc98d8423010977806a6"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4"
dependencies = [
"fs2",
"regex",
@ -20021,6 +20035,7 @@ dependencies = [
"snippet_provider",
"snippets_ui",
"supermaven",
"svg_preview",
"sysinfo",
"tab_switcher",
"task",

View file

@ -95,6 +95,7 @@ members = [
"crates/markdown_preview",
"crates/media",
"crates/menu",
"crates/svg_preview",
"crates/migrator",
"crates/mistral",
"crates/multi_buffer",
@ -304,6 +305,7 @@ lmstudio = { path = "crates/lmstudio" }
lsp = { path = "crates/lsp" }
markdown = { path = "crates/markdown" }
markdown_preview = { path = "crates/markdown_preview" }
svg_preview = { path = "crates/svg_preview" }
media = { path = "crates/media" }
menu = { path = "crates/menu" }
migrator = { path = "crates/migrator" }
@ -595,7 +597,7 @@ tree-sitter-html = "0.23"
tree-sitter-jsdoc = "0.23"
tree-sitter-json = "0.24"
tree-sitter-md = { git = "https://github.com/tree-sitter-grammars/tree-sitter-markdown", rev = "9a23c1a96c0513d8fc6520972beedd419a973539" }
tree-sitter-python = "0.23"
tree-sitter-python = { git = "https://github.com/zed-industries/tree-sitter-python", rev = "218fcbf3fda3d029225f3dec005cb497d111b35e" }
tree-sitter-regex = "0.24"
tree-sitter-ruby = "0.23"
tree-sitter-rust = "0.24"

View file

@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.2
FROM rust:1.87-bookworm as builder
FROM rust:1.88-bookworm as builder
WORKDIR app
COPY . .

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-down10-icon lucide-arrow-down-1-0"><path d="m3 16 4 4 4-4"/><path d="M7 20V4"/><path d="M17 10V4h-2"/><path d="M15 10h4"/><rect x="15" y="14" width="4" height="6" ry="2"/></svg>

After

Width:  |  Height:  |  Size: 386 B

View file

@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.75776 5.50003H8.49988C8.70769 5.50003 8.89518 5.62971 8.95455 5.82346C9.04049 6.01876 8.9858 6.23906 8.82956 6.37656L4.82971 9.87643C4.65315 10.0295 4.39488 10.042 4.20614 9.90455C4.01724 9.76705 3.94849 9.51706 4.04052 9.30301L5.24219 6.49999H3.48601C3.2918 6.49999 3.10524 6.37031 3.03197 6.17657C2.9587 5.98126 3.014 5.76096 3.1708 5.62346L7.17018 2.12375C7.34674 1.97001 7.60454 1.95829 7.7936 2.09547C7.98265 2.23275 8.0514 2.48218 7.95922 2.69695L6.75776 5.50003Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 601 B

View file

@ -0,0 +1,12 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 3L7 4" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 4L10 3" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.002 6V5.51658C5.98992 5.32067 6.03266 5.12502 6.12762 4.94143C6.22259 4.75784 6.36781 4.59012 6.55453 4.44839C6.74125 4.30666 6.9656 4.19386 7.21403 4.1168C7.46246 4.03973 7.72983 4 8 4C8.27017 4 8.53754 4.03973 8.78597 4.1168C9.0344 4.19386 9.25875 4.30666 9.44547 4.44839C9.63219 4.59012 9.77741 4.75784 9.87238 4.94143C9.96734 5.12502 10.0101 5.32067 9.998 5.51658V6" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 13C6.35 13 5 11.5462 5 9.76923V8.15385C5 7.58261 5.21071 7.03477 5.58579 6.63085C5.96086 6.22692 6.46957 6 7 6H9C9.53043 6 10.0391 6.22692 10.4142 6.63085C10.7893 7.03477 11 7.58261 11 8.15385V9.76923C11 11.5462 9.65 13 8 13Z" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 6.16663C3.90652 6.06663 3 5.21663 3 4.16663" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 9H3" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 13C3 11.95 3.89474 11.05 5 11" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13 4C13 5.05 12.0857 5.9 11 6" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13 9H11" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11 11C12.1053 11.05 13 11.95 13 13" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.84265 10.7778C4.39206 11.6001 5.17295 12.241 6.08658 12.6194C7.00021 12.9978 8.00555 13.0969 8.97545 12.9039C9.94535 12.711 10.8363 12.2348 11.5355 11.5355C12.2348 10.8363 12.711 9.94535 12.9039 8.97545C13.0969 8.00555 12.9978 7.00021 12.6194 6.08658C12.241 5.17295 11.6001 4.39206 10.7778 3.84265C9.9556 3.29324 8.9889 3 8 3C6.60219 3.00526 5.26054 3.55068 4.25556 4.52222L3 5.77778" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 3V6H6" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 685 B

View file

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 13C10.7614 13 13 10.7614 13 8C13 5.23858 10.7614 3 8 3C5.23858 3 3 5.23858 3 8C3 10.7614 5.23858 13 8 13Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5 5L11 11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 409 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-scroll-text-icon lucide-scroll-text"><path d="M15 12h-5"/><path d="M15 8h-5"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/><path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"/></svg>

After

Width:  |  Height:  |  Size: 441 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-split-icon lucide-split"><path d="M16 3h5v5"/><path d="M8 3H3v5"/><path d="M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3"/><path d="m15 9 6-6"/></svg>

After

Width:  |  Height:  |  Size: 345 B

View file

@ -244,6 +244,7 @@
"ctrl-alt-e": "agent::RemoveAllContext",
"ctrl-shift-e": "project_panel::ToggleFocus",
"ctrl-shift-enter": "agent::ContinueThread",
"super-ctrl-b": "agent::ToggleBurnMode",
"alt-enter": "agent::ContinueWithBurnMode"
}
},
@ -490,13 +491,27 @@
"ctrl-k r": "editor::RevealInFileManager",
"ctrl-k p": "editor::CopyPath",
"ctrl-\\": "pane::SplitRight",
"ctrl-k v": "markdown::OpenPreviewToTheSide",
"ctrl-shift-v": "markdown::OpenPreview",
"ctrl-alt-shift-c": "editor::DisplayCursorNames",
"alt-.": "editor::GoToHunk",
"alt-,": "editor::GoToPreviousHunk"
}
},
{
"context": "Editor && extension == md",
"use_key_equivalents": true,
"bindings": {
"ctrl-k v": "markdown::OpenPreviewToTheSide",
"ctrl-shift-v": "markdown::OpenPreview"
}
},
{
"context": "Editor && extension == svg",
"use_key_equivalents": true,
"bindings": {
"ctrl-k v": "svg::OpenPreviewToTheSide",
"ctrl-shift-v": "svg::OpenPreview"
}
},
{
"context": "Editor && mode == full",
"bindings": {
@ -904,7 +919,9 @@
"context": "BreakpointList",
"bindings": {
"space": "debugger::ToggleEnableBreakpoint",
"backspace": "debugger::UnsetBreakpoint"
"backspace": "debugger::UnsetBreakpoint",
"left": "debugger::PreviousBreakpointProperty",
"right": "debugger::NextBreakpointProperty"
}
},
{

View file

@ -283,6 +283,7 @@
"cmd->": "assistant::QuoteSelection",
"cmd-alt-e": "agent::RemoveAllContext",
"cmd-shift-e": "project_panel::ToggleFocus",
"cmd-ctrl-b": "agent::ToggleBurnMode",
"cmd-shift-enter": "agent::ContinueThread",
"alt-enter": "agent::ContinueWithBurnMode"
}
@ -544,11 +545,25 @@
"cmd-k r": "editor::RevealInFileManager",
"cmd-k p": "editor::CopyPath",
"cmd-\\": "pane::SplitRight",
"cmd-k v": "markdown::OpenPreviewToTheSide",
"cmd-shift-v": "markdown::OpenPreview",
"ctrl-cmd-c": "editor::DisplayCursorNames"
}
},
{
"context": "Editor && extension == md",
"use_key_equivalents": true,
"bindings": {
"cmd-k v": "markdown::OpenPreviewToTheSide",
"cmd-shift-v": "markdown::OpenPreview"
}
},
{
"context": "Editor && extension == svg",
"use_key_equivalents": true,
"bindings": {
"cmd-k v": "svg::OpenPreviewToTheSide",
"cmd-shift-v": "svg::OpenPreview"
}
},
{
"context": "Editor && mode == full",
"use_key_equivalents": true,
@ -587,6 +602,7 @@
"alt-cmd-o": ["projects::OpenRecent", { "create_new_window": false }],
"ctrl-cmd-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
"ctrl-cmd-shift-o": ["projects::OpenRemote", { "from_existing_connection": true, "create_new_window": false }],
"cmd-ctrl-b": "branches::OpenRecent",
"ctrl-~": "workspace::NewTerminal",
"cmd-s": "workspace::Save",
"cmd-k s": "workspace::SaveWithoutFormat",
@ -604,6 +620,7 @@
"cmd-8": ["workspace::ActivatePane", 7],
"cmd-9": ["workspace::ActivatePane", 8],
"cmd-b": "workspace::ToggleLeftDock",
"cmd-alt-b": "workspace::ToggleRightDock",
"cmd-r": "workspace::ToggleRightDock",
"cmd-j": "workspace::ToggleBottomDock",
"alt-cmd-y": "workspace::CloseAllDocks",
@ -963,7 +980,9 @@
"context": "BreakpointList",
"bindings": {
"space": "debugger::ToggleEnableBreakpoint",
"backspace": "debugger::UnsetBreakpoint"
"backspace": "debugger::UnsetBreakpoint",
"left": "debugger::PreviousBreakpointProperty",
"right": "debugger::NextBreakpointProperty"
}
},
{

View file

@ -59,7 +59,8 @@
"alt->": "editor::MoveToEnd", // end-of-buffer
"ctrl-l": "editor::ScrollCursorCenterTopBottom", // recenter-top-bottom
"ctrl-s": "buffer_search::Deploy", // isearch-forward
"alt-^": "editor::JoinLines" // join-line
"alt-^": "editor::JoinLines", // join-line
"alt-q": "editor::Rewrap" // fill-paragraph
}
},
{

View file

@ -59,7 +59,8 @@
"alt->": "editor::MoveToEnd", // end-of-buffer
"ctrl-l": "editor::ScrollCursorCenterTopBottom", // recenter-top-bottom
"ctrl-s": "buffer_search::Deploy", // isearch-forward
"alt-^": "editor::JoinLines" // join-line
"alt-^": "editor::JoinLines", // join-line
"alt-q": "editor::Rewrap" // fill-paragraph
}
},
{

View file

@ -72,6 +72,7 @@ gpui = { workspace = true, "features" = ["test-support"] }
indoc.workspace = true
language = { workspace = true, "features" = ["test-support"] }
language_model = { workspace = true, "features" = ["test-support"] }
parking_lot.workspace = true
pretty_assertions.workspace = true
project = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }

View file

@ -96,16 +96,11 @@ impl AgentProfile {
fn is_enabled(settings: &AgentProfileSettings, source: ToolSource, name: String) -> bool {
match source {
ToolSource::Native => *settings.tools.get(name.as_str()).unwrap_or(&false),
ToolSource::ContextServer { id } => {
if settings.enable_all_context_servers {
return true;
}
let Some(preset) = settings.context_servers.get(id.as_ref()) else {
return false;
};
*preset.tools.get(name.as_str()).unwrap_or(&false)
}
ToolSource::ContextServer { id } => settings
.context_servers
.get(id.as_ref())
.and_then(|preset| preset.tools.get(name.as_str()).copied())
.unwrap_or(settings.enable_all_context_servers),
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -19,7 +19,7 @@ use audio::{Audio, Sound};
use collections::{HashMap, HashSet};
use editor::actions::{MoveUp, Paste};
use editor::scroll::Autoscroll;
use editor::{Editor, EditorElement, EditorEvent, EditorStyle, MultiBuffer};
use editor::{Editor, EditorElement, EditorEvent, EditorStyle, MultiBuffer, SelectionEffects};
use gpui::{
AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent, ClipboardEntry,
ClipboardItem, DefiniteLength, EdgesRefinement, Empty, Entity, EventEmitter, Focusable, Hsla,
@ -47,8 +47,8 @@ use std::time::Duration;
use text::ToPoint;
use theme::ThemeSettings;
use ui::{
Disclosure, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState, TextSize, Tooltip,
prelude::*,
Banner, Disclosure, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState, TextSize,
Tooltip, prelude::*,
};
use util::ResultExt as _;
use util::markdown::MarkdownCodeBlock;
@ -58,6 +58,7 @@ use zed_llm_client::CompletionIntent;
const CODEBLOCK_CONTAINER_GROUP: &str = "codeblock_container";
const EDIT_PREVIOUS_MESSAGE_MIN_LINES: usize = 1;
const RESPONSE_PADDING_X: Pixels = px(19.);
pub struct ActiveThread {
context_store: Entity<ContextStore>,
@ -204,7 +205,7 @@ pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle
MarkdownStyle {
base_text_style: text_style.clone(),
syntax: cx.theme().syntax().clone(),
selection_background_color: cx.theme().players().local().selection,
selection_background_color: cx.theme().colors().element_selection_background,
code_block_overflow_x_scroll: true,
table_overflow_x_scroll: true,
heading_level_styles: Some(HeadingLevelStyles {
@ -301,7 +302,7 @@ fn tool_use_markdown_style(window: &Window, cx: &mut App) -> MarkdownStyle {
MarkdownStyle {
base_text_style: text_style,
syntax: cx.theme().syntax().clone(),
selection_background_color: cx.theme().players().local().selection,
selection_background_color: cx.theme().colors().element_selection_background,
code_block_overflow_x_scroll: false,
code_block: StyleRefinement {
margin: EdgesRefinement::default(),
@ -689,9 +690,12 @@ fn open_markdown_link(
})
.context("Could not find matching symbol")?;
editor.change_selections(Some(Autoscroll::center()), window, cx, |s| {
s.select_anchor_ranges([symbol_range.start..symbol_range.start])
});
editor.change_selections(
SelectionEffects::scroll(Autoscroll::center()),
window,
cx,
|s| s.select_anchor_ranges([symbol_range.start..symbol_range.start]),
);
anyhow::Ok(())
})
})
@ -708,10 +712,15 @@ fn open_markdown_link(
.downcast::<Editor>()
.context("Item is not an editor")?;
active_editor.update_in(cx, |editor, window, cx| {
editor.change_selections(Some(Autoscroll::center()), window, cx, |s| {
s.select_ranges([Point::new(line_range.start as u32, 0)
..Point::new(line_range.start as u32, 0)])
});
editor.change_selections(
SelectionEffects::scroll(Autoscroll::center()),
window,
cx,
|s| {
s.select_ranges([Point::new(line_range.start as u32, 0)
..Point::new(line_range.start as u32, 0)])
},
);
anyhow::Ok(())
})
})
@ -1140,6 +1149,9 @@ impl ActiveThread {
self.save_thread(cx);
cx.notify();
}
ThreadEvent::RetriesFailed { message } => {
self.show_notification(message, ui::IconName::Warning, window, cx);
}
}
}
@ -1835,9 +1847,10 @@ impl ActiveThread {
.filter(|(id, _)| *id == message_id)
.map(|(_, state)| state);
let colors = cx.theme().colors();
let editor_bg_color = colors.editor_background;
let panel_bg = colors.panel_background;
let (editor_bg_color, panel_bg) = {
let colors = cx.theme().colors();
(colors.editor_background, colors.panel_background)
};
let open_as_markdown = IconButton::new(("open-as-markdown", ix), IconName::DocumentText)
.icon_size(IconSize::XSmall)
@ -1862,9 +1875,6 @@ impl ActiveThread {
this.scroll_to_top(cx);
}));
// For all items that should be aligned with the LLM's response.
const RESPONSE_PADDING_X: Pixels = px(19.);
let show_feedback = thread.is_turn_end(ix);
let feedback_container = h_flex()
.group("feedback_container")
@ -2025,152 +2035,162 @@ impl ActiveThread {
}
});
let styled_message = match message.role {
Role::User => v_flex()
.id(("message-container", ix))
.pt_2()
.pl_2()
.pr_2p5()
.pb_4()
.child(
let styled_message = if message.ui_only {
self.render_ui_notification(message_content, ix, cx)
} else {
match message.role {
Role::User => {
let colors = cx.theme().colors();
v_flex()
.id(("user-message", ix))
.bg(editor_bg_color)
.rounded_lg()
.shadow_md()
.border_1()
.border_color(colors.border)
.hover(|hover| hover.border_color(colors.text_accent.opacity(0.5)))
.id(("message-container", ix))
.pt_2()
.pl_2()
.pr_2p5()
.pb_4()
.child(
v_flex()
.p_2p5()
.gap_1()
.children(message_content)
.when_some(editing_message_state, |this, state| {
let focus_handle = state.editor.focus_handle(cx).clone();
.id(("user-message", ix))
.bg(editor_bg_color)
.rounded_lg()
.shadow_md()
.border_1()
.border_color(colors.border)
.hover(|hover| hover.border_color(colors.text_accent.opacity(0.5)))
.child(
v_flex()
.p_2p5()
.gap_1()
.children(message_content)
.when_some(editing_message_state, |this, state| {
let focus_handle = state.editor.focus_handle(cx).clone();
this.child(
h_flex()
.w_full()
.gap_1()
.justify_between()
.flex_wrap()
.child(
this.child(
h_flex()
.gap_1p5()
.w_full()
.gap_1()
.justify_between()
.flex_wrap()
.child(
div()
.opacity(0.8)
h_flex()
.gap_1p5()
.child(
Icon::new(IconName::Warning)
.size(IconSize::Indicator)
.color(Color::Warning)
div()
.opacity(0.8)
.child(
Icon::new(IconName::Warning)
.size(IconSize::Indicator)
.color(Color::Warning)
),
)
.child(
Label::new("Editing will restart the thread from this point.")
.color(Color::Muted)
.size(LabelSize::XSmall),
),
)
.child(
Label::new("Editing will restart the thread from this point.")
.color(Color::Muted)
.size(LabelSize::XSmall),
),
)
.child(
h_flex()
.gap_0p5()
.child(
IconButton::new(
"cancel-edit-message",
IconName::Close,
)
.shape(ui::IconButtonShape::Square)
.icon_color(Color::Error)
.icon_size(IconSize::Small)
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Cancel Edit",
&menu::Cancel,
&focus_handle,
window,
cx,
h_flex()
.gap_0p5()
.child(
IconButton::new(
"cancel-edit-message",
IconName::Close,
)
}
})
.on_click(cx.listener(Self::handle_cancel_click)),
.shape(ui::IconButtonShape::Square)
.icon_color(Color::Error)
.icon_size(IconSize::Small)
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Cancel Edit",
&menu::Cancel,
&focus_handle,
window,
cx,
)
}
})
.on_click(cx.listener(Self::handle_cancel_click)),
)
.child(
IconButton::new(
"confirm-edit-message",
IconName::Return,
)
.disabled(state.editor.read(cx).is_empty(cx))
.shape(ui::IconButtonShape::Square)
.icon_color(Color::Muted)
.icon_size(IconSize::Small)
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Regenerate",
&menu::Confirm,
&focus_handle,
window,
cx,
)
}
})
.on_click(
cx.listener(Self::handle_regenerate_click),
),
),
)
.child(
IconButton::new(
"confirm-edit-message",
IconName::Return,
)
.disabled(state.editor.read(cx).is_empty(cx))
.shape(ui::IconButtonShape::Square)
.icon_color(Color::Muted)
.icon_size(IconSize::Small)
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
"Regenerate",
&menu::Confirm,
&focus_handle,
window,
cx,
)
}
})
.on_click(
cx.listener(Self::handle_regenerate_click),
),
),
)
)
}),
}),
)
.on_click(cx.listener({
let message_creases = message.creases.clone();
move |this, _, window, cx| {
if let Some(message_text) =
this.thread.read(cx).message(message_id).and_then(|message| {
message.segments.first().and_then(|segment| {
match segment {
MessageSegment::Text(message_text) => {
Some(Into::<Arc<str>>::into(message_text.as_str()))
}
_ => {
None
}
}
})
})
{
this.start_editing_message(
message_id,
message_text,
&message_creases,
window,
cx,
);
}
}
})),
)
.on_click(cx.listener({
let message_creases = message.creases.clone();
move |this, _, window, cx| {
if let Some(message_text) =
this.thread.read(cx).message(message_id).and_then(|message| {
message.segments.first().and_then(|segment| {
match segment {
MessageSegment::Text(message_text) => {
Some(Into::<Arc<str>>::into(message_text.as_str()))
}
_ => {
None
}
}
})
})
{
this.start_editing_message(
message_id,
message_text,
&message_creases,
window,
cx,
);
}
}
})),
),
Role::Assistant => v_flex()
.id(("message-container", ix))
.px(RESPONSE_PADDING_X)
.gap_2()
.children(message_content)
.when(has_tool_uses, |parent| {
parent.children(tool_uses.into_iter().map(|tool_use| {
self.render_tool_use(tool_use, window, workspace.clone(), cx)
}))
}),
Role::System => div().id(("message-container", ix)).py_1().px_2().child(
v_flex()
.bg(colors.editor_background)
.rounded_sm()
.child(div().p_4().children(message_content)),
),
}
Role::Assistant => v_flex()
.id(("message-container", ix))
.px(RESPONSE_PADDING_X)
.gap_2()
.children(message_content)
.when(has_tool_uses, |parent| {
parent.children(tool_uses.into_iter().map(|tool_use| {
self.render_tool_use(tool_use, window, workspace.clone(), cx)
}))
}),
Role::System => {
let colors = cx.theme().colors();
div().id(("message-container", ix)).py_1().px_2().child(
v_flex()
.bg(colors.editor_background)
.rounded_sm()
.child(div().p_4().children(message_content)),
)
}
}
};
let after_editing_message = self
@ -2509,6 +2529,26 @@ impl ActiveThread {
.blend(cx.theme().colors().editor_foreground.opacity(0.025))
}
fn render_ui_notification(
&self,
message_content: impl IntoIterator<Item = impl IntoElement>,
ix: usize,
cx: &mut Context<Self>,
) -> Stateful<Div> {
let message = div()
.flex_1()
.min_w_0()
.text_size(TextSize::XSmall.rems(cx))
.text_color(cx.theme().colors().text_muted)
.children(message_content);
div()
.id(("message-container", ix))
.py_1()
.px_2p5()
.child(Banner::new().severity(ui::Severity::Warning).child(message))
}
fn render_message_thinking_segment(
&self,
message_id: MessageId,
@ -3763,9 +3803,9 @@ mod tests {
// Stream response to user message
thread.update(cx, |thread, cx| {
let request =
thread.to_completion_request(model.clone(), CompletionIntent::UserPrompt, cx);
thread.stream_completion(request, model, cx.active_window(), cx)
let intent = CompletionIntent::UserPrompt;
let request = thread.to_completion_request(model.clone(), intent, cx);
thread.stream_completion(request, model, intent, cx.active_window(), cx)
});
// Follow the agent
cx.update(|window, cx| {

View file

@ -180,7 +180,7 @@ impl ConfigurationSource {
}
fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)>) -> String {
let (name, path, args, env) = match existing {
let (name, command, args, env) = match existing {
Some((id, cmd)) => {
let args = serde_json::to_string(&cmd.args).unwrap();
let env = serde_json::to_string(&cmd.env.unwrap_or_default()).unwrap();
@ -198,14 +198,12 @@ fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)
r#"{{
/// The name of your MCP server
"{name}": {{
"command": {{
/// The path to the executable
"path": "{path}",
/// The arguments to pass to the executable
"args": {args},
/// The environment variables to set for the executable
"env": {env}
}}
/// The command which runs the MCP server
"command": "{command}",
/// The arguments to pass to the MCP server
"args": {args},
/// The environment variables to set
"env": {env}
}}
}}"#
)
@ -439,8 +437,7 @@ fn parse_input(text: &str) -> Result<(ContextServerId, ContextServerCommand)> {
let object = value.as_object().context("Expected object")?;
anyhow::ensure!(object.len() == 1, "Expected exactly one key-value pair");
let (context_server_name, value) = object.into_iter().next().unwrap();
let command = value.get("command").context("Expected command")?;
let command: ContextServerCommand = serde_json::from_value(command.clone())?;
let command: ContextServerCommand = serde_json::from_value(value.clone())?;
Ok((ContextServerId(context_server_name.clone().into()), command))
}
@ -748,7 +745,7 @@ pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle
MarkdownStyle {
base_text_style: text_style.clone(),
selection_background_color: cx.theme().players().local().selection,
selection_background_color: colors.element_selection_background,
link: TextStyleRefinement {
background_color: Some(colors.editor_foreground.opacity(0.025)),
underline: Some(UnderlineStyle {

View file

@ -5,7 +5,8 @@ use anyhow::Result;
use buffer_diff::DiffHunkStatus;
use collections::{HashMap, HashSet};
use editor::{
Direction, Editor, EditorEvent, EditorSettings, MultiBuffer, MultiBufferSnapshot, ToPoint,
Direction, Editor, EditorEvent, EditorSettings, MultiBuffer, MultiBufferSnapshot,
SelectionEffects, ToPoint,
actions::{GoToHunk, GoToPreviousHunk},
scroll::Autoscroll,
};
@ -171,15 +172,9 @@ impl AgentDiffPane {
if let Some(first_hunk) = first_hunk {
let first_hunk_start = first_hunk.multi_buffer_range().start;
editor.change_selections(
Some(Autoscroll::fit()),
window,
cx,
|selections| {
selections
.select_anchor_ranges([first_hunk_start..first_hunk_start]);
},
)
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_anchor_ranges([first_hunk_start..first_hunk_start]);
})
}
}
@ -242,7 +237,7 @@ impl AgentDiffPane {
if let Some(first_hunk) = first_hunk {
let first_hunk_start = first_hunk.multi_buffer_range().start;
editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_anchor_ranges([first_hunk_start..first_hunk_start]);
})
}
@ -416,7 +411,7 @@ fn update_editor_selection(
};
if let Some(target_hunk) = target_hunk {
editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
editor.change_selections(Default::default(), window, cx, |selections| {
let next_hunk_start = target_hunk.multi_buffer_range().start;
selections.select_anchor_ranges([next_hunk_start..next_hunk_start]);
})
@ -1380,6 +1375,7 @@ impl AgentDiff {
| ThreadEvent::ToolConfirmationNeeded
| ThreadEvent::ToolUseLimitReached
| ThreadEvent::CancelEditing
| ThreadEvent::RetriesFailed { .. }
| ThreadEvent::ProfileChanged => {}
}
}
@ -1543,7 +1539,7 @@ impl AgentDiff {
let first_hunk_start = first_hunk.multi_buffer_range().start;
editor.change_selections(
Some(Autoscroll::center()),
SelectionEffects::scroll(Autoscroll::center()),
window,
cx,
|selections| {
@ -1867,7 +1863,7 @@ mod tests {
// Rejecting a hunk also moves the cursor to the next hunk, possibly cycling if it's at the end.
editor.update_in(cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |selections| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
selections.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
});
});
@ -2123,7 +2119,7 @@ mod tests {
// Rejecting a hunk also moves the cursor to the next hunk, possibly cycling if it's at the end.
editor1.update_in(cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |selections| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
selections.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
});
});

View file

@ -11,7 +11,7 @@ use language_model::{ConfiguredModel, LanguageModelRegistry};
use picker::popover_menu::PickerPopoverMenu;
use settings::update_settings_file;
use std::sync::Arc;
use ui::{PopoverMenuHandle, Tooltip, prelude::*};
use ui::{ButtonLike, PopoverMenuHandle, Tooltip, prelude::*};
pub struct AgentModelSelector {
selector: Entity<LanguageModelSelector>,
@ -94,20 +94,35 @@ impl Render for AgentModelSelector {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let model = self.selector.read(cx).delegate.active_model(cx);
let model_name = model
.as_ref()
.map(|model| model.model.name().0)
.unwrap_or_else(|| SharedString::from("No model selected"));
let provider_icon = model
.as_ref()
.map(|model| model.provider.icon())
.unwrap_or_else(|| IconName::Ai);
let focus_handle = self.focus_handle.clone();
PickerPopoverMenu::new(
self.selector.clone(),
Button::new("active-model", model_name)
.label_size(LabelSize::Small)
.color(Color::Muted)
.icon(IconName::ChevronDown)
.icon_size(IconSize::XSmall)
.icon_position(IconPosition::End)
.icon_color(Color::Muted),
ButtonLike::new("active-model")
.child(
Icon::new(provider_icon)
.color(Color::Muted)
.size(IconSize::XSmall),
)
.child(
Label::new(model_name)
.color(Color::Muted)
.size(LabelSize::Small)
.ml_0p5(),
)
.child(
Icon::new(IconName::ChevronDown)
.color(Color::Muted)
.size(IconSize::XSmall),
),
move |window, cx| {
Tooltip::for_action_in(
"Change Model",

File diff suppressed because it is too large Load diff

View file

@ -4,6 +4,7 @@ mod agent_diff;
mod agent_model_selector;
mod agent_panel;
mod buffer_codegen;
mod burn_mode_tooltip;
mod context_picker;
mod context_server_configuration;
mod context_strip;
@ -11,7 +12,6 @@ mod debug;
mod inline_assistant;
mod inline_prompt_editor;
mod language_model_selector;
mod max_mode_tooltip;
mod message_editor;
mod profile_selector;
mod slash_command;
@ -48,7 +48,7 @@ pub use crate::agent_panel::{AgentPanel, ConcreteAssistantPanelDelegate};
pub use crate::inline_assistant::InlineAssistant;
use crate::slash_command_settings::SlashCommandSettings;
pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
pub use text_thread_editor::AgentPanelDelegate;
pub use text_thread_editor::{AgentPanelDelegate, TextThreadEditor};
pub use ui::preview::{all_agent_previews, get_agent_preview};
actions!(
@ -157,6 +157,7 @@ pub fn init(
agent::init(cx);
agent_panel::init(cx);
context_server_configuration::init(language_registry.clone(), fs.clone(), cx);
TextThreadEditor::init(cx);
register_slash_commands(cx);
inline_assistant::init(

View file

@ -1094,15 +1094,9 @@ mod tests {
};
use language_model::{LanguageModelRegistry, TokenUsage};
use rand::prelude::*;
use serde::Serialize;
use settings::SettingsStore;
use std::{future, sync::Arc};
#[derive(Serialize)]
pub struct DummyCompletionRequest {
pub name: String,
}
#[gpui::test(iterations = 10)]
async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) {
init_test(cx);

View file

@ -1,11 +1,11 @@
use gpui::{Context, FontWeight, IntoElement, Render, Window};
use ui::{prelude::*, tooltip_container};
pub struct MaxModeTooltip {
pub struct BurnModeTooltip {
selected: bool,
}
impl MaxModeTooltip {
impl BurnModeTooltip {
pub fn new() -> Self {
Self { selected: false }
}
@ -16,7 +16,7 @@ impl MaxModeTooltip {
}
}
impl Render for MaxModeTooltip {
impl Render for BurnModeTooltip {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let (icon, color) = if self.selected {
(IconName::ZedBurnModeOn, Color::Error)

View file

@ -661,7 +661,7 @@ fn recent_context_picker_entries(
let active_thread_id = workspace
.panel::<AgentPanel>(cx)
.and_then(|panel| Some(panel.read(cx).active_thread()?.read(cx).id()));
.and_then(|panel| Some(panel.read(cx).active_thread(cx)?.read(cx).id()));
if let Some((thread_store, text_thread_store)) = thread_store
.and_then(|store| store.upgrade())
@ -930,8 +930,8 @@ impl MentionLink {
format!(
"[@{} ({}-{})]({}:{}:{}-{})",
file_name,
line_range.start,
line_range.end,
line_range.start + 1,
line_range.end + 1,
Self::SELECTION,
full_path,
line_range.start,

View file

@ -161,7 +161,7 @@ impl ContextStrip {
let workspace = self.workspace.upgrade()?;
let panel = workspace.read(cx).panel::<AgentPanel>(cx)?.read(cx);
if let Some(active_thread) = panel.active_thread() {
if let Some(active_thread) = panel.active_thread(cx) {
let weak_active_thread = active_thread.downgrade();
let active_thread = active_thread.read(cx);

View file

@ -18,6 +18,7 @@ use agent_settings::AgentSettings;
use anyhow::{Context as _, Result};
use client::telemetry::Telemetry;
use collections::{HashMap, HashSet, VecDeque, hash_map};
use editor::SelectionEffects;
use editor::{
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorEvent, ExcerptId, ExcerptRange,
MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint,
@ -1159,7 +1160,7 @@ impl InlineAssistant {
let position = assist.range.start;
editor.update(cx, |editor, cx| {
editor.change_selections(None, window, cx, |selections| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
selections.select_anchor_ranges([position..position])
});

View file

@ -399,7 +399,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let all_models = self.all_models.clone();
let current_index = self.selected_index;
let active_model = (self.get_active_model)(cx);
let bg_executor = cx.background_executor();
let language_model_registry = LanguageModelRegistry::global(cx);
@ -441,12 +441,9 @@ impl PickerDelegate for LanguageModelPickerDelegate {
cx.spawn_in(window, async move |this, cx| {
this.update_in(cx, |this, window, cx| {
this.delegate.filtered_entries = filtered_models.entries();
// Preserve selection focus
let new_index = if current_index >= this.delegate.filtered_entries.len() {
0
} else {
current_index
};
// Finds the currently selected model in the list
let new_index =
Self::get_active_model_index(&this.delegate.filtered_entries, active_model);
this.set_selected_index(new_index, Some(picker::Direction::Down), true, window, cx);
cx.notify();
})

View file

@ -575,7 +575,7 @@ impl MessageEditor {
fn render_burn_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
let thread = self.thread.read(cx);
let model = thread.configured_model();
if !model?.model.supports_max_mode() {
if !model?.model.supports_burn_mode() {
return None;
}

View file

@ -1,8 +1,8 @@
use crate::{
burn_mode_tooltip::BurnModeTooltip,
language_model_selector::{
LanguageModelSelector, ToggleModelSelector, language_model_selector,
},
max_mode_tooltip::MaxModeTooltip,
};
use agent_settings::{AgentSettings, CompletionMode};
use anyhow::Result;
@ -21,7 +21,6 @@ use editor::{
BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata, CustomBlockId, FoldId,
RenderBlock, ToDisplayPoint,
},
scroll::Autoscroll,
};
use editor::{FoldPlaceholder, display_map::CreaseId};
use fs::Fs;
@ -69,7 +68,7 @@ use workspace::{
searchable::{Direction, SearchableItemHandle},
};
use workspace::{
Save, Toast, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
Save, Toast, Workspace,
item::{self, FollowableItem, Item, ItemHandle},
notifications::NotificationId,
pane,
@ -389,7 +388,7 @@ impl TextThreadEditor {
cursor..cursor
};
self.editor.update(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges([new_selection])
});
});
@ -449,8 +448,7 @@ impl TextThreadEditor {
if let Some(command) = self.slash_commands.command(name, cx) {
self.editor.update(cx, |editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
editor
.change_selections(Some(Autoscroll::fit()), window, cx, |s| s.try_cancel());
editor.change_selections(Default::default(), window, cx, |s| s.try_cancel());
let snapshot = editor.buffer().read(cx).snapshot(cx);
let newest_cursor = editor.selections.newest::<Point>(cx).head();
if newest_cursor.column > 0
@ -1583,7 +1581,7 @@ impl TextThreadEditor {
self.editor.update(cx, |editor, cx| {
editor.transact(window, cx, |this, window, cx| {
this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
this.change_selections(Default::default(), window, cx, |s| {
s.select(selections);
});
this.insert("", window, cx);
@ -2075,12 +2073,12 @@ impl TextThreadEditor {
)
}
fn render_max_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
fn render_burn_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
let context = self.context().read(cx);
let active_model = LanguageModelRegistry::read_global(cx)
.default_model()
.map(|default| default.model)?;
if !active_model.supports_max_mode() {
if !active_model.supports_burn_mode() {
return None;
}
@ -2107,7 +2105,7 @@ impl TextThreadEditor {
});
}))
.tooltip(move |_window, cx| {
cx.new(|_| MaxModeTooltip::new().selected(burn_mode_enabled))
cx.new(|_| BurnModeTooltip::new().selected(burn_mode_enabled))
.into()
})
.into_any_element(),
@ -2122,12 +2120,21 @@ impl TextThreadEditor {
let active_model = LanguageModelRegistry::read_global(cx)
.default_model()
.map(|default| default.model);
let focus_handle = self.editor().focus_handle(cx).clone();
let model_name = match active_model {
Some(model) => model.name().0,
None => SharedString::from("No model selected"),
};
let active_provider = LanguageModelRegistry::read_global(cx)
.default_model()
.map(|default| default.provider);
let provider_icon = match active_provider {
Some(provider) => provider.icon(),
None => IconName::Ai,
};
let focus_handle = self.editor().focus_handle(cx).clone();
PickerPopoverMenu::new(
self.language_model_selector.clone(),
ButtonLike::new("active-model")
@ -2135,10 +2142,16 @@ impl TextThreadEditor {
.child(
h_flex()
.gap_0p5()
.child(
Icon::new(provider_icon)
.color(Color::Muted)
.size(IconSize::XSmall),
)
.child(
Label::new(model_name)
.color(Color::Muted)
.size(LabelSize::Small)
.color(Color::Muted),
.ml_0p5(),
)
.child(
Icon::new(IconName::ChevronDown)
@ -2575,7 +2588,7 @@ impl Render for TextThreadEditor {
};
let language_model_selector = self.language_model_selector_menu_handle.clone();
let max_mode_toggle = self.render_max_mode_toggle(cx);
let burn_mode_toggle = self.render_burn_mode_toggle(cx);
v_flex()
.key_context("ContextEditor")
@ -2630,7 +2643,7 @@ impl Render for TextThreadEditor {
h_flex()
.gap_0p5()
.child(self.render_inject_context_menu(cx))
.when_some(max_mode_toggle, |this, element| this.child(element)),
.when_some(burn_mode_toggle, |this, element| this.child(element)),
)
.child(
h_flex()
@ -2924,13 +2937,6 @@ impl FollowableItem for TextThreadEditor {
}
}
pub struct ContextEditorToolbarItem {
active_context_editor: Option<WeakEntity<TextThreadEditor>>,
model_summary_editor: Entity<Editor>,
}
impl ContextEditorToolbarItem {}
pub fn render_remaining_tokens(
context_editor: &Entity<TextThreadEditor>,
cx: &App,
@ -2983,98 +2989,6 @@ pub fn render_remaining_tokens(
)
}
impl Render for ContextEditorToolbarItem {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let left_side = h_flex()
.group("chat-title-group")
.gap_1()
.items_center()
.flex_grow()
.child(
div()
.w_full()
.when(self.active_context_editor.is_some(), |left_side| {
left_side.child(self.model_summary_editor.clone())
}),
)
.child(
div().visible_on_hover("chat-title-group").child(
IconButton::new("regenerate-context", IconName::RefreshTitle)
.shape(ui::IconButtonShape::Square)
.tooltip(Tooltip::text("Regenerate Title"))
.on_click(cx.listener(move |_, _, _window, cx| {
cx.emit(ContextEditorToolbarItemEvent::RegenerateSummary)
})),
),
);
let right_side = h_flex()
.gap_2()
// TODO display this in a nicer way, once we have a design for it.
// .children({
// let project = self
// .workspace
// .upgrade()
// .map(|workspace| workspace.read(cx).project().downgrade());
//
// let scan_items_remaining = cx.update_global(|db: &mut SemanticDb, cx| {
// project.and_then(|project| db.remaining_summaries(&project, cx))
// });
// scan_items_remaining
// .map(|remaining_items| format!("Files to scan: {}", remaining_items))
// })
.children(
self.active_context_editor
.as_ref()
.and_then(|editor| editor.upgrade())
.and_then(|editor| render_remaining_tokens(&editor, cx)),
);
h_flex()
.px_0p5()
.size_full()
.gap_2()
.justify_between()
.child(left_side)
.child(right_side)
}
}
impl ToolbarItemView for ContextEditorToolbarItem {
fn set_active_pane_item(
&mut self,
active_pane_item: Option<&dyn ItemHandle>,
_window: &mut Window,
cx: &mut Context<Self>,
) -> ToolbarItemLocation {
self.active_context_editor = active_pane_item
.and_then(|item| item.act_as::<TextThreadEditor>(cx))
.map(|editor| editor.downgrade());
cx.notify();
if self.active_context_editor.is_none() {
ToolbarItemLocation::Hidden
} else {
ToolbarItemLocation::PrimaryRight
}
}
fn pane_focus_update(
&mut self,
_pane_focused: bool,
_window: &mut Window,
cx: &mut Context<Self>,
) {
cx.notify();
}
}
impl EventEmitter<ToolbarItemEvent> for ContextEditorToolbarItem {}
pub enum ContextEditorToolbarItemEvent {
RegenerateSummary,
}
impl EventEmitter<ContextEditorToolbarItemEvent> for ContextEditorToolbarItem {}
enum PendingSlashCommand {}
fn invoked_slash_command_fold_placeholder(
@ -3240,6 +3154,7 @@ pub fn make_lsp_adapter_delegate(
#[cfg(test)]
mod tests {
use super::*;
use editor::SelectionEffects;
use fs::FakeFs;
use gpui::{App, TestAppContext, VisualTestContext};
use indoc::indoc;
@ -3465,7 +3380,9 @@ mod tests {
) {
context_editor.update_in(cx, |context_editor, window, cx| {
context_editor.editor.update(cx, |editor, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([range]));
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([range])
});
});
context_editor.copy(&Default::default(), window, cx);

View file

@ -1,13 +1,13 @@
mod agent_notification;
mod animated_label;
mod burn_mode_tooltip;
mod context_pill;
mod max_mode_tooltip;
mod onboarding_modal;
pub mod preview;
mod upsell;
pub use agent_notification::*;
pub use animated_label::*;
pub use burn_mode_tooltip::*;
pub use context_pill::*;
pub use max_mode_tooltip::*;
pub use onboarding_modal::*;

View file

@ -2346,13 +2346,13 @@ impl AssistantContext {
completion_request.messages.push(request_message);
}
}
let supports_max_mode = if let Some(model) = model {
model.supports_max_mode()
let supports_burn_mode = if let Some(model) = model {
model.supports_burn_mode()
} else {
false
};
if supports_max_mode {
if supports_burn_mode {
completion_request.mode = Some(self.completion_mode.into());
}
completion_request

View file

@ -74,7 +74,7 @@ impl SlashCommand for DeltaSlashCommand {
.slice(section.range.to_offset(&context_buffer)),
);
file_command_new_outputs.push(Arc::new(FileSlashCommand).run(
&[metadata.path.clone()],
std::slice::from_ref(&metadata.path),
context_slash_command_output_sections,
context_buffer.clone(),
workspace.clone(),

View file

@ -10,7 +10,7 @@ use assistant_tool::{
ToolUseStatus,
};
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey, scroll::Autoscroll};
use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey};
use futures::StreamExt;
use gpui::{
Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task,
@ -823,7 +823,7 @@ impl ToolCard for EditFileToolCard {
let first_hunk_start =
first_hunk.multi_buffer_range().start;
editor.change_selections(
Some(Autoscroll::fit()),
Default::default(),
window,
cx,
|selections| {
@ -1065,7 +1065,7 @@ fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
MarkdownStyle {
base_text_style: text_style.clone(),
selection_background_color: cx.theme().players().local().selection,
selection_background_color: cx.theme().colors().element_selection_background,
..Default::default()
}
}

View file

@ -691,7 +691,7 @@ fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
MarkdownStyle {
base_text_style: text_style.clone(),
selection_background_color: cx.theme().players().local().selection,
selection_background_color: cx.theme().colors().element_selection_background,
..Default::default()
}
}

View file

@ -1,7 +1,7 @@
use auto_update::AutoUpdater;
use client::proto::UpdateNotification;
use editor::{Editor, MultiBuffer};
use gpui::{App, Context, DismissEvent, Entity, SharedString, Window, actions, prelude::*};
use gpui::{App, Context, DismissEvent, Entity, Window, actions, prelude::*};
use http_client::HttpClient;
use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
use release_channel::{AppVersion, ReleaseChannel};
@ -94,7 +94,6 @@ fn view_release_notes_locally(
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
let tab_content = Some(SharedString::from(body.title.to_string()));
let editor = cx.new(|cx| {
Editor::for_multibuffer(buffer, Some(project), window, cx)
});
@ -105,7 +104,6 @@ fn view_release_notes_locally(
editor,
workspace_handle,
language_registry,
tab_content,
window,
cx,
);

View file

@ -1867,7 +1867,7 @@ mod tests {
let hunk = diff.hunks(&buffer, cx).next().unwrap();
let new_index_text = diff
.stage_or_unstage_hunks(true, &[hunk.clone()], &buffer, true, cx)
.stage_or_unstage_hunks(true, std::slice::from_ref(&hunk), &buffer, true, cx)
.unwrap()
.to_string();
assert_eq!(new_index_text, buffer_text);

View file

@ -734,8 +734,8 @@ impl Database {
users.push(proto::User {
id: user.id.to_proto(),
avatar_url: format!(
"https://github.com/{}.png?size=128",
user.github_login
"https://avatars.githubusercontent.com/u/{}?s=128&v=4",
user.github_user_id
),
github_login: user.github_login,
name: user.name,

View file

@ -76,7 +76,10 @@ async fn test_purge_old_embeddings(cx: &mut gpui::TestAppContext) {
db.purge_old_embeddings().await.unwrap();
// Try to retrieve the purged embeddings
let retrieved_embeddings = db.get_embeddings(model, &[digest.clone()]).await.unwrap();
let retrieved_embeddings = db
.get_embeddings(model, std::slice::from_ref(&digest))
.await
.unwrap();
assert!(
retrieved_embeddings.is_empty(),
"Old embeddings should have been purged"

View file

@ -179,7 +179,7 @@ struct Session {
}
impl Session {
async fn db(&self) -> tokio::sync::MutexGuard<DbHandle> {
async fn db(&self) -> tokio::sync::MutexGuard<'_, DbHandle> {
#[cfg(test)]
tokio::task::yield_now().await;
let guard = self.db.lock().await;
@ -1037,7 +1037,7 @@ impl Server {
}
}
pub async fn snapshot(self: &Arc<Self>) -> ServerSnapshot {
pub async fn snapshot(self: &Arc<Self>) -> ServerSnapshot<'_> {
ServerSnapshot {
connection_pool: ConnectionPoolGuard {
guard: self.connection_pool.lock(),

View file

@ -178,7 +178,7 @@ async fn test_channel_notes_participant_indices(
channel_view_a.update_in(cx_a, |notes, window, cx| {
notes.editor.update(cx, |editor, cx| {
editor.insert("a", window, cx);
editor.change_selections(None, window, cx, |selections| {
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges(vec![0..1]);
});
});
@ -188,7 +188,7 @@ async fn test_channel_notes_participant_indices(
notes.editor.update(cx, |editor, cx| {
editor.move_down(&Default::default(), window, cx);
editor.insert("b", window, cx);
editor.change_selections(None, window, cx, |selections| {
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges(vec![1..2]);
});
});
@ -198,7 +198,7 @@ async fn test_channel_notes_participant_indices(
notes.editor.update(cx, |editor, cx| {
editor.move_down(&Default::default(), window, cx);
editor.insert("c", window, cx);
editor.change_selections(None, window, cx, |selections| {
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges(vec![2..3]);
});
});
@ -273,12 +273,12 @@ async fn test_channel_notes_participant_indices(
.unwrap();
editor_a.update_in(cx_a, |editor, window, cx| {
editor.change_selections(None, window, cx, |selections| {
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges(vec![0..1]);
});
});
editor_b.update_in(cx_b, |editor, window, cx| {
editor.change_selections(None, window, cx, |selections| {
editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges(vec![2..3]);
});
});

View file

@ -4,7 +4,7 @@ use crate::{
};
use call::ActiveCall;
use editor::{
DocumentColorsRenderMode, Editor, EditorSettings, RowInfo,
DocumentColorsRenderMode, Editor, EditorSettings, RowInfo, SelectionEffects,
actions::{
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst,
ExpandMacroRecursively, MoveToEnd, Redo, Rename, SelectAll, ToggleCodeActions, Undo,
@ -348,7 +348,9 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
// Type a completion trigger character as the guest.
editor_b.update_in(cx_b, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([13..13])
});
editor.handle_input(".", window, cx);
});
cx_b.focus(&editor_b);
@ -461,7 +463,9 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
// Now we do a second completion, this time to ensure that documentation/snippets are
// resolved
editor_b.update_in(cx_b, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([46..46]));
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([46..46])
});
editor.handle_input("; a", window, cx);
editor.handle_input(".", window, cx);
});
@ -613,7 +617,7 @@ async fn test_collaborating_with_code_actions(
// Move cursor to a location that contains code actions.
editor_b.update_in(cx_b, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
});
});
@ -817,7 +821,9 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
// Move cursor to a location that can be renamed.
let prepare_rename = editor_b.update_in(cx_b, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([7..7]));
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([7..7])
});
editor.rename(&Rename, window, cx).unwrap()
});
@ -863,7 +869,9 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
editor.cancel(&editor::actions::Cancel, window, cx);
});
let prepare_rename = editor_b.update_in(cx_b, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([7..8]));
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([7..8])
});
editor.rename(&Rename, window, cx).unwrap()
});
@ -1364,7 +1372,9 @@ async fn test_on_input_format_from_host_to_guest(
// Type a on type formatting trigger character as the guest.
cx_a.focus(&editor_a);
editor_a.update_in(cx_a, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([13..13])
});
editor.handle_input(">", window, cx);
});
@ -1460,7 +1470,9 @@ async fn test_on_input_format_from_guest_to_host(
// Type a on type formatting trigger character as the guest.
cx_b.focus(&editor_b);
editor_b.update_in(cx_b, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([13..13])
});
editor.handle_input(":", window, cx);
});
@ -1697,7 +1709,9 @@ async fn test_mutual_editor_inlay_hint_cache_update(
let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
editor_b.update_in(cx_b, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13].clone()));
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([13..13].clone())
});
editor.handle_input(":", window, cx);
});
cx_b.focus(&editor_b);
@ -1718,7 +1732,9 @@ async fn test_mutual_editor_inlay_hint_cache_update(
let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
editor_a.update_in(cx_a, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([13..13])
});
editor.handle_input("a change to increment both buffers' versions", window, cx);
});
cx_a.focus(&editor_a);
@ -2121,7 +2137,9 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo
});
editor_a.update_in(cx_a, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13].clone()));
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([13..13].clone())
});
editor.handle_input(":", window, cx);
});
color_request_handle.next().await.unwrap();

View file

@ -6,7 +6,7 @@ use collab_ui::{
channel_view::ChannelView,
notifications::project_shared_notification::ProjectSharedNotification,
};
use editor::{Editor, MultiBuffer, PathKey};
use editor::{Editor, MultiBuffer, PathKey, SelectionEffects};
use gpui::{
AppContext as _, BackgroundExecutor, BorrowAppContext, Entity, SharedString, TestAppContext,
VisualContext, VisualTestContext, point,
@ -376,7 +376,9 @@ async fn test_basic_following(
// Changes to client A's editor are reflected on client B.
editor_a1.update_in(cx_a, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2]));
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([1..1, 2..2])
});
});
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
executor.run_until_parked();
@ -393,7 +395,9 @@ async fn test_basic_following(
editor_b1.update(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
editor_a1.update_in(cx_a, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([3..3])
});
editor.set_scroll_position(point(0., 100.), window, cx);
});
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
@ -1647,7 +1651,9 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T
// b should follow a to position 1
editor_a.update_in(cx_a, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]))
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([1..1])
})
});
cx_a.executor()
.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
@ -1667,7 +1673,9 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T
// b should not follow a to position 2
editor_a.update_in(cx_a, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]))
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([2..2])
})
});
cx_a.executor()
.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
@ -1968,7 +1976,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
notes.editor.update(cx, |editor, cx| {
editor.insert("Hello from A.", window, cx);
editor.change_selections(None, window, cx, |selections| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
selections.select_ranges(vec![3..4]);
});
});
@ -2109,7 +2117,7 @@ async fn test_following_after_replacement(cx_a: &mut TestAppContext, cx_b: &mut
workspace.add_item_to_center(Box::new(editor.clone()) as _, window, cx)
});
editor.update_in(cx_a, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::row_range(4..4)]);
})
});

View file

@ -7,8 +7,8 @@ use client::{
};
use collections::HashMap;
use editor::{
CollaborationHub, DisplayPoint, Editor, EditorEvent, display_map::ToDisplayPoint,
scroll::Autoscroll,
CollaborationHub, DisplayPoint, Editor, EditorEvent, SelectionEffects,
display_map::ToDisplayPoint, scroll::Autoscroll,
};
use gpui::{
AnyView, App, ClipboardItem, Context, Entity, EventEmitter, Focusable, Pixels, Point, Render,
@ -260,9 +260,16 @@ impl ChannelView {
.find(|item| &Channel::slug(&item.text).to_lowercase() == &position)
{
self.editor.update(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::focused()), window, cx, |s| {
s.replace_cursors_with(|map| vec![item.range.start.to_display_point(map)])
})
editor.change_selections(
SelectionEffects::scroll(Autoscroll::focused()),
window,
cx,
|s| {
s.replace_cursors_with(|map| {
vec![item.range.start.to_display_point(map)]
})
},
)
});
return;
}

View file

@ -29,6 +29,7 @@ impl Display for ContextServerId {
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
pub struct ContextServerCommand {
#[serde(rename = "command")]
pub path: String,
pub args: Vec<String>,
pub env: Option<HashMap<String, String>>,

View file

@ -698,16 +698,16 @@ async fn stream_completion(
completion_url: Arc<str>,
request: Request,
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
let is_vision_request = request.messages.last().map_or(false, |message| match message {
ChatMessage::User { content }
| ChatMessage::Assistant { content, .. }
| ChatMessage::Tool { content, .. } => {
matches!(content, ChatMessageContent::Multipart(parts) if parts.iter().any(|part| matches!(part, ChatMessagePart::Image { .. })))
}
_ => false,
});
let is_vision_request = request.messages.iter().any(|message| match message {
ChatMessage::User { content }
| ChatMessage::Assistant { content, .. }
| ChatMessage::Tool { content, .. } => {
matches!(content, ChatMessageContent::Multipart(parts) if parts.iter().any(|part| matches!(part, ChatMessagePart::Image { .. })))
}
_ => false,
});
let request_builder = HttpRequest::builder()
let mut request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(completion_url.as_ref())
.header(
@ -719,8 +719,12 @@ async fn stream_completion(
)
.header("Authorization", format!("Bearer {}", api_key))
.header("Content-Type", "application/json")
.header("Copilot-Integration-Id", "vscode-chat")
.header("Copilot-Vision-Request", is_vision_request.to_string());
.header("Copilot-Integration-Id", "vscode-chat");
if is_vision_request {
request_builder =
request_builder.header("Copilot-Vision-Request", is_vision_request.to_string());
}
let is_streaming = request.stream;

View file

@ -264,7 +264,8 @@ fn common_prefix<T1: Iterator<Item = char>, T2: Iterator<Item = char>>(a: T1, b:
mod tests {
use super::*;
use editor::{
Editor, ExcerptRange, MultiBuffer, test::editor_lsp_test_context::EditorLspTestContext,
Editor, ExcerptRange, MultiBuffer, SelectionEffects,
test::editor_lsp_test_context::EditorLspTestContext,
};
use fs::FakeFs;
use futures::StreamExt;
@ -478,7 +479,7 @@ mod tests {
// Reset the editor to verify how suggestions behave when tabbing on leading indentation.
cx.update_editor(|editor, window, cx| {
editor.set_text("fn foo() {\n \n}", window, cx);
editor.change_selections(None, window, cx, |s| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(1, 2)..Point::new(1, 2)])
});
});
@ -767,7 +768,7 @@ mod tests {
);
_ = editor.update(cx, |editor, window, cx| {
// Ensure copilot suggestions are shown for the first excerpt.
editor.change_selections(None, window, cx, |s| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(1, 5)..Point::new(1, 5)])
});
editor.next_edit_prediction(&Default::default(), window, cx);
@ -793,7 +794,7 @@ mod tests {
);
_ = editor.update(cx, |editor, window, cx| {
// Move to another excerpt, ensuring the suggestion gets cleared.
editor.change_selections(None, window, cx, |s| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
});
assert!(!editor.has_active_inline_completion());
@ -1019,7 +1020,7 @@ mod tests {
);
_ = editor.update(cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |selections| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
});
editor.refresh_inline_completion(true, false, window, cx);
@ -1029,7 +1030,7 @@ mod tests {
assert!(copilot_requests.try_next().is_err());
_ = editor.update(cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(5, 0)..Point::new(5, 0)])
});
editor.refresh_inline_completion(true, false, window, cx);

View file

@ -33,6 +33,7 @@ log.workspace = true
paths.workspace = true
serde.workspace = true
serde_json.workspace = true
shlex.workspace = true
task.workspace = true
util.workspace = true
workspace-hack.workspace = true

View file

@ -4,7 +4,6 @@ mod go;
mod javascript;
mod php;
mod python;
mod ruby;
use std::sync::Arc;
@ -25,7 +24,6 @@ use gpui::{App, BorrowAppContext};
use javascript::JsDebugAdapter;
use php::PhpDebugAdapter;
use python::PythonDebugAdapter;
use ruby::RubyDebugAdapter;
use serde_json::json;
use task::{DebugScenario, ZedDebugConfig};
@ -35,7 +33,6 @@ pub fn init(cx: &mut App) {
registry.add_adapter(Arc::from(PythonDebugAdapter::default()));
registry.add_adapter(Arc::from(PhpDebugAdapter::default()));
registry.add_adapter(Arc::from(JsDebugAdapter::default()));
registry.add_adapter(Arc::from(RubyDebugAdapter));
registry.add_adapter(Arc::from(GoDebugAdapter::default()));
registry.add_adapter(Arc::from(GdbDebugAdapter));

View file

@ -5,7 +5,7 @@ use gpui::AsyncApp;
use serde_json::Value;
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
use task::DebugRequest;
use util::ResultExt;
use util::{ResultExt, maybe};
use crate::*;
@ -72,6 +72,24 @@ impl JsDebugAdapter {
let mut configuration = task_definition.config.clone();
if let Some(configuration) = configuration.as_object_mut() {
maybe!({
configuration
.get("type")
.filter(|value| value == &"node-terminal")?;
let command = configuration.get("command")?.as_str()?.to_owned();
let mut args = shlex::split(&command)?.into_iter();
let program = args.next()?;
configuration.insert("program".to_owned(), program.into());
configuration.insert(
"args".to_owned(),
args.map(Value::from).collect::<Vec<_>>().into(),
);
configuration.insert("console".to_owned(), "externalTerminal".into());
Some(())
});
configuration.entry("type").and_modify(normalize_task_type);
if let Some(program) = configuration
.get("program")
.cloned()
@ -96,7 +114,6 @@ impl JsDebugAdapter {
.entry("cwd")
.or_insert(delegate.worktree_root_path().to_string_lossy().into());
configuration.entry("type").and_modify(normalize_task_type);
configuration
.entry("console")
.or_insert("externalTerminal".into());
@ -512,7 +529,7 @@ fn normalize_task_type(task_type: &mut Value) {
};
let new_name = match task_type_str {
"node" | "pwa-node" => "pwa-node",
"node" | "pwa-node" | "node-terminal" => "pwa-node",
"chrome" | "pwa-chrome" => "pwa-chrome",
"edge" | "msedge" | "pwa-edge" | "pwa-msedge" => "pwa-msedge",
_ => task_type_str,

View file

@ -1,208 +0,0 @@
use anyhow::{Result, bail};
use async_trait::async_trait;
use collections::FxHashMap;
use dap::{
DebugRequest, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
adapters::{
DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
},
};
use gpui::{AsyncApp, SharedString};
use language::LanguageName;
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::path::PathBuf;
use std::{ffi::OsStr, sync::Arc};
use task::{DebugScenario, ZedDebugConfig};
use util::command::new_smol_command;
#[derive(Default)]
pub(crate) struct RubyDebugAdapter;
impl RubyDebugAdapter {
const ADAPTER_NAME: &'static str = "Ruby";
}
#[derive(Serialize, Deserialize)]
struct RubyDebugConfig {
script_or_command: Option<String>,
script: Option<String>,
command: Option<String>,
#[serde(default)]
args: Vec<String>,
#[serde(default)]
env: FxHashMap<String, String>,
cwd: Option<PathBuf>,
}
#[async_trait(?Send)]
impl DebugAdapter for RubyDebugAdapter {
fn name(&self) -> DebugAdapterName {
DebugAdapterName(Self::ADAPTER_NAME.into())
}
fn adapter_language_name(&self) -> Option<LanguageName> {
Some(SharedString::new_static("Ruby").into())
}
async fn request_kind(
&self,
_: &serde_json::Value,
) -> Result<StartDebuggingRequestArgumentsRequest> {
Ok(StartDebuggingRequestArgumentsRequest::Launch)
}
fn dap_schema(&self) -> serde_json::Value {
json!({
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "Command name (ruby, rake, bin/rails, bundle exec ruby, etc)",
},
"script": {
"type": "string",
"description": "Absolute path to a Ruby file."
},
"cwd": {
"type": "string",
"description": "Directory to execute the program in",
"default": "${ZED_WORKTREE_ROOT}"
},
"args": {
"type": "array",
"description": "Command line arguments passed to the program",
"items": {
"type": "string"
},
"default": []
},
"env": {
"type": "object",
"description": "Additional environment variables to pass to the debugging (and debugged) process",
"default": {}
},
}
})
}
async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
match zed_scenario.request {
DebugRequest::Launch(launch) => {
let config = RubyDebugConfig {
script_or_command: Some(launch.program),
script: None,
command: None,
args: launch.args,
env: launch.env,
cwd: launch.cwd.clone(),
};
let config = serde_json::to_value(config)?;
Ok(DebugScenario {
adapter: zed_scenario.adapter,
label: zed_scenario.label,
config,
tcp_connection: None,
build: None,
})
}
DebugRequest::Attach(_) => {
anyhow::bail!("Attach requests are unsupported");
}
}
}
async fn get_binary(
&self,
delegate: &Arc<dyn DapDelegate>,
definition: &DebugTaskDefinition,
_user_installed_path: Option<PathBuf>,
_user_args: Option<Vec<String>>,
_cx: &mut AsyncApp,
) -> Result<DebugAdapterBinary> {
let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
let mut rdbg_path = adapter_path.join("rdbg");
if !delegate.fs().is_file(&rdbg_path).await {
match delegate.which("rdbg".as_ref()).await {
Some(path) => rdbg_path = path,
None => {
delegate.output_to_console(
"rdbg not found on path, trying `gem install debug`".to_string(),
);
let output = new_smol_command("gem")
.arg("install")
.arg("--no-document")
.arg("--bindir")
.arg(adapter_path)
.arg("debug")
.output()
.await?;
anyhow::ensure!(
output.status.success(),
"Failed to install rdbg:\n{}",
String::from_utf8_lossy(&output.stderr).to_string()
);
}
}
}
let tcp_connection = definition.tcp_connection.clone().unwrap_or_default();
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
let ruby_config = serde_json::from_value::<RubyDebugConfig>(definition.config.clone())?;
let mut arguments = vec![
"--open".to_string(),
format!("--port={}", port),
format!("--host={}", host),
];
if let Some(script) = &ruby_config.script {
arguments.push(script.clone());
} else if let Some(command) = &ruby_config.command {
arguments.push("--command".to_string());
arguments.push(command.clone());
} else if let Some(command_or_script) = &ruby_config.script_or_command {
if delegate
.which(OsStr::new(&command_or_script))
.await
.is_some()
{
arguments.push("--command".to_string());
}
arguments.push(command_or_script.clone());
} else {
bail!("Ruby debug config must have 'script' or 'command' args");
}
arguments.extend(ruby_config.args);
let mut configuration = definition.config.clone();
if let Some(configuration) = configuration.as_object_mut() {
configuration
.entry("cwd")
.or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
}
Ok(DebugAdapterBinary {
command: Some(rdbg_path.to_string_lossy().to_string()),
arguments,
connection: Some(dap::adapters::TcpArguments {
host,
port,
timeout,
}),
cwd: Some(
ruby_config
.cwd
.unwrap_or(delegate.worktree_root_path().to_owned()),
),
envs: ruby_config.env.into_iter().collect(),
request_args: StartDebuggingRequestArguments {
request: self.request_kind(&definition.config).await?,
configuration,
},
})
}
}

View file

@ -21,7 +21,7 @@ use project::{
use settings::Settings as _;
use std::{
borrow::Cow,
collections::{HashMap, VecDeque},
collections::{BTreeMap, HashMap, VecDeque},
sync::Arc,
};
use util::maybe;
@ -32,13 +32,6 @@ use workspace::{
ui::{Button, Clickable, ContextMenu, Label, LabelCommon, PopoverMenu, h_flex},
};
// TODO:
// - [x] stop sorting by session ID
// - [x] pick the most recent session by default (logs if available, RPC messages otherwise)
// - [ ] dump the launch/attach request somewhere (logs?)
const MAX_SESSIONS: usize = 10;
struct DapLogView {
editor: Entity<Editor>,
focus_handle: FocusHandle,
@ -49,14 +42,34 @@ struct DapLogView {
_subscriptions: Vec<Subscription>,
}
struct LogStoreEntryIdentifier<'a> {
session_id: SessionId,
project: Cow<'a, WeakEntity<Project>>,
}
impl LogStoreEntryIdentifier<'_> {
fn to_owned(&self) -> LogStoreEntryIdentifier<'static> {
LogStoreEntryIdentifier {
session_id: self.session_id,
project: Cow::Owned(self.project.as_ref().clone()),
}
}
}
struct LogStoreMessage {
id: LogStoreEntryIdentifier<'static>,
kind: IoKind,
command: Option<SharedString>,
message: SharedString,
}
pub struct LogStore {
projects: HashMap<WeakEntity<Project>, ProjectState>,
debug_sessions: VecDeque<DebugAdapterState>,
rpc_tx: UnboundedSender<(SessionId, IoKind, Option<SharedString>, SharedString)>,
adapter_log_tx: UnboundedSender<(SessionId, IoKind, Option<SharedString>, SharedString)>,
rpc_tx: UnboundedSender<LogStoreMessage>,
adapter_log_tx: UnboundedSender<LogStoreMessage>,
}
struct ProjectState {
debug_sessions: BTreeMap<SessionId, DebugAdapterState>,
_subscriptions: [gpui::Subscription; 2],
}
@ -122,13 +135,12 @@ impl DebugAdapterState {
impl LogStore {
pub fn new(cx: &Context<Self>) -> Self {
let (rpc_tx, mut rpc_rx) =
unbounded::<(SessionId, IoKind, Option<SharedString>, SharedString)>();
let (rpc_tx, mut rpc_rx) = unbounded::<LogStoreMessage>();
cx.spawn(async move |this, cx| {
while let Some((session_id, io_kind, command, message)) = rpc_rx.next().await {
while let Some(message) = rpc_rx.next().await {
if let Some(this) = this.upgrade() {
this.update(cx, |this, cx| {
this.add_debug_adapter_message(session_id, io_kind, command, message, cx);
this.add_debug_adapter_message(message, cx);
})?;
}
@ -138,13 +150,12 @@ impl LogStore {
})
.detach_and_log_err(cx);
let (adapter_log_tx, mut adapter_log_rx) =
unbounded::<(SessionId, IoKind, Option<SharedString>, SharedString)>();
let (adapter_log_tx, mut adapter_log_rx) = unbounded::<LogStoreMessage>();
cx.spawn(async move |this, cx| {
while let Some((session_id, io_kind, _, message)) = adapter_log_rx.next().await {
while let Some(message) = adapter_log_rx.next().await {
if let Some(this) = this.upgrade() {
this.update(cx, |this, cx| {
this.add_debug_adapter_log(session_id, io_kind, message, cx);
this.add_debug_adapter_log(message, cx);
})?;
}
@ -157,57 +168,76 @@ impl LogStore {
rpc_tx,
adapter_log_tx,
projects: HashMap::new(),
debug_sessions: Default::default(),
}
}
pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) {
let weak_project = project.downgrade();
self.projects.insert(
project.downgrade(),
ProjectState {
_subscriptions: [
cx.observe_release(project, move |this, _, _| {
this.projects.remove(&weak_project);
cx.observe_release(project, {
let weak_project = project.downgrade();
move |this, _, _| {
this.projects.remove(&weak_project);
}
}),
cx.subscribe(
&project.read(cx).dap_store(),
|this, dap_store, event, cx| match event {
cx.subscribe(&project.read(cx).dap_store(), {
let weak_project = project.downgrade();
move |this, dap_store, event, cx| match event {
dap_store::DapStoreEvent::DebugClientStarted(session_id) => {
let session = dap_store.read(cx).session_by_id(session_id);
if let Some(session) = session {
this.add_debug_session(*session_id, session, cx);
this.add_debug_session(
LogStoreEntryIdentifier {
project: Cow::Owned(weak_project.clone()),
session_id: *session_id,
},
session,
cx,
);
}
}
dap_store::DapStoreEvent::DebugClientShutdown(session_id) => {
this.get_debug_adapter_state(*session_id)
.iter_mut()
.for_each(|state| state.is_terminated = true);
let id = LogStoreEntryIdentifier {
project: Cow::Borrowed(&weak_project),
session_id: *session_id,
};
if let Some(state) = this.get_debug_adapter_state(&id) {
state.is_terminated = true;
}
this.clean_sessions(cx);
}
_ => {}
},
),
}
}),
],
debug_sessions: Default::default(),
},
);
}
fn get_debug_adapter_state(&mut self, id: SessionId) -> Option<&mut DebugAdapterState> {
self.debug_sessions
.iter_mut()
.find(|adapter_state| adapter_state.id == id)
fn get_debug_adapter_state(
&mut self,
id: &LogStoreEntryIdentifier<'_>,
) -> Option<&mut DebugAdapterState> {
self.projects
.get_mut(&id.project)
.and_then(|state| state.debug_sessions.get_mut(&id.session_id))
}
fn add_debug_adapter_message(
&mut self,
id: SessionId,
io_kind: IoKind,
command: Option<SharedString>,
message: SharedString,
LogStoreMessage {
id,
kind: io_kind,
command,
message,
}: LogStoreMessage,
cx: &mut Context<Self>,
) {
let Some(debug_client_state) = self.get_debug_adapter_state(id) else {
let Some(debug_client_state) = self.get_debug_adapter_state(&id) else {
return;
};
@ -229,7 +259,7 @@ impl LogStore {
if rpc_messages.last_message_kind != Some(kind) {
Self::get_debug_adapter_entry(
&mut rpc_messages.messages,
id,
id.to_owned(),
kind.label().into(),
LogKind::Rpc,
cx,
@ -239,7 +269,7 @@ impl LogStore {
let entry = Self::get_debug_adapter_entry(
&mut rpc_messages.messages,
id,
id.to_owned(),
message,
LogKind::Rpc,
cx,
@ -260,12 +290,15 @@ impl LogStore {
fn add_debug_adapter_log(
&mut self,
id: SessionId,
io_kind: IoKind,
message: SharedString,
LogStoreMessage {
id,
kind: io_kind,
message,
..
}: LogStoreMessage,
cx: &mut Context<Self>,
) {
let Some(debug_adapter_state) = self.get_debug_adapter_state(id) else {
let Some(debug_adapter_state) = self.get_debug_adapter_state(&id) else {
return;
};
@ -276,7 +309,7 @@ impl LogStore {
Self::get_debug_adapter_entry(
&mut debug_adapter_state.log_messages,
id,
id.to_owned(),
message,
LogKind::Adapter,
cx,
@ -286,13 +319,17 @@ impl LogStore {
fn get_debug_adapter_entry(
log_lines: &mut VecDeque<SharedString>,
id: SessionId,
id: LogStoreEntryIdentifier<'static>,
message: SharedString,
kind: LogKind,
cx: &mut Context<Self>,
) -> SharedString {
while log_lines.len() >= RpcMessages::MESSAGE_QUEUE_LIMIT {
log_lines.pop_front();
if let Some(excess) = log_lines
.len()
.checked_sub(RpcMessages::MESSAGE_QUEUE_LIMIT)
&& excess > 0
{
log_lines.drain(..excess);
}
let format_messages = DebuggerSettings::get_global(cx).format_dap_log_messages;
@ -322,118 +359,111 @@ impl LogStore {
fn add_debug_session(
&mut self,
session_id: SessionId,
id: LogStoreEntryIdentifier<'static>,
session: Entity<Session>,
cx: &mut Context<Self>,
) {
if self
.debug_sessions
.iter_mut()
.any(|adapter_state| adapter_state.id == session_id)
{
return;
}
maybe!({
let project_entry = self.projects.get_mut(&id.project)?;
let std::collections::btree_map::Entry::Vacant(state) =
project_entry.debug_sessions.entry(id.session_id)
else {
return None;
};
let (adapter_name, has_adapter_logs) = session.read_with(cx, |session, _| {
(
session.adapter(),
session
.adapter_client()
.map(|client| client.has_adapter_logs())
.unwrap_or(false),
)
let (adapter_name, has_adapter_logs) = session.read_with(cx, |session, _| {
(
session.adapter(),
session
.adapter_client()
.map_or(false, |client| client.has_adapter_logs()),
)
});
state.insert(DebugAdapterState::new(
id.session_id,
adapter_name,
has_adapter_logs,
));
self.clean_sessions(cx);
let io_tx = self.rpc_tx.clone();
let client = session.read(cx).adapter_client()?;
let project = id.project.clone();
let session_id = id.session_id;
client.add_log_handler(
move |kind, command, message| {
io_tx
.unbounded_send(LogStoreMessage {
id: LogStoreEntryIdentifier {
session_id,
project: project.clone(),
},
kind,
command: command.map(|command| command.to_owned().into()),
message: message.to_owned().into(),
})
.ok();
},
LogKind::Rpc,
);
let log_io_tx = self.adapter_log_tx.clone();
let project = id.project;
client.add_log_handler(
move |kind, command, message| {
log_io_tx
.unbounded_send(LogStoreMessage {
id: LogStoreEntryIdentifier {
session_id,
project: project.clone(),
},
kind,
command: command.map(|command| command.to_owned().into()),
message: message.to_owned().into(),
})
.ok();
},
LogKind::Adapter,
);
Some(())
});
self.debug_sessions.push_back(DebugAdapterState::new(
session_id,
adapter_name,
has_adapter_logs,
));
self.clean_sessions(cx);
let io_tx = self.rpc_tx.clone();
let Some(client) = session.read(cx).adapter_client() else {
return;
};
client.add_log_handler(
move |io_kind, command, message| {
io_tx
.unbounded_send((
session_id,
io_kind,
command.map(|command| command.to_owned().into()),
message.to_owned().into(),
))
.ok();
},
LogKind::Rpc,
);
let log_io_tx = self.adapter_log_tx.clone();
client.add_log_handler(
move |io_kind, command, message| {
log_io_tx
.unbounded_send((
session_id,
io_kind,
command.map(|command| command.to_owned().into()),
message.to_owned().into(),
))
.ok();
},
LogKind::Adapter,
);
}
fn clean_sessions(&mut self, cx: &mut Context<Self>) {
let mut to_remove = self.debug_sessions.len().saturating_sub(MAX_SESSIONS);
self.debug_sessions.retain(|session| {
if to_remove > 0 && session.is_terminated {
to_remove -= 1;
return false;
}
true
self.projects.values_mut().for_each(|project| {
project
.debug_sessions
.retain(|_, session| !session.is_terminated);
});
cx.notify();
}
fn log_messages_for_session(
&mut self,
session_id: SessionId,
id: &LogStoreEntryIdentifier<'_>,
) -> Option<&mut VecDeque<SharedString>> {
self.debug_sessions
.iter_mut()
.find(|session| session.id == session_id)
self.get_debug_adapter_state(id)
.map(|state| &mut state.log_messages)
}
fn rpc_messages_for_session(
&mut self,
session_id: SessionId,
id: &LogStoreEntryIdentifier<'_>,
) -> Option<&mut VecDeque<SharedString>> {
self.debug_sessions.iter_mut().find_map(|state| {
if state.id == session_id {
Some(&mut state.rpc_messages.messages)
} else {
None
}
})
self.get_debug_adapter_state(id)
.map(|state| &mut state.rpc_messages.messages)
}
fn initialization_sequence_for_session(
&mut self,
session_id: SessionId,
) -> Option<&mut Vec<SharedString>> {
self.debug_sessions.iter_mut().find_map(|state| {
if state.id == session_id {
Some(&mut state.rpc_messages.initialization_sequence)
} else {
None
}
})
id: &LogStoreEntryIdentifier<'_>,
) -> Option<&Vec<SharedString>> {
self.get_debug_adapter_state(&id)
.map(|state| &state.rpc_messages.initialization_sequence)
}
}
@ -453,10 +483,11 @@ impl Render for DapLogToolbarItemView {
return Empty.into_any_element();
};
let (menu_rows, current_session_id) = log_view.update(cx, |log_view, cx| {
let (menu_rows, current_session_id, project) = log_view.update(cx, |log_view, cx| {
(
log_view.menu_items(cx),
log_view.current_view.map(|(session_id, _)| session_id),
log_view.project.downgrade(),
)
});
@ -484,6 +515,7 @@ impl Render for DapLogToolbarItemView {
.menu(move |mut window, cx| {
let log_view = log_view.clone();
let menu_rows = menu_rows.clone();
let project = project.clone();
ContextMenu::build(&mut window, cx, move |mut menu, window, _cx| {
for row in menu_rows.into_iter() {
menu = menu.custom_row(move |_window, _cx| {
@ -509,8 +541,15 @@ impl Render for DapLogToolbarItemView {
.child(Label::new(ADAPTER_LOGS))
.into_any_element()
},
window.handler_for(&log_view, move |view, window, cx| {
view.show_log_messages_for_adapter(row.session_id, window, cx);
window.handler_for(&log_view, {
let project = project.clone();
let id = LogStoreEntryIdentifier {
project: Cow::Owned(project),
session_id: row.session_id,
};
move |view, window, cx| {
view.show_log_messages_for_adapter(&id, window, cx);
}
}),
);
}
@ -524,8 +563,15 @@ impl Render for DapLogToolbarItemView {
.child(Label::new(RPC_MESSAGES))
.into_any_element()
},
window.handler_for(&log_view, move |view, window, cx| {
view.show_rpc_trace_for_server(row.session_id, window, cx);
window.handler_for(&log_view, {
let project = project.clone();
let id = LogStoreEntryIdentifier {
project: Cow::Owned(project),
session_id: row.session_id,
};
move |view, window, cx| {
view.show_rpc_trace_for_server(&id, window, cx);
}
}),
)
.custom_entry(
@ -536,12 +582,17 @@ impl Render for DapLogToolbarItemView {
.child(Label::new(INITIALIZATION_SEQUENCE))
.into_any_element()
},
window.handler_for(&log_view, move |view, window, cx| {
view.show_initialization_sequence_for_server(
row.session_id,
window,
cx,
);
window.handler_for(&log_view, {
let project = project.clone();
let id = LogStoreEntryIdentifier {
project: Cow::Owned(project),
session_id: row.session_id,
};
move |view, window, cx| {
view.show_initialization_sequence_for_server(
&id, window, cx,
);
}
}),
);
}
@ -613,7 +664,9 @@ impl DapLogView {
let events_subscriptions = cx.subscribe(&log_store, |log_view, _, event, cx| match event {
Event::NewLogEntry { id, entry, kind } => {
if log_view.current_view == Some((*id, *kind)) {
if log_view.current_view == Some((id.session_id, *kind))
&& log_view.project == *id.project
{
log_view.editor.update(cx, |editor, cx| {
editor.set_read_only(false);
let last_point = editor.buffer().read(cx).len(cx);
@ -629,12 +682,18 @@ impl DapLogView {
}
}
});
let weak_project = project.downgrade();
let state_info = log_store
.read(cx)
.debug_sessions
.back()
.map(|session| (session.id, session.has_adapter_logs));
.projects
.get(&weak_project)
.and_then(|project| {
project
.debug_sessions
.values()
.next_back()
.map(|session| (session.id, session.has_adapter_logs))
});
let mut this = Self {
editor,
@ -647,10 +706,14 @@ impl DapLogView {
};
if let Some((session_id, have_adapter_logs)) = state_info {
let id = LogStoreEntryIdentifier {
session_id,
project: Cow::Owned(weak_project),
};
if have_adapter_logs {
this.show_log_messages_for_adapter(session_id, window, cx);
this.show_log_messages_for_adapter(&id, window, cx);
} else {
this.show_rpc_trace_for_server(session_id, window, cx);
this.show_rpc_trace_for_server(&id, window, cx);
}
}
@ -690,31 +753,38 @@ impl DapLogView {
fn menu_items(&self, cx: &App) -> Vec<DapMenuItem> {
self.log_store
.read(cx)
.debug_sessions
.iter()
.rev()
.map(|state| DapMenuItem {
session_id: state.id,
adapter_name: state.adapter_name.clone(),
has_adapter_logs: state.has_adapter_logs,
selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind),
.projects
.get(&self.project.downgrade())
.map_or_else(Vec::new, |state| {
state
.debug_sessions
.values()
.rev()
.map(|state| DapMenuItem {
session_id: state.id,
adapter_name: state.adapter_name.clone(),
has_adapter_logs: state.has_adapter_logs,
selected_entry: self
.current_view
.map_or(LogKind::Adapter, |(_, kind)| kind),
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>()
}
fn show_rpc_trace_for_server(
&mut self,
session_id: SessionId,
id: &LogStoreEntryIdentifier<'_>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let rpc_log = self.log_store.update(cx, |log_store, _| {
log_store
.rpc_messages_for_session(session_id)
.rpc_messages_for_session(id)
.map(|state| log_contents(state.iter().cloned()))
});
if let Some(rpc_log) = rpc_log {
self.current_view = Some((session_id, LogKind::Rpc));
self.current_view = Some((id.session_id, LogKind::Rpc));
let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
let language = self.project.read(cx).languages().language_for_name("JSON");
editor
@ -725,8 +795,7 @@ impl DapLogView {
.expect("log buffer should be a singleton")
.update(cx, |_, cx| {
cx.spawn({
let buffer = cx.entity();
async move |_, cx| {
async move |buffer, cx| {
let language = language.await.ok();
buffer.update(cx, |buffer, cx| {
buffer.set_language(language, cx);
@ -746,17 +815,17 @@ impl DapLogView {
fn show_log_messages_for_adapter(
&mut self,
session_id: SessionId,
id: &LogStoreEntryIdentifier<'_>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let message_log = self.log_store.update(cx, |log_store, _| {
log_store
.log_messages_for_session(session_id)
.log_messages_for_session(id)
.map(|state| log_contents(state.iter().cloned()))
});
if let Some(message_log) = message_log {
self.current_view = Some((session_id, LogKind::Adapter));
self.current_view = Some((id.session_id, LogKind::Adapter));
let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, window, cx);
editor
.read(cx)
@ -775,17 +844,17 @@ impl DapLogView {
fn show_initialization_sequence_for_server(
&mut self,
session_id: SessionId,
id: &LogStoreEntryIdentifier<'_>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let rpc_log = self.log_store.update(cx, |log_store, _| {
log_store
.initialization_sequence_for_session(session_id)
.initialization_sequence_for_session(id)
.map(|state| log_contents(state.iter().cloned()))
});
if let Some(rpc_log) = rpc_log {
self.current_view = Some((session_id, LogKind::Rpc));
self.current_view = Some((id.session_id, LogKind::Rpc));
let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
let language = self.project.read(cx).languages().language_for_name("JSON");
editor
@ -993,9 +1062,9 @@ impl Focusable for DapLogView {
}
}
pub enum Event {
enum Event {
NewLogEntry {
id: SessionId,
id: LogStoreEntryIdentifier<'static>,
entry: SharedString,
kind: LogKind,
},
@ -1008,31 +1077,30 @@ impl EventEmitter<SearchEvent> for DapLogView {}
#[cfg(any(test, feature = "test-support"))]
impl LogStore {
pub fn contained_session_ids(&self) -> Vec<SessionId> {
self.debug_sessions
.iter()
.map(|session| session.id)
.collect()
pub fn has_projects(&self) -> bool {
!self.projects.is_empty()
}
pub fn rpc_messages_for_session_id(&self, session_id: SessionId) -> Vec<SharedString> {
self.debug_sessions
.iter()
.find(|adapter_state| adapter_state.id == session_id)
.expect("This session should exist if a test is calling")
.rpc_messages
.messages
.clone()
.into()
pub fn contained_session_ids(&self, project: &WeakEntity<Project>) -> Vec<SessionId> {
self.projects.get(project).map_or(vec![], |state| {
state.debug_sessions.keys().copied().collect()
})
}
pub fn log_messages_for_session_id(&self, session_id: SessionId) -> Vec<SharedString> {
self.debug_sessions
.iter()
.find(|adapter_state| adapter_state.id == session_id)
.expect("This session should exist if a test is calling")
.log_messages
.clone()
.into()
pub fn rpc_messages_for_session_id(
&self,
project: &WeakEntity<Project>,
session_id: SessionId,
) -> Vec<SharedString> {
self.projects.get(&project).map_or(vec![], |state| {
state
.debug_sessions
.get(&session_id)
.expect("This session should exist if a test is calling")
.rpc_messages
.messages
.clone()
.into()
})
}
}

View file

@ -28,6 +28,7 @@ test-support = [
[dependencies]
alacritty_terminal.workspace = true
anyhow.workspace = true
bitflags.workspace = true
client.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true

View file

@ -100,7 +100,13 @@ impl DebugPanel {
sessions: vec![],
active_session: None,
focus_handle,
breakpoint_list: BreakpointList::new(None, workspace.weak_handle(), &project, cx),
breakpoint_list: BreakpointList::new(
None,
workspace.weak_handle(),
&project,
window,
cx,
),
project,
workspace: workspace.weak_handle(),
context_menu: None,
@ -862,7 +868,7 @@ impl DebugPanel {
let threads =
running_state.update(cx, |running_state, cx| {
let session = running_state.session();
session.read(cx).is_running().then(|| {
session.read(cx).is_started().then(|| {
session.update(cx, |session, cx| {
session.threads(cx)
})
@ -1292,6 +1298,11 @@ impl Render for DebugPanel {
}
v_flex()
.when_else(
self.position(window, cx) == DockPosition::Bottom,
|this| this.max_h(self.size),
|this| this.max_w(self.size),
)
.size_full()
.key_context("DebugPanel")
.child(h_flex().children(self.top_controls_strip(window, cx)))
@ -1462,6 +1473,94 @@ impl Render for DebugPanel {
if has_sessions {
this.children(self.active_session.clone())
} else {
let docked_to_bottom = self.position(window, cx) == DockPosition::Bottom;
let welcome_experience = v_flex()
.when_else(
docked_to_bottom,
|this| this.w_2_3().h_full().pr_8(),
|this| this.w_full().h_1_3(),
)
.items_center()
.justify_center()
.gap_2()
.child(
Button::new("spawn-new-session-empty-state", "New Session")
.icon(IconName::Plus)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(|_, window, cx| {
window.dispatch_action(crate::Start.boxed_clone(), cx);
}),
)
.child(
Button::new("edit-debug-settings", "Edit debug.json")
.icon(IconName::Code)
.icon_size(IconSize::XSmall)
.color(Color::Muted)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(|_, window, cx| {
window.dispatch_action(
zed_actions::OpenProjectDebugTasks.boxed_clone(),
cx,
);
}),
)
.child(
Button::new("open-debugger-docs", "Debugger Docs")
.icon(IconName::Book)
.color(Color::Muted)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(|_, _, cx| cx.open_url("https://zed.dev/docs/debugger")),
)
.child(
Button::new(
"spawn-new-session-install-extensions",
"Debugger Extensions",
)
.icon(IconName::Blocks)
.color(Color::Muted)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(|_, window, cx| {
window.dispatch_action(
zed_actions::Extensions {
category_filter: Some(
zed_actions::ExtensionCategoryFilter::DebugAdapters,
),
}
.boxed_clone(),
cx,
);
}),
);
let breakpoint_list =
v_flex()
.group("base-breakpoint-list")
.items_start()
.when_else(
docked_to_bottom,
|this| this.min_w_1_3().h_full(),
|this| this.w_full().h_2_3(),
)
.p_1()
.child(
h_flex()
.pl_1()
.w_full()
.justify_between()
.child(Label::new("Breakpoints").size(LabelSize::Small))
.child(h_flex().visible_on_hover("base-breakpoint-list").child(
self.breakpoint_list.read(cx).render_control_strip(),
))
.track_focus(&self.breakpoint_list.focus_handle(cx)),
)
.child(Divider::horizontal())
.child(self.breakpoint_list.clone());
this.child(
v_flex()
.h_full()
@ -1469,65 +1568,23 @@ impl Render for DebugPanel {
.items_center()
.justify_center()
.child(
h_flex().size_full()
.items_start()
.child(v_flex().group("base-breakpoint-list").items_start().min_w_1_3().h_full().p_1()
.child(h_flex().pl_1().w_full().justify_between()
.child(Label::new("Breakpoints").size(LabelSize::Small))
.child(h_flex().visible_on_hover("base-breakpoint-list").child(self.breakpoint_list.read(cx).render_control_strip())))
.child(Divider::horizontal())
.child(self.breakpoint_list.clone()))
.child(Divider::vertical())
.child(
v_flex().w_2_3().h_full().items_center().justify_center()
.gap_2()
.pr_8()
.child(
Button::new("spawn-new-session-empty-state", "New Session")
.icon(IconName::Plus)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(|_, window, cx| {
window.dispatch_action(crate::Start.boxed_clone(), cx);
})
)
.child(
Button::new("edit-debug-settings", "Edit debug.json")
.icon(IconName::Code)
.icon_size(IconSize::XSmall)
.color(Color::Muted)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(|_, window, cx| {
window.dispatch_action(zed_actions::OpenProjectDebugTasks.boxed_clone(), cx);
})
)
.child(
Button::new("open-debugger-docs", "Debugger Docs")
.icon(IconName::Book)
.color(Color::Muted)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(|_, _, cx| {
cx.open_url("https://zed.dev/docs/debugger")
})
)
.child(
Button::new("spawn-new-session-install-extensions", "Debugger Extensions")
.icon(IconName::Blocks)
.color(Color::Muted)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.icon_position(IconPosition::Start)
.on_click(|_, window, cx| {
window.dispatch_action(zed_actions::Extensions { category_filter: Some(zed_actions::ExtensionCategoryFilter::DebugAdapters)}.boxed_clone(), cx);
})
)
)
)
div()
.when_else(docked_to_bottom, Div::h_flex, Div::v_flex)
.size_full()
.map(|this| {
if docked_to_bottom {
this.items_start()
.child(breakpoint_list)
.child(Divider::vertical())
.child(welcome_experience)
} else {
this.items_end()
.child(welcome_experience)
.child(Divider::horizontal())
.child(breakpoint_list)
}
}),
),
)
}
})

View file

@ -697,8 +697,13 @@ impl RunningState {
)
});
let breakpoint_list =
BreakpointList::new(Some(session.clone()), workspace.clone(), &project, cx);
let breakpoint_list = BreakpointList::new(
Some(session.clone()),
workspace.clone(),
&project,
window,
cx,
);
let _subscriptions = vec![
cx.on_app_quit(move |this, cx| {

View file

@ -5,11 +5,11 @@ use std::{
time::Duration,
};
use dap::ExceptionBreakpointsFilter;
use dap::{Capabilities, ExceptionBreakpointsFilter};
use editor::Editor;
use gpui::{
Action, AppContext, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, Stateful,
Task, UniformListScrollHandle, WeakEntity, uniform_list,
Action, AppContext, ClickEvent, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy,
Stateful, Task, UniformListScrollHandle, WeakEntity, actions, uniform_list,
};
use language::Point;
use project::{
@ -21,16 +21,20 @@ use project::{
worktree_store::WorktreeStore,
};
use ui::{
AnyElement, App, ButtonCommon, Clickable, Color, Context, Disableable, Div, FluentBuilder as _,
Icon, IconButton, IconName, IconSize, Indicator, InteractiveElement, IntoElement, Label,
LabelCommon, LabelSize, ListItem, ParentElement, Render, Scrollbar, ScrollbarState,
SharedString, StatefulInteractiveElement, Styled, Toggleable, Tooltip, Window, div, h_flex, px,
v_flex,
ActiveTheme, AnyElement, App, ButtonCommon, Clickable, Color, Context, Disableable, Div,
Divider, FluentBuilder as _, Icon, IconButton, IconName, IconSize, Indicator,
InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement,
Render, RenderOnce, Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement,
Styled, Toggleable, Tooltip, Window, div, h_flex, px, v_flex,
};
use util::ResultExt;
use workspace::Workspace;
use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint};
actions!(
debugger,
[PreviousBreakpointProperty, NextBreakpointProperty]
);
#[derive(Clone, Copy, PartialEq)]
pub(crate) enum SelectedBreakpointKind {
Source,
@ -48,6 +52,8 @@ pub(crate) struct BreakpointList {
focus_handle: FocusHandle,
scroll_handle: UniformListScrollHandle,
selected_ix: Option<usize>,
input: Entity<Editor>,
strip_mode: Option<ActiveBreakpointStripMode>,
}
impl Focusable for BreakpointList {
@ -56,11 +62,19 @@ impl Focusable for BreakpointList {
}
}
#[derive(Clone, Copy, PartialEq)]
enum ActiveBreakpointStripMode {
Log,
Condition,
HitCondition,
}
impl BreakpointList {
pub(crate) fn new(
session: Option<Entity<Session>>,
workspace: WeakEntity<Workspace>,
project: &Entity<Project>,
window: &mut Window,
cx: &mut App,
) -> Entity<Self> {
let project = project.read(cx);
@ -70,7 +84,7 @@ impl BreakpointList {
let scroll_handle = UniformListScrollHandle::new();
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
cx.new(|_| Self {
cx.new(|cx| Self {
breakpoint_store,
worktree_store,
scrollbar_state,
@ -82,17 +96,28 @@ impl BreakpointList {
focus_handle,
scroll_handle,
selected_ix: None,
input: cx.new(|cx| Editor::single_line(window, cx)),
strip_mode: None,
})
}
fn edit_line_breakpoint(
&mut self,
&self,
path: Arc<Path>,
row: u32,
action: BreakpointEditAction,
cx: &mut Context<Self>,
cx: &mut App,
) {
self.breakpoint_store.update(cx, |breakpoint_store, cx| {
Self::edit_line_breakpoint_inner(&self.breakpoint_store, path, row, action, cx);
}
fn edit_line_breakpoint_inner(
breakpoint_store: &Entity<BreakpointStore>,
path: Arc<Path>,
row: u32,
action: BreakpointEditAction,
cx: &mut App,
) {
breakpoint_store.update(cx, |breakpoint_store, cx| {
if let Some((buffer, breakpoint)) = breakpoint_store.breakpoint_at_row(&path, row, cx) {
breakpoint_store.toggle_breakpoint(buffer, breakpoint, action, cx);
} else {
@ -148,16 +173,63 @@ impl BreakpointList {
})
}
fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
fn set_active_breakpoint_property(
&mut self,
prop: ActiveBreakpointStripMode,
window: &mut Window,
cx: &mut App,
) {
self.strip_mode = Some(prop);
let placeholder = match prop {
ActiveBreakpointStripMode::Log => "Set Log Message",
ActiveBreakpointStripMode::Condition => "Set Condition",
ActiveBreakpointStripMode::HitCondition => "Set Hit Condition",
};
let mut is_exception_breakpoint = true;
let active_value = self.selected_ix.and_then(|ix| {
self.breakpoints.get(ix).and_then(|bp| {
if let BreakpointEntryKind::LineBreakpoint(bp) = &bp.kind {
is_exception_breakpoint = false;
match prop {
ActiveBreakpointStripMode::Log => bp.breakpoint.message.clone(),
ActiveBreakpointStripMode::Condition => bp.breakpoint.condition.clone(),
ActiveBreakpointStripMode::HitCondition => {
bp.breakpoint.hit_condition.clone()
}
}
} else {
None
}
})
});
self.input.update(cx, |this, cx| {
this.set_placeholder_text(placeholder, cx);
this.set_read_only(is_exception_breakpoint);
this.set_text(active_value.as_deref().unwrap_or(""), window, cx);
});
}
fn select_ix(&mut self, ix: Option<usize>, window: &mut Window, cx: &mut Context<Self>) {
self.selected_ix = ix;
if let Some(ix) = ix {
self.scroll_handle
.scroll_to_item(ix, ScrollStrategy::Center);
}
if let Some(mode) = self.strip_mode {
self.set_active_breakpoint_property(mode, window, cx);
}
cx.notify();
}
fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
if self.strip_mode.is_some() {
if self.input.focus_handle(cx).contains_focused(window, cx) {
cx.propagate();
return;
}
}
let ix = match self.selected_ix {
_ if self.breakpoints.len() == 0 => None,
None => Some(0),
@ -169,15 +241,21 @@ impl BreakpointList {
}
}
};
self.select_ix(ix, cx);
self.select_ix(ix, window, cx);
}
fn select_previous(
&mut self,
_: &menu::SelectPrevious,
_window: &mut Window,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self.strip_mode.is_some() {
if self.input.focus_handle(cx).contains_focused(window, cx) {
cx.propagate();
return;
}
}
let ix = match self.selected_ix {
_ if self.breakpoints.len() == 0 => None,
None => Some(self.breakpoints.len() - 1),
@ -189,37 +267,105 @@ impl BreakpointList {
}
}
};
self.select_ix(ix, cx);
self.select_ix(ix, window, cx);
}
fn select_first(
&mut self,
_: &menu::SelectFirst,
_window: &mut Window,
cx: &mut Context<Self>,
) {
fn select_first(&mut self, _: &menu::SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
if self.strip_mode.is_some() {
if self.input.focus_handle(cx).contains_focused(window, cx) {
cx.propagate();
return;
}
}
let ix = if self.breakpoints.len() > 0 {
Some(0)
} else {
None
};
self.select_ix(ix, cx);
self.select_ix(ix, window, cx);
}
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
fn select_last(&mut self, _: &menu::SelectLast, window: &mut Window, cx: &mut Context<Self>) {
if self.strip_mode.is_some() {
if self.input.focus_handle(cx).contains_focused(window, cx) {
cx.propagate();
return;
}
}
let ix = if self.breakpoints.len() > 0 {
Some(self.breakpoints.len() - 1)
} else {
None
};
self.select_ix(ix, cx);
self.select_ix(ix, window, cx);
}
fn dismiss(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
if self.input.focus_handle(cx).contains_focused(window, cx) {
self.focus_handle.focus(window);
} else if self.strip_mode.is_some() {
self.strip_mode.take();
cx.notify();
} else {
cx.propagate();
}
}
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
return;
};
if let Some(mode) = self.strip_mode {
let handle = self.input.focus_handle(cx);
if handle.is_focused(window) {
// Go back to the main strip. Save the result as well.
let text = self.input.read(cx).text(cx);
match mode {
ActiveBreakpointStripMode::Log => match &entry.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
Self::edit_line_breakpoint_inner(
&self.breakpoint_store,
line_breakpoint.breakpoint.path.clone(),
line_breakpoint.breakpoint.row,
BreakpointEditAction::EditLogMessage(Arc::from(text)),
cx,
);
}
_ => {}
},
ActiveBreakpointStripMode::Condition => match &entry.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
Self::edit_line_breakpoint_inner(
&self.breakpoint_store,
line_breakpoint.breakpoint.path.clone(),
line_breakpoint.breakpoint.row,
BreakpointEditAction::EditCondition(Arc::from(text)),
cx,
);
}
_ => {}
},
ActiveBreakpointStripMode::HitCondition => match &entry.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
Self::edit_line_breakpoint_inner(
&self.breakpoint_store,
line_breakpoint.breakpoint.path.clone(),
line_breakpoint.breakpoint.row,
BreakpointEditAction::EditHitCondition(Arc::from(text)),
cx,
);
}
_ => {}
},
}
self.focus_handle.focus(window);
} else {
handle.focus(window);
}
return;
}
match &mut entry.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
let path = line_breakpoint.breakpoint.path.clone();
@ -233,12 +379,18 @@ impl BreakpointList {
fn toggle_enable_breakpoint(
&mut self,
_: &ToggleEnableBreakpoint,
_window: &mut Window,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
return;
};
if self.strip_mode.is_some() {
if self.input.focus_handle(cx).contains_focused(window, cx) {
cx.propagate();
return;
}
}
match &mut entry.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
@ -279,6 +431,50 @@ impl BreakpointList {
cx.notify();
}
fn previous_breakpoint_property(
&mut self,
_: &PreviousBreakpointProperty,
window: &mut Window,
cx: &mut Context<Self>,
) {
let next_mode = match self.strip_mode {
Some(ActiveBreakpointStripMode::Log) => None,
Some(ActiveBreakpointStripMode::Condition) => Some(ActiveBreakpointStripMode::Log),
Some(ActiveBreakpointStripMode::HitCondition) => {
Some(ActiveBreakpointStripMode::Condition)
}
None => Some(ActiveBreakpointStripMode::HitCondition),
};
if let Some(mode) = next_mode {
self.set_active_breakpoint_property(mode, window, cx);
} else {
self.strip_mode.take();
}
cx.notify();
}
fn next_breakpoint_property(
&mut self,
_: &NextBreakpointProperty,
window: &mut Window,
cx: &mut Context<Self>,
) {
let next_mode = match self.strip_mode {
Some(ActiveBreakpointStripMode::Log) => Some(ActiveBreakpointStripMode::Condition),
Some(ActiveBreakpointStripMode::Condition) => {
Some(ActiveBreakpointStripMode::HitCondition)
}
Some(ActiveBreakpointStripMode::HitCondition) => None,
None => Some(ActiveBreakpointStripMode::Log),
};
if let Some(mode) = next_mode {
self.set_active_breakpoint_property(mode, window, cx);
} else {
self.strip_mode.take();
}
cx.notify();
}
fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
@ -294,20 +490,31 @@ impl BreakpointList {
}))
}
fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
fn render_list(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let selected_ix = self.selected_ix;
let focus_handle = self.focus_handle.clone();
let supported_breakpoint_properties = self
.session
.as_ref()
.map(|session| SupportedBreakpointProperties::from(session.read(cx).capabilities()))
.unwrap_or_else(SupportedBreakpointProperties::empty);
let strip_mode = self.strip_mode;
uniform_list(
"breakpoint-list",
self.breakpoints.len(),
cx.processor(move |this, range: Range<usize>, window, cx| {
cx.processor(move |this, range: Range<usize>, _, _| {
range
.clone()
.zip(&mut this.breakpoints[range])
.map(|(ix, breakpoint)| {
breakpoint
.render(ix, focus_handle.clone(), window, cx)
.toggle_state(Some(ix) == selected_ix)
.render(
strip_mode,
supported_breakpoint_properties,
ix,
Some(ix) == selected_ix,
focus_handle.clone(),
)
.into_any_element()
})
.collect()
@ -443,7 +650,6 @@ impl BreakpointList {
impl Render for BreakpointList {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
// let old_len = self.breakpoints.len();
let breakpoints = self.breakpoint_store.read(cx).all_source_breakpoints(cx);
self.breakpoints.clear();
let weak = cx.weak_entity();
@ -523,15 +729,46 @@ impl Render for BreakpointList {
.on_action(cx.listener(Self::select_previous))
.on_action(cx.listener(Self::select_first))
.on_action(cx.listener(Self::select_last))
.on_action(cx.listener(Self::dismiss))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::toggle_enable_breakpoint))
.on_action(cx.listener(Self::unset_breakpoint))
.on_action(cx.listener(Self::next_breakpoint_property))
.on_action(cx.listener(Self::previous_breakpoint_property))
.size_full()
.m_0p5()
.child(self.render_list(window, cx))
.children(self.render_vertical_scrollbar(cx))
.child(
v_flex()
.size_full()
.child(self.render_list(cx))
.children(self.render_vertical_scrollbar(cx)),
)
.when_some(self.strip_mode, |this, _| {
this.child(Divider::horizontal()).child(
h_flex()
// .w_full()
.m_0p5()
.p_0p5()
.border_1()
.rounded_sm()
.when(
self.input.focus_handle(cx).contains_focused(window, cx),
|this| {
let colors = cx.theme().colors();
let border = if self.input.read(cx).read_only(cx) {
colors.border_disabled
} else {
colors.border_focused
};
this.border_color(border)
},
)
.child(self.input.clone()),
)
})
}
}
#[derive(Clone, Debug)]
struct LineBreakpoint {
name: SharedString,
@ -543,7 +780,10 @@ struct LineBreakpoint {
impl LineBreakpoint {
fn render(
&mut self,
props: SupportedBreakpointProperties,
strip_mode: Option<ActiveBreakpointStripMode>,
ix: usize,
is_selected: bool,
focus_handle: FocusHandle,
weak: WeakEntity<BreakpointList>,
) -> ListItem {
@ -594,15 +834,16 @@ impl LineBreakpoint {
})
.child(Indicator::icon(Icon::new(icon_name)).color(Color::Debugger))
.on_mouse_down(MouseButton::Left, move |_, _, _| {});
ListItem::new(SharedString::from(format!(
"breakpoint-ui-item-{:?}/{}:{}",
self.dir, self.name, self.line
)))
.on_click({
let weak = weak.clone();
move |_, _, cx| {
move |_, window, cx| {
weak.update(cx, |breakpoint_list, cx| {
breakpoint_list.select_ix(Some(ix), cx);
breakpoint_list.select_ix(Some(ix), window, cx);
})
.ok();
}
@ -613,39 +854,67 @@ impl LineBreakpoint {
cx.stop_propagation();
})
.child(
v_flex()
.py_1()
h_flex()
.w_full()
.mr_4()
.py_0p5()
.gap_1()
.min_h(px(26.))
.justify_center()
.justify_between()
.id(SharedString::from(format!(
"breakpoint-ui-on-click-go-to-line-{:?}/{}:{}",
self.dir, self.name, self.line
)))
.on_click(move |_, window, cx| {
weak.update(cx, |breakpoint_list, cx| {
breakpoint_list.select_ix(Some(ix), cx);
breakpoint_list.go_to_line_breakpoint(path.clone(), row, window, cx);
})
.ok();
.on_click({
let weak = weak.clone();
move |_, window, cx| {
weak.update(cx, |breakpoint_list, cx| {
breakpoint_list.select_ix(Some(ix), window, cx);
breakpoint_list.go_to_line_breakpoint(path.clone(), row, window, cx);
})
.ok();
}
})
.cursor_pointer()
.child(
h_flex()
.gap_1()
.gap_0p5()
.child(
Label::new(format!("{}:{}", self.name, self.line))
.size(LabelSize::Small)
.line_height_style(ui::LineHeightStyle::UiLabel),
)
.children(self.dir.clone().map(|dir| {
Label::new(dir)
.color(Color::Muted)
.size(LabelSize::Small)
.line_height_style(ui::LineHeightStyle::UiLabel)
.children(self.dir.as_ref().and_then(|dir| {
let path_without_root = Path::new(dir.as_ref())
.components()
.skip(1)
.collect::<PathBuf>();
path_without_root.components().next()?;
Some(
Label::new(path_without_root.to_string_lossy().into_owned())
.color(Color::Muted)
.size(LabelSize::Small)
.line_height_style(ui::LineHeightStyle::UiLabel)
.truncate(),
)
})),
),
)
.when_some(self.dir.as_ref(), |this, parent_dir| {
this.tooltip(Tooltip::text(format!("Worktree parent path: {parent_dir}")))
})
.child(BreakpointOptionsStrip {
props,
breakpoint: BreakpointEntry {
kind: BreakpointEntryKind::LineBreakpoint(self.clone()),
weak: weak,
},
is_selected,
focus_handle,
strip_mode,
index: ix,
}),
)
.toggle_state(is_selected)
}
}
#[derive(Clone, Debug)]
@ -658,7 +927,10 @@ struct ExceptionBreakpoint {
impl ExceptionBreakpoint {
fn render(
&mut self,
props: SupportedBreakpointProperties,
strip_mode: Option<ActiveBreakpointStripMode>,
ix: usize,
is_selected: bool,
focus_handle: FocusHandle,
list: WeakEntity<BreakpointList>,
) -> ListItem {
@ -669,15 +941,15 @@ impl ExceptionBreakpoint {
};
let id = SharedString::from(&self.id);
let is_enabled = self.is_enabled;
let weak = list.clone();
ListItem::new(SharedString::from(format!(
"exception-breakpoint-ui-item-{}",
self.id
)))
.on_click({
let list = list.clone();
move |_, _, cx| {
list.update(cx, |list, cx| list.select_ix(Some(ix), cx))
move |_, window, cx| {
list.update(cx, |list, cx| list.select_ix(Some(ix), window, cx))
.ok();
}
})
@ -691,18 +963,21 @@ impl ExceptionBreakpoint {
"exception-breakpoint-ui-item-{}-click-handler",
self.id
)))
.tooltip(move |window, cx| {
Tooltip::for_action_in(
if is_enabled {
"Disable Exception Breakpoint"
} else {
"Enable Exception Breakpoint"
},
&ToggleEnableBreakpoint,
&focus_handle,
window,
cx,
)
.tooltip({
let focus_handle = focus_handle.clone();
move |window, cx| {
Tooltip::for_action_in(
if is_enabled {
"Disable Exception Breakpoint"
} else {
"Enable Exception Breakpoint"
},
&ToggleEnableBreakpoint,
&focus_handle,
window,
cx,
)
}
})
.on_click({
let list = list.clone();
@ -722,21 +997,40 @@ impl ExceptionBreakpoint {
.child(Indicator::icon(Icon::new(IconName::Flame)).color(color)),
)
.child(
v_flex()
.py_1()
.gap_1()
.min_h(px(26.))
.justify_center()
.id(("exception-breakpoint-label", ix))
h_flex()
.w_full()
.mr_4()
.py_0p5()
.justify_between()
.child(
Label::new(self.data.label.clone())
.size(LabelSize::Small)
.line_height_style(ui::LineHeightStyle::UiLabel),
v_flex()
.py_1()
.gap_1()
.min_h(px(26.))
.justify_center()
.id(("exception-breakpoint-label", ix))
.child(
Label::new(self.data.label.clone())
.size(LabelSize::Small)
.line_height_style(ui::LineHeightStyle::UiLabel),
)
.when_some(self.data.description.clone(), |el, description| {
el.tooltip(Tooltip::text(description))
}),
)
.when_some(self.data.description.clone(), |el, description| {
el.tooltip(Tooltip::text(description))
.child(BreakpointOptionsStrip {
props,
breakpoint: BreakpointEntry {
kind: BreakpointEntryKind::ExceptionBreakpoint(self.clone()),
weak: weak,
},
is_selected,
focus_handle,
strip_mode,
index: ix,
}),
)
.toggle_state(is_selected)
}
}
#[derive(Clone, Debug)]
@ -754,18 +1048,267 @@ struct BreakpointEntry {
impl BreakpointEntry {
fn render(
&mut self,
strip_mode: Option<ActiveBreakpointStripMode>,
props: SupportedBreakpointProperties,
ix: usize,
is_selected: bool,
focus_handle: FocusHandle,
_: &mut Window,
_: &mut App,
) -> ListItem {
match &mut self.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => line_breakpoint.render(
props,
strip_mode,
ix,
is_selected,
focus_handle,
self.weak.clone(),
),
BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => exception_breakpoint
.render(
props.for_exception_breakpoints(),
strip_mode,
ix,
is_selected,
focus_handle,
self.weak.clone(),
),
}
}
fn id(&self) -> SharedString {
match &self.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => format!(
"source-breakpoint-control-strip-{:?}:{}",
line_breakpoint.breakpoint.path, line_breakpoint.breakpoint.row
)
.into(),
BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => format!(
"exception-breakpoint-control-strip--{}",
exception_breakpoint.id
)
.into(),
}
}
fn has_log(&self) -> bool {
match &self.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
line_breakpoint.render(ix, focus_handle, self.weak.clone())
line_breakpoint.breakpoint.message.is_some()
}
BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
exception_breakpoint.render(ix, focus_handle, self.weak.clone())
_ => false,
}
}
fn has_condition(&self) -> bool {
match &self.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
line_breakpoint.breakpoint.condition.is_some()
}
// We don't support conditions on exception breakpoints
BreakpointEntryKind::ExceptionBreakpoint(_) => false,
}
}
fn has_hit_condition(&self) -> bool {
match &self.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
line_breakpoint.breakpoint.hit_condition.is_some()
}
_ => false,
}
}
}
bitflags::bitflags! {
#[derive(Clone, Copy)]
pub struct SupportedBreakpointProperties: u32 {
const LOG = 1 << 0;
const CONDITION = 1 << 1;
const HIT_CONDITION = 1 << 2;
// Conditions for exceptions can be set only when exception filters are supported.
const EXCEPTION_FILTER_OPTIONS = 1 << 3;
}
}
impl From<&Capabilities> for SupportedBreakpointProperties {
fn from(caps: &Capabilities) -> Self {
let mut this = Self::empty();
for (prop, offset) in [
(caps.supports_log_points, Self::LOG),
(caps.supports_conditional_breakpoints, Self::CONDITION),
(
caps.supports_hit_conditional_breakpoints,
Self::HIT_CONDITION,
),
(
caps.supports_exception_options,
Self::EXCEPTION_FILTER_OPTIONS,
),
] {
if prop.unwrap_or_default() {
this.insert(offset);
}
}
this
}
}
impl SupportedBreakpointProperties {
fn for_exception_breakpoints(self) -> Self {
// TODO: we don't yet support conditions for exception breakpoints at the data layer, hence all props are disabled here.
Self::empty()
}
}
#[derive(IntoElement)]
struct BreakpointOptionsStrip {
props: SupportedBreakpointProperties,
breakpoint: BreakpointEntry,
is_selected: bool,
focus_handle: FocusHandle,
strip_mode: Option<ActiveBreakpointStripMode>,
index: usize,
}
impl BreakpointOptionsStrip {
fn is_toggled(&self, expected_mode: ActiveBreakpointStripMode) -> bool {
self.is_selected && self.strip_mode == Some(expected_mode)
}
fn on_click_callback(
&self,
mode: ActiveBreakpointStripMode,
) -> impl for<'a> Fn(&ClickEvent, &mut Window, &'a mut App) + use<> {
let list = self.breakpoint.weak.clone();
let ix = self.index;
move |_, window, cx| {
list.update(cx, |this, cx| {
if this.strip_mode != Some(mode) {
this.set_active_breakpoint_property(mode, window, cx);
} else if this.selected_ix == Some(ix) {
this.strip_mode.take();
} else {
cx.propagate();
}
})
.ok();
}
}
fn add_border(
&self,
kind: ActiveBreakpointStripMode,
available: bool,
window: &Window,
cx: &App,
) -> impl Fn(Div) -> Div {
move |this: Div| {
// Avoid layout shifts in case there's no colored border
let this = this.border_2().rounded_sm();
if self.is_selected && self.strip_mode == Some(kind) {
let theme = cx.theme().colors();
if self.focus_handle.is_focused(window) {
this.border_color(theme.border_selected)
} else {
this.border_color(theme.border_disabled)
}
} else if !available {
this.border_color(cx.theme().colors().border_disabled)
} else {
this
}
}
}
}
impl RenderOnce for BreakpointOptionsStrip {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let id = self.breakpoint.id();
let supports_logs = self.props.contains(SupportedBreakpointProperties::LOG);
let supports_condition = self
.props
.contains(SupportedBreakpointProperties::CONDITION);
let supports_hit_condition = self
.props
.contains(SupportedBreakpointProperties::HIT_CONDITION);
let has_logs = self.breakpoint.has_log();
let has_condition = self.breakpoint.has_condition();
let has_hit_condition = self.breakpoint.has_hit_condition();
let style_for_toggle = |mode, is_enabled| {
if is_enabled && self.strip_mode == Some(mode) && self.is_selected {
ui::ButtonStyle::Filled
} else {
ui::ButtonStyle::Subtle
}
};
let color_for_toggle = |is_enabled| {
if is_enabled {
ui::Color::Default
} else {
ui::Color::Muted
}
};
h_flex()
.gap_1()
.child(
div().map(self.add_border(ActiveBreakpointStripMode::Log, supports_logs, window, cx))
.child(
IconButton::new(
SharedString::from(format!("{id}-log-toggle")),
IconName::ScrollText,
)
.icon_size(IconSize::XSmall)
.style(style_for_toggle(ActiveBreakpointStripMode::Log, has_logs))
.icon_color(color_for_toggle(has_logs))
.disabled(!supports_logs)
.toggle_state(self.is_toggled(ActiveBreakpointStripMode::Log))
.on_click(self.on_click_callback(ActiveBreakpointStripMode::Log)).tooltip(|window, cx| Tooltip::with_meta("Set Log Message", None, "Set log message to display (instead of stopping) when a breakpoint is hit", window, cx))
)
.when(!has_logs && !self.is_selected, |this| this.invisible()),
)
.child(
div().map(self.add_border(
ActiveBreakpointStripMode::Condition,
supports_condition,
window, cx
))
.child(
IconButton::new(
SharedString::from(format!("{id}-condition-toggle")),
IconName::SplitAlt,
)
.icon_size(IconSize::XSmall)
.style(style_for_toggle(
ActiveBreakpointStripMode::Condition,
has_condition
))
.icon_color(color_for_toggle(has_condition))
.disabled(!supports_condition)
.toggle_state(self.is_toggled(ActiveBreakpointStripMode::Condition))
.on_click(self.on_click_callback(ActiveBreakpointStripMode::Condition))
.tooltip(|window, cx| Tooltip::with_meta("Set Condition", None, "Set condition to evaluate when a breakpoint is hit. Program execution will stop only when the condition is met", window, cx))
)
.when(!has_condition && !self.is_selected, |this| this.invisible()),
)
.child(
div().map(self.add_border(
ActiveBreakpointStripMode::HitCondition,
supports_hit_condition,window, cx
))
.child(
IconButton::new(
SharedString::from(format!("{id}-hit-condition-toggle")),
IconName::ArrowDown10,
)
.icon_size(IconSize::XSmall)
.style(style_for_toggle(
ActiveBreakpointStripMode::HitCondition,
has_hit_condition,
))
.icon_color(color_for_toggle(has_hit_condition))
.disabled(!supports_hit_condition)
.toggle_state(self.is_toggled(ActiveBreakpointStripMode::HitCondition))
.on_click(self.on_click_callback(ActiveBreakpointStripMode::HitCondition)).tooltip(|window, cx| Tooltip::with_meta("Set Hit Condition", None, "Set expression that controls how many hits of the breakpoint are ignored.", window, cx))
)
.when(!has_hit_condition && !self.is_selected, |this| {
this.invisible()
}),
)
}
}

View file

@ -114,7 +114,7 @@ impl Console {
}
fn is_running(&self, cx: &Context<Self>) -> bool {
self.session.read(cx).is_running()
self.session.read(cx).is_started()
}
fn handle_stack_frame_list_events(

View file

@ -4,7 +4,7 @@ use collections::HashMap;
use dap::StackFrameId;
use editor::{
Anchor, Bias, DebugStackFrameLine, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer,
RowHighlightOptions, ToPoint, scroll::Autoscroll,
RowHighlightOptions, SelectionEffects, ToPoint, scroll::Autoscroll,
};
use gpui::{
AnyView, App, AppContext, Entity, EventEmitter, Focusable, IntoElement, Render, SharedString,
@ -99,10 +99,11 @@ impl StackTraceView {
if frame_anchor.excerpt_id
!= editor.selections.newest_anchor().head().excerpt_id
{
let auto_scroll =
Some(Autoscroll::center().for_anchor(frame_anchor));
let effects = SelectionEffects::scroll(
Autoscroll::center().for_anchor(frame_anchor),
);
editor.change_selections(auto_scroll, window, cx, |selections| {
editor.change_selections(effects, window, cx, |selections| {
let selection_id = selections.new_selection_id();
let selection = Selection {

View file

@ -37,15 +37,23 @@ async fn test_dap_logger_captures_all_session_rpc_messages(
.await;
assert!(
log_store.read_with(cx, |log_store, _| log_store
.contained_session_ids()
.is_empty()),
"log_store shouldn't contain any session IDs before any sessions were created"
log_store.read_with(cx, |log_store, _| !log_store.has_projects()),
"log_store shouldn't contain any projects before any projects were created"
);
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
assert!(
log_store.read_with(cx, |log_store, _| log_store.has_projects()),
"log_store shouldn't contain any projects before any projects were created"
);
assert!(
log_store.read_with(cx, |log_store, _| log_store
.contained_session_ids(&project.downgrade())
.is_empty()),
"log_store shouldn't contain any projects before any projects were created"
);
let cx = &mut VisualTestContext::from_window(*workspace, cx);
// Start a debug session
@ -54,20 +62,22 @@ async fn test_dap_logger_captures_all_session_rpc_messages(
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
assert_eq!(
log_store.read_with(cx, |log_store, _| log_store.contained_session_ids().len()),
log_store.read_with(cx, |log_store, _| log_store
.contained_session_ids(&project.downgrade())
.len()),
1,
);
assert!(
log_store.read_with(cx, |log_store, _| log_store
.contained_session_ids()
.contained_session_ids(&project.downgrade())
.contains(&session_id)),
"log_store should contain the session IDs of the started session"
);
assert!(
!log_store.read_with(cx, |log_store, _| log_store
.rpc_messages_for_session_id(session_id)
.rpc_messages_for_session_id(&project.downgrade(), session_id)
.is_empty()),
"We should have the initialization sequence in the log store"
);

View file

@ -267,7 +267,6 @@ async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppConte
"Debugpy",
"PHP",
"JavaScript",
"Ruby",
"Delve",
"GDB",
"fake-adapter",

View file

@ -4,7 +4,6 @@ use editor::{
Anchor, Editor, EditorSnapshot, ToOffset,
display_map::{BlockContext, BlockPlacement, BlockProperties, BlockStyle},
hover_popover::diagnostics_markdown_style,
scroll::Autoscroll,
};
use gpui::{AppContext, Entity, Focusable, WeakEntity};
use language::{BufferId, Diagnostic, DiagnosticEntry};
@ -311,7 +310,7 @@ impl DiagnosticBlock {
let range = range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot);
editor.unfold_ranges(&[range.start..range.end], true, false, cx);
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
editor.change_selections(Default::default(), window, cx, |s| {
s.select_ranges([range.start..range.start]);
});
window.focus(&editor.focus_handle(cx));

View file

@ -12,7 +12,6 @@ use diagnostic_renderer::DiagnosticBlock;
use editor::{
DEFAULT_MULTIBUFFER_CONTEXT, Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,
display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
scroll::Autoscroll,
};
use futures::future::join_all;
use gpui::{
@ -626,7 +625,7 @@ impl ProjectDiagnosticsEditor {
if let Some(anchor_range) = anchor_ranges.first() {
let range_to_select = anchor_range.start..anchor_range.start;
this.editor.update(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
editor.change_selections(Default::default(), window, cx, |s| {
s.select_anchor_ranges([range_to_select]);
})
});

View file

@ -388,7 +388,6 @@ actions!(
RestartLanguageServer,
RevealInFileManager,
ReverseLines,
RevertFile,
ReloadFile,
Rewrap,
RunFlycheck,

View file

@ -37,7 +37,9 @@ pub use block_map::{
use block_map::{BlockRow, BlockSnapshot};
use collections::{HashMap, HashSet};
pub use crease_map::*;
pub use fold_map::{ChunkRenderer, ChunkRendererContext, Fold, FoldId, FoldPlaceholder, FoldPoint};
pub use fold_map::{
ChunkRenderer, ChunkRendererContext, ChunkRendererId, Fold, FoldId, FoldPlaceholder, FoldPoint,
};
use fold_map::{FoldMap, FoldSnapshot};
use gpui::{App, Context, Entity, Font, HighlightStyle, LineLayout, Pixels, UnderlineStyle};
pub use inlay_map::Inlay;
@ -538,7 +540,7 @@ impl DisplayMap {
pub fn update_fold_widths(
&mut self,
widths: impl IntoIterator<Item = (FoldId, Pixels)>,
widths: impl IntoIterator<Item = (ChunkRendererId, Pixels)>,
cx: &mut Context<Self>,
) -> bool {
let snapshot = self.buffer.read(cx).snapshot(cx);
@ -966,10 +968,22 @@ impl DisplaySnapshot {
.and_then(|id| id.style(&editor_style.syntax));
if let Some(chunk_highlight) = chunk.highlight_style {
// For color inlays, blend the color with the editor background
let mut processed_highlight = chunk_highlight;
if chunk.is_inlay {
if let Some(inlay_color) = chunk_highlight.color {
// Only blend if the color has transparency (alpha < 1.0)
if inlay_color.a < 1.0 {
let blended_color = editor_style.background.blend(inlay_color);
processed_highlight.color = Some(blended_color);
}
}
}
if let Some(highlight_style) = highlight_style.as_mut() {
highlight_style.highlight(chunk_highlight);
highlight_style.highlight(processed_highlight);
} else {
highlight_style = Some(chunk_highlight);
highlight_style = Some(processed_highlight);
}
}

View file

@ -1,3 +1,5 @@
use crate::{InlayId, display_map::inlay_map::InlayChunk};
use super::{
Highlights,
inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot},
@ -275,13 +277,16 @@ impl FoldMapWriter<'_> {
pub(crate) fn update_fold_widths(
&mut self,
new_widths: impl IntoIterator<Item = (FoldId, Pixels)>,
new_widths: impl IntoIterator<Item = (ChunkRendererId, Pixels)>,
) -> (FoldSnapshot, Vec<FoldEdit>) {
let mut edits = Vec::new();
let inlay_snapshot = self.0.snapshot.inlay_snapshot.clone();
let buffer = &inlay_snapshot.buffer;
for (id, new_width) in new_widths {
let ChunkRendererId::Fold(id) = id else {
continue;
};
if let Some(metadata) = self.0.snapshot.fold_metadata_by_id.get(&id).cloned() {
if Some(new_width) != metadata.width {
let buffer_start = metadata.range.start.to_offset(buffer);
@ -528,7 +533,7 @@ impl FoldMap {
text: ELLIPSIS,
chars: 1,
renderer: ChunkRenderer {
id: fold.id,
id: ChunkRendererId::Fold(fold.id),
render: Arc::new(move |cx| {
(fold.placeholder.render)(
fold_id,
@ -1070,7 +1075,7 @@ impl sum_tree::Summary for TransformSummary {
}
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, Ord, PartialOrd, Hash)]
pub struct FoldId(usize);
pub struct FoldId(pub(super) usize);
impl From<FoldId> for ElementId {
fn from(val: FoldId) -> Self {
@ -1279,11 +1284,17 @@ pub struct Chunk<'a> {
pub chars: u128,
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ChunkRendererId {
Fold(FoldId),
Inlay(InlayId),
}
/// A recipe for how the chunk should be presented.
#[derive(Clone)]
pub struct ChunkRenderer {
/// The id of the fold associated with this chunk.
pub id: FoldId,
/// The id of the renderer associated with this chunk.
pub id: ChunkRendererId,
/// Creates a custom element to represent this chunk.
pub render: Arc<dyn Send + Sync + Fn(&mut ChunkRendererContext) -> AnyElement>,
/// If true, the element is constrained to the shaped width of the text.
@ -1325,7 +1336,7 @@ impl DerefMut for ChunkRendererContext<'_, '_> {
pub struct FoldChunks<'a> {
transform_cursor: Cursor<'a, Transform, (FoldOffset, InlayOffset)>,
inlay_chunks: InlayChunks<'a>,
inlay_chunk: Option<(InlayOffset, language::Chunk<'a>)>,
inlay_chunk: Option<(InlayOffset, InlayChunk<'a>)>,
inlay_offset: InlayOffset,
output_offset: FoldOffset,
max_output_offset: FoldOffset,
@ -1418,7 +1429,8 @@ impl<'a> Iterator for FoldChunks<'a> {
}
// Otherwise, take a chunk from the buffer's text.
if let Some((buffer_chunk_start, mut chunk)) = self.inlay_chunk.clone() {
if let Some((buffer_chunk_start, mut inlay_chunk)) = self.inlay_chunk.clone() {
let chunk = &mut inlay_chunk.chunk;
let buffer_chunk_end = buffer_chunk_start + InlayOffset(chunk.text.len());
let transform_end = self.transform_cursor.end(&()).1;
let chunk_end = buffer_chunk_end.min(transform_end);
@ -1455,7 +1467,7 @@ impl<'a> Iterator for FoldChunks<'a> {
is_tab: chunk.is_tab,
is_inlay: chunk.is_inlay,
underline: chunk.underline,
renderer: None,
renderer: inlay_chunk.renderer,
});
}

View file

@ -1,4 +1,4 @@
use crate::{HighlightStyles, InlayId};
use crate::{ChunkRenderer, HighlightStyles, InlayId};
use collections::BTreeSet;
use gpui::{Hsla, Rgba};
use language::{Chunk, Edit, Point, TextSummary};
@ -8,11 +8,13 @@ use multi_buffer::{
use std::{
cmp,
ops::{Add, AddAssign, Range, Sub, SubAssign},
sync::Arc,
};
use sum_tree::{Bias, Cursor, SumTree};
use text::{Patch, Rope};
use ui::{ActiveTheme, IntoElement as _, ParentElement as _, Styled as _, div};
use super::{Highlights, custom_highlights::CustomHighlightsChunks};
use super::{Highlights, custom_highlights::CustomHighlightsChunks, fold_map::ChunkRendererId};
/// Decides where the [`Inlay`]s should be displayed.
///
@ -252,6 +254,13 @@ pub struct InlayChunks<'a> {
snapshot: &'a InlaySnapshot,
}
#[derive(Clone)]
pub struct InlayChunk<'a> {
pub chunk: Chunk<'a>,
/// Whether the inlay should be customly rendered.
pub renderer: Option<ChunkRenderer>,
}
impl InlayChunks<'_> {
pub fn seek(&mut self, new_range: Range<InlayOffset>) {
self.transforms.seek(&new_range.start, Bias::Right, &());
@ -271,7 +280,7 @@ impl InlayChunks<'_> {
}
impl<'a> Iterator for InlayChunks<'a> {
type Item = Chunk<'a>;
type Item = InlayChunk<'a>;
fn next(&mut self) -> Option<Self::Item> {
if self.output_offset == self.max_output_offset {
@ -311,11 +320,14 @@ impl<'a> Iterator for InlayChunks<'a> {
chunk.text = suffix;
self.output_offset.0 += prefix.len();
// FIXME: chunk cloning is wrong because the bitmaps might be off
Chunk {
text: prefix,
chars,
tabs,
..chunk.clone()
InlayChunk {
chunk: Chunk {
text: prefix,
chars,
tabs,
..chunk.clone()
},
renderer: None,
}
}
Transform::Inlay(inlay) => {
@ -330,6 +342,7 @@ impl<'a> Iterator for InlayChunks<'a> {
}
}
let mut renderer = None;
let mut highlight_style = match inlay.id {
InlayId::InlineCompletion(_) => {
self.highlight_styles.inline_completion.map(|s| {
@ -342,14 +355,31 @@ impl<'a> Iterator for InlayChunks<'a> {
}
InlayId::Hint(_) => self.highlight_styles.inlay_hint,
InlayId::DebuggerValue(_) => self.highlight_styles.inlay_hint,
InlayId::Color(_) => match inlay.color {
Some(color) => {
let style = self.highlight_styles.inlay_hint.get_or_insert_default();
style.color = Some(color);
Some(*style)
InlayId::Color(_) => {
if let Some(color) = inlay.color {
renderer = Some(ChunkRenderer {
id: ChunkRendererId::Inlay(inlay.id),
render: Arc::new(move |cx| {
div()
.relative()
.size_3p5()
.child(
div()
.absolute()
.right_1()
.size_3()
.border_1()
.border_color(cx.theme().colors().border)
.bg(color),
)
.into_any_element()
}),
constrain_width: false,
measured_width: None,
});
}
None => self.highlight_styles.inlay_hint,
},
self.highlight_styles.inlay_hint
}
};
let next_inlay_highlight_endpoint;
let offset_in_inlay = self.output_offset - self.transforms.start().0;
@ -388,11 +418,14 @@ impl<'a> Iterator for InlayChunks<'a> {
self.output_offset.0 += chunk.len();
// todo! figure out how to get tabs here
Chunk {
text: chunk,
highlight_style,
is_inlay: true,
..Default::default()
InlayChunk {
chunk: Chunk {
text: chunk,
highlight_style,
is_inlay: true,
..Chunk::default()
},
renderer,
}
}
};
@ -1084,7 +1117,7 @@ impl InlaySnapshot {
#[cfg(test)]
pub fn text(&self) -> String {
self.chunks(Default::default()..self.len(), false, Highlights::default())
.map(|chunk| chunk.text)
.map(|chunk| chunk.chunk.text)
.collect()
}
@ -1723,7 +1756,7 @@ mod tests {
..Highlights::default()
},
)
.map(|chunk| chunk.text)
.map(|chunk| chunk.chunk.text)
.collect::<String>();
assert_eq!(
actual_text,

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -12,8 +12,8 @@ use crate::{
ToggleFold,
code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
display_map::{
Block, BlockContext, BlockStyle, DisplaySnapshot, EditorMargins, FoldId, HighlightKey,
HighlightedChunk, ToDisplayPoint,
Block, BlockContext, BlockStyle, ChunkRendererId, DisplaySnapshot, EditorMargins,
HighlightKey, HighlightedChunk, ToDisplayPoint,
},
editor_settings::{
CurrentLineHighlight, DocumentColorsRenderMode, DoubleClickInMultibuffer, Minimap,
@ -7119,7 +7119,7 @@ pub(crate) struct LineWithInvisibles {
enum LineFragment {
Text(ShapedLine),
Element {
id: FoldId,
id: ChunkRendererId,
element: Option<AnyElement>,
size: Size<Pixels>,
len: usize,
@ -8297,7 +8297,7 @@ impl Element for EditorElement {
window,
cx,
);
let new_fold_widths = line_layouts
let new_renrerer_widths = line_layouts
.iter()
.flat_map(|layout| &layout.fragments)
.filter_map(|fragment| {
@ -8308,7 +8308,7 @@ impl Element for EditorElement {
}
});
if self.editor.update(cx, |editor, cx| {
editor.update_fold_widths(new_fold_widths, cx)
editor.update_renderer_widths(new_renrerer_widths, cx)
}) {
// If the fold widths have changed, we need to prepaint
// the element again to account for any changes in
@ -10051,7 +10051,7 @@ fn compute_auto_height_layout(
mod tests {
use super::*;
use crate::{
Editor, MultiBuffer,
Editor, MultiBuffer, SelectionEffects,
display_map::{BlockPlacement, BlockProperties},
editor_tests::{init_test, update_test_language_settings},
};
@ -10176,7 +10176,7 @@ mod tests {
window
.update(cx, |editor, window, cx| {
editor.cursor_shape = CursorShape::Block;
editor.change_selections(None, window, cx, |s| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([
Point::new(0, 0)..Point::new(1, 0),
Point::new(3, 2)..Point::new(3, 3),

View file

@ -1257,7 +1257,7 @@ mod tests {
let snapshot = editor.buffer().read(cx).snapshot(cx);
let anchor_range = snapshot.anchor_before(selection_range.start)
..snapshot.anchor_after(selection_range.end);
editor.change_selections(Some(crate::Autoscroll::fit()), window, cx, |s| {
editor.change_selections(Default::default(), window, cx, |s| {
s.set_pending_anchor_range(anchor_range, crate::SelectMode::Character)
});
});

View file

@ -3,7 +3,7 @@ use crate::{
EditorSnapshot, GlobalDiagnosticRenderer, Hover,
display_map::{InlayOffset, ToDisplayPoint, invisibles::is_invisible},
hover_links::{InlayHighlight, RangeInEditor},
scroll::{Autoscroll, ScrollAmount},
scroll::ScrollAmount,
};
use anyhow::Context as _;
use gpui::{
@ -648,7 +648,7 @@ pub fn hover_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
..Default::default()
},
syntax: cx.theme().syntax().clone(),
selection_background_color: { cx.theme().players().local().selection },
selection_background_color: cx.theme().colors().element_selection_background,
heading: StyleRefinement::default()
.font_weight(FontWeight::BOLD)
.text_base()
@ -697,7 +697,7 @@ pub fn diagnostics_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
..Default::default()
},
syntax: cx.theme().syntax().clone(),
selection_background_color: { cx.theme().players().local().selection },
selection_background_color: cx.theme().colors().element_selection_background,
height_is_multiple_of_line_height: true,
heading: StyleRefinement::default()
.font_weight(FontWeight::BOLD)
@ -746,7 +746,7 @@ pub fn open_markdown_url(link: SharedString, window: &mut Window, cx: &mut App)
};
editor.update_in(cx, |editor, window, cx| {
editor.change_selections(
Some(Autoscroll::fit()),
Default::default(),
window,
cx,
|selections| {

View file

@ -956,7 +956,7 @@ fn fetch_and_update_hints(
.update(cx, |editor, cx| {
if got_throttled {
let query_not_around_visible_range = match editor
.excerpts_for_inlay_hints_query(None, cx)
.visible_excerpts(None, cx)
.remove(&query.excerpt_id)
{
Some((_, _, current_visible_range)) => {
@ -1302,6 +1302,7 @@ fn apply_hint_update(
#[cfg(test)]
pub mod tests {
use crate::SelectionEffects;
use crate::editor_tests::update_test_language_settings;
use crate::scroll::ScrollAmount;
use crate::{ExcerptRange, scroll::Autoscroll, test::editor_lsp_test_context::rust_lang};
@ -1384,7 +1385,9 @@ pub mod tests {
editor
.update(cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([13..13])
});
editor.handle_input("some change", window, cx);
})
.unwrap();
@ -1698,7 +1701,9 @@ pub mod tests {
rs_editor
.update(cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([13..13])
});
editor.handle_input("some rs change", window, cx);
})
.unwrap();
@ -1733,7 +1738,9 @@ pub mod tests {
md_editor
.update(cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([13..13])
});
editor.handle_input("some md change", window, cx);
})
.unwrap();
@ -2155,7 +2162,9 @@ pub mod tests {
] {
editor
.update(cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([13..13])
});
editor.handle_input(change_after_opening, window, cx);
})
.unwrap();
@ -2199,7 +2208,9 @@ pub mod tests {
edits.push(cx.spawn(|mut cx| async move {
task_editor
.update(&mut cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([13..13])
});
editor.handle_input(async_later_change, window, cx);
})
.unwrap();
@ -2447,9 +2458,12 @@ pub mod tests {
editor
.update(cx, |editor, window, cx| {
editor.change_selections(Some(Autoscroll::center()), window, cx, |s| {
s.select_ranges([selection_in_cached_range..selection_in_cached_range])
});
editor.change_selections(
SelectionEffects::scroll(Autoscroll::center()),
window,
cx,
|s| s.select_ranges([selection_in_cached_range..selection_in_cached_range]),
);
})
.unwrap();
cx.executor().advance_clock(Duration::from_millis(
@ -2511,9 +2525,7 @@ pub mod tests {
cx: &mut gpui::TestAppContext,
) -> Range<Point> {
let ranges = editor
.update(cx, |editor, _window, cx| {
editor.excerpts_for_inlay_hints_query(None, cx)
})
.update(cx, |editor, _window, cx| editor.visible_excerpts(None, cx))
.unwrap();
assert_eq!(
ranges.len(),
@ -2712,15 +2724,24 @@ pub mod tests {
editor
.update(cx, |editor, window, cx| {
editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
});
editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
s.select_ranges([Point::new(22, 0)..Point::new(22, 0)])
});
editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
s.select_ranges([Point::new(50, 0)..Point::new(50, 0)])
});
editor.change_selections(
SelectionEffects::scroll(Autoscroll::Next),
window,
cx,
|s| s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]),
);
editor.change_selections(
SelectionEffects::scroll(Autoscroll::Next),
window,
cx,
|s| s.select_ranges([Point::new(22, 0)..Point::new(22, 0)]),
);
editor.change_selections(
SelectionEffects::scroll(Autoscroll::Next),
window,
cx,
|s| s.select_ranges([Point::new(50, 0)..Point::new(50, 0)]),
);
})
.unwrap();
cx.executor().run_until_parked();
@ -2745,9 +2766,12 @@ pub mod tests {
editor
.update(cx, |editor, window, cx| {
editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
s.select_ranges([Point::new(100, 0)..Point::new(100, 0)])
});
editor.change_selections(
SelectionEffects::scroll(Autoscroll::Next),
window,
cx,
|s| s.select_ranges([Point::new(100, 0)..Point::new(100, 0)]),
);
})
.unwrap();
cx.executor().advance_clock(Duration::from_millis(
@ -2778,9 +2802,12 @@ pub mod tests {
editor
.update(cx, |editor, window, cx| {
editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
});
editor.change_selections(
SelectionEffects::scroll(Autoscroll::Next),
window,
cx,
|s| s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]),
);
})
.unwrap();
cx.executor().advance_clock(Duration::from_millis(
@ -2812,7 +2839,7 @@ pub mod tests {
editor_edited.store(true, Ordering::Release);
editor
.update(cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(57, 0)..Point::new(57, 0)])
});
editor.handle_input("++++more text++++", window, cx);
@ -3130,7 +3157,7 @@ pub mod tests {
cx.executor().run_until_parked();
editor
.update(cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
})
})
@ -3412,7 +3439,7 @@ pub mod tests {
cx.executor().run_until_parked();
editor
.update(cx, |editor, window, cx| {
editor.change_selections(None, window, cx, |s| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
})
})

View file

@ -778,7 +778,7 @@ impl Item for Editor {
fn deactivated(&mut self, _: &mut Window, cx: &mut Context<Self>) {
let selection = self.selections.newest_anchor();
self.push_to_nav_history(selection.head(), None, true, cx);
self.push_to_nav_history(selection.head(), None, true, false, cx);
}
fn workspace_deactivated(&mut self, _: &mut Window, cx: &mut Context<Self>) {
@ -1352,7 +1352,7 @@ impl ProjectItem for Editor {
cx,
);
if !restoration_data.selections.is_empty() {
editor.change_selections(None, window, cx, |s| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges(clip_ranges(&restoration_data.selections, &snapshot));
});
}
@ -1521,7 +1521,7 @@ impl SearchableItem for Editor {
fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
let setting = EditorSettings::get_global(cx).seed_search_query_from_cursor;
let snapshot = &self.snapshot(window, cx).buffer_snapshot;
let selection = self.selections.newest::<usize>(cx);
let selection = self.selections.newest_adjusted(cx);
match setting {
SeedQuerySetting::Never => String::new(),
@ -1558,7 +1558,7 @@ impl SearchableItem for Editor {
) {
self.unfold_ranges(&[matches[index].clone()], false, true, cx);
let range = self.range_for_match(&matches[index]);
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
self.change_selections(Default::default(), window, cx, |s| {
s.select_ranges([range]);
})
}
@ -1570,7 +1570,7 @@ impl SearchableItem for Editor {
cx: &mut Context<Self>,
) {
self.unfold_ranges(matches, false, false, cx);
self.change_selections(None, window, cx, |s| {
self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges(matches.iter().cloned())
});
}

View file

@ -843,7 +843,7 @@ mod jsx_tag_autoclose_tests {
let mut cx = EditorTestContext::for_editor(editor, cx).await;
cx.update_editor(|editor, window, cx| {
editor.change_selections(None, window, cx, |selections| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
selections.select(vec![
Selection::from_offset(4),
Selection::from_offset(9),

View file

@ -3,10 +3,10 @@ use std::{cmp, ops::Range};
use collections::HashMap;
use futures::future::join_all;
use gpui::{Hsla, Rgba};
use itertools::Itertools;
use language::point_from_lsp;
use lsp::LanguageServerId;
use multi_buffer::Anchor;
use project::DocumentColor;
use project::{DocumentColor, lsp_store::ColorFetchStrategy};
use settings::Settings as _;
use text::{Bias, BufferId, OffsetRangeExt as _};
use ui::{App, Context, Window};
@ -19,16 +19,21 @@ use crate::{
#[derive(Debug)]
pub(super) struct LspColorData {
buffer_colors: HashMap<BufferId, BufferColors>,
render_mode: DocumentColorsRenderMode,
}
#[derive(Debug, Default)]
struct BufferColors {
colors: Vec<(Range<Anchor>, DocumentColor, InlayId)>,
inlay_colors: HashMap<InlayId, usize>,
render_mode: DocumentColorsRenderMode,
cache_version_used: usize,
}
impl LspColorData {
pub fn new(cx: &App) -> Self {
Self {
colors: Vec::new(),
inlay_colors: HashMap::default(),
buffer_colors: HashMap::default(),
render_mode: EditorSettings::get_global(cx).lsp_document_colors,
}
}
@ -45,8 +50,9 @@ impl LspColorData {
DocumentColorsRenderMode::Inlay => Some(InlaySplice {
to_remove: Vec::new(),
to_insert: self
.colors
.buffer_colors
.iter()
.flat_map(|(_, buffer_colors)| buffer_colors.colors.iter())
.map(|(range, color, id)| {
Inlay::color(
id.id(),
@ -61,33 +67,49 @@ impl LspColorData {
})
.collect(),
}),
DocumentColorsRenderMode::None => {
self.colors.clear();
Some(InlaySplice {
to_remove: self.inlay_colors.drain().map(|(id, _)| id).collect(),
to_insert: Vec::new(),
})
}
DocumentColorsRenderMode::None => Some(InlaySplice {
to_remove: self
.buffer_colors
.drain()
.flat_map(|(_, buffer_colors)| buffer_colors.inlay_colors)
.map(|(id, _)| id)
.collect(),
to_insert: Vec::new(),
}),
DocumentColorsRenderMode::Border | DocumentColorsRenderMode::Background => {
Some(InlaySplice {
to_remove: self.inlay_colors.drain().map(|(id, _)| id).collect(),
to_remove: self
.buffer_colors
.iter_mut()
.flat_map(|(_, buffer_colors)| buffer_colors.inlay_colors.drain())
.map(|(id, _)| id)
.collect(),
to_insert: Vec::new(),
})
}
}
}
fn set_colors(&mut self, colors: Vec<(Range<Anchor>, DocumentColor, InlayId)>) -> bool {
if self.colors == colors {
fn set_colors(
&mut self,
buffer_id: BufferId,
colors: Vec<(Range<Anchor>, DocumentColor, InlayId)>,
cache_version: Option<usize>,
) -> bool {
let buffer_colors = self.buffer_colors.entry(buffer_id).or_default();
if let Some(cache_version) = cache_version {
buffer_colors.cache_version_used = cache_version;
}
if buffer_colors.colors == colors {
return false;
}
self.inlay_colors = colors
buffer_colors.inlay_colors = colors
.iter()
.enumerate()
.map(|(i, (_, _, id))| (*id, i))
.collect();
self.colors = colors;
buffer_colors.colors = colors;
true
}
@ -101,8 +123,9 @@ impl LspColorData {
{
Vec::new()
} else {
self.colors
self.buffer_colors
.iter()
.flat_map(|(_, buffer_colors)| &buffer_colors.colors)
.map(|(range, color, _)| {
let display_range = range.clone().to_display_points(snapshot);
let color = Hsla::from(Rgba {
@ -122,7 +145,7 @@ impl LspColorData {
impl Editor {
pub(super) fn refresh_colors(
&mut self,
for_server_id: Option<LanguageServerId>,
ignore_cache: bool,
buffer_id: Option<BufferId>,
_: &Window,
cx: &mut Context<Self>,
@ -141,29 +164,40 @@ impl Editor {
return;
}
let visible_buffers = self
.visible_excerpts(None, cx)
.into_values()
.map(|(buffer, ..)| buffer)
.filter(|editor_buffer| {
buffer_id.is_none_or(|buffer_id| buffer_id == editor_buffer.read(cx).remote_id())
})
.unique_by(|buffer| buffer.read(cx).remote_id())
.collect::<Vec<_>>();
let all_colors_task = project.read(cx).lsp_store().update(cx, |lsp_store, cx| {
self.buffer()
.update(cx, |multi_buffer, cx| {
multi_buffer
.all_buffers()
.into_iter()
.filter(|editor_buffer| {
buffer_id.is_none_or(|buffer_id| {
buffer_id == editor_buffer.read(cx).remote_id()
})
})
.collect::<Vec<_>>()
})
visible_buffers
.into_iter()
.filter_map(|buffer| {
let buffer_id = buffer.read(cx).remote_id();
let colors_task = lsp_store.document_colors(for_server_id, buffer, cx)?;
let fetch_strategy = if ignore_cache {
ColorFetchStrategy::IgnoreCache
} else {
ColorFetchStrategy::UseCache {
known_cache_version: self.colors.as_ref().and_then(|colors| {
Some(colors.buffer_colors.get(&buffer_id)?.cache_version_used)
}),
}
};
let colors_task = lsp_store.document_colors(fetch_strategy, buffer, cx)?;
Some(async move { (buffer_id, colors_task.await) })
})
.collect::<Vec<_>>()
});
cx.spawn(async move |editor, cx| {
let all_colors = join_all(all_colors_task).await;
if all_colors.is_empty() {
return;
}
let Ok((multi_buffer_snapshot, editor_excerpts)) = editor.update(cx, |editor, cx| {
let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
let editor_excerpts = multi_buffer_snapshot.excerpts().fold(
@ -187,14 +221,14 @@ impl Editor {
return;
};
let mut new_editor_colors = Vec::<(Range<Anchor>, DocumentColor)>::new();
let mut new_editor_colors = HashMap::default();
for (buffer_id, colors) in all_colors {
let Some(excerpts) = editor_excerpts.get(&buffer_id) else {
continue;
};
match colors {
Ok(colors) => {
for color in colors {
for color in colors.colors {
let color_start = point_from_lsp(color.lsp_range.start);
let color_end = point_from_lsp(color.lsp_range.end);
@ -227,8 +261,15 @@ impl Editor {
continue;
};
let new_entry =
new_editor_colors.entry(buffer_id).or_insert_with(|| {
(Vec::<(Range<Anchor>, DocumentColor)>::new(), None)
});
new_entry.1 = colors.cache_version;
let new_buffer_colors = &mut new_entry.0;
let (Ok(i) | Err(i)) =
new_editor_colors.binary_search_by(|(probe, _)| {
new_buffer_colors.binary_search_by(|(probe, _)| {
probe
.start
.cmp(&color_start_anchor, &multi_buffer_snapshot)
@ -238,7 +279,7 @@ impl Editor {
.cmp(&color_end_anchor, &multi_buffer_snapshot)
})
});
new_editor_colors
new_buffer_colors
.insert(i, (color_start_anchor..color_end_anchor, color));
break;
}
@ -251,45 +292,70 @@ impl Editor {
editor
.update(cx, |editor, cx| {
let mut colors_splice = InlaySplice::default();
let mut new_color_inlays = Vec::with_capacity(new_editor_colors.len());
let Some(colors) = &mut editor.colors else {
return;
};
let mut existing_colors = colors.colors.iter().peekable();
for (new_range, new_color) in new_editor_colors {
let rgba_color = Rgba {
r: new_color.color.red,
g: new_color.color.green,
b: new_color.color.blue,
a: new_color.color.alpha,
};
let mut updated = false;
for (buffer_id, (new_buffer_colors, new_cache_version)) in new_editor_colors {
let mut new_buffer_color_inlays =
Vec::with_capacity(new_buffer_colors.len());
let mut existing_buffer_colors = colors
.buffer_colors
.entry(buffer_id)
.or_default()
.colors
.iter()
.peekable();
for (new_range, new_color) in new_buffer_colors {
let rgba_color = Rgba {
r: new_color.color.red,
g: new_color.color.green,
b: new_color.color.blue,
a: new_color.color.alpha,
};
loop {
match existing_colors.peek() {
Some((existing_range, existing_color, existing_inlay_id)) => {
match existing_range
.start
.cmp(&new_range.start, &multi_buffer_snapshot)
.then_with(|| {
existing_range
.end
.cmp(&new_range.end, &multi_buffer_snapshot)
}) {
cmp::Ordering::Less => {
colors_splice.to_remove.push(*existing_inlay_id);
existing_colors.next();
continue;
}
cmp::Ordering::Equal => {
if existing_color == &new_color {
new_color_inlays.push((
new_range,
new_color,
*existing_inlay_id,
));
} else {
loop {
match existing_buffer_colors.peek() {
Some((existing_range, existing_color, existing_inlay_id)) => {
match existing_range
.start
.cmp(&new_range.start, &multi_buffer_snapshot)
.then_with(|| {
existing_range
.end
.cmp(&new_range.end, &multi_buffer_snapshot)
}) {
cmp::Ordering::Less => {
colors_splice.to_remove.push(*existing_inlay_id);
existing_buffer_colors.next();
continue;
}
cmp::Ordering::Equal => {
if existing_color == &new_color {
new_buffer_color_inlays.push((
new_range,
new_color,
*existing_inlay_id,
));
} else {
colors_splice
.to_remove
.push(*existing_inlay_id);
let inlay = Inlay::color(
post_inc(&mut editor.next_color_inlay_id),
new_range.start,
rgba_color,
);
let inlay_id = inlay.id;
colors_splice.to_insert.push(inlay);
new_buffer_color_inlays
.push((new_range, new_color, inlay_id));
}
existing_buffer_colors.next();
break;
}
cmp::Ordering::Greater => {
let inlay = Inlay::color(
post_inc(&mut editor.next_color_inlay_id),
new_range.start,
@ -297,46 +363,40 @@ impl Editor {
);
let inlay_id = inlay.id;
colors_splice.to_insert.push(inlay);
new_color_inlays
new_buffer_color_inlays
.push((new_range, new_color, inlay_id));
break;
}
existing_colors.next();
break;
}
cmp::Ordering::Greater => {
let inlay = Inlay::color(
post_inc(&mut editor.next_color_inlay_id),
new_range.start,
rgba_color,
);
let inlay_id = inlay.id;
colors_splice.to_insert.push(inlay);
new_color_inlays.push((new_range, new_color, inlay_id));
break;
}
}
}
None => {
let inlay = Inlay::color(
post_inc(&mut editor.next_color_inlay_id),
new_range.start,
rgba_color,
);
let inlay_id = inlay.id;
colors_splice.to_insert.push(inlay);
new_color_inlays.push((new_range, new_color, inlay_id));
break;
None => {
let inlay = Inlay::color(
post_inc(&mut editor.next_color_inlay_id),
new_range.start,
rgba_color,
);
let inlay_id = inlay.id;
colors_splice.to_insert.push(inlay);
new_buffer_color_inlays
.push((new_range, new_color, inlay_id));
break;
}
}
}
}
}
if existing_colors.peek().is_some() {
colors_splice
.to_remove
.extend(existing_colors.map(|(_, _, id)| *id));
if existing_buffer_colors.peek().is_some() {
colors_splice
.to_remove
.extend(existing_buffer_colors.map(|(_, _, id)| *id));
}
updated |= colors.set_colors(
buffer_id,
new_buffer_color_inlays,
new_cache_version,
);
}
let mut updated = colors.set_colors(new_color_inlays);
if colors.render_mode == DocumentColorsRenderMode::Inlay
&& (!colors_splice.to_insert.is_empty()
|| !colors_splice.to_remove.is_empty())

View file

@ -1,8 +1,8 @@
use crate::{
Copy, CopyAndTrim, CopyPermalinkToLine, Cut, DisplayPoint, DisplaySnapshot, Editor,
EvaluateSelectedText, FindAllReferences, GoToDeclaration, GoToDefinition, GoToImplementation,
GoToTypeDefinition, Paste, Rename, RevealInFileManager, SelectMode, SelectionExt,
ToDisplayPoint, ToggleCodeActions,
GoToTypeDefinition, Paste, Rename, RevealInFileManager, SelectMode, SelectionEffects,
SelectionExt, ToDisplayPoint, ToggleCodeActions,
actions::{Format, FormatSelections},
selections_collection::SelectionsCollection,
};
@ -177,7 +177,7 @@ pub fn deploy_context_menu(
let anchor = buffer.anchor_before(point.to_point(&display_map));
if !display_ranges(&display_map, &editor.selections).any(|r| r.contains(&point)) {
// Move the cursor to the clicked location so that dispatched actions make sense
editor.change_selections(None, window, cx, |s| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.clear_disjoint();
s.set_pending_anchor_range(anchor..anchor, SelectMode::Character);
});
@ -275,10 +275,10 @@ pub fn deploy_context_menu(
cx,
),
None => {
let character_size = editor.character_size(window);
let character_size = editor.character_dimensions(window);
let menu_position = MenuPosition::PinnedToEditor {
source: source_anchor,
offset: gpui::point(character_size.width, character_size.height),
offset: gpui::point(character_size.em_width, character_size.line_height),
};
Some(MouseContextMenu::new(
editor,

View file

@ -1,4 +1,4 @@
use crate::{ApplyAllDiffHunks, Editor, EditorEvent, SemanticsProvider};
use crate::{ApplyAllDiffHunks, Editor, EditorEvent, SelectionEffects, SemanticsProvider};
use buffer_diff::BufferDiff;
use collections::HashSet;
use futures::{channel::mpsc, future::join_all};
@ -213,7 +213,9 @@ impl ProposedChangesEditor {
self.buffer_entries = buffer_entries;
self.editor.update(cx, |editor, cx| {
editor.change_selections(None, window, cx, |selections| selections.refresh());
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
selections.refresh()
});
editor.buffer.update(cx, |buffer, cx| {
for diff in new_diffs {
buffer.add_diff(diff, cx)

View file

@ -487,8 +487,9 @@ impl Editor {
if opened_first_time {
cx.spawn_in(window, async move |editor, cx| {
editor
.update(cx, |editor, cx| {
editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx)
.update_in(cx, |editor, window, cx| {
editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
editor.refresh_colors(false, None, window, cx);
})
.ok()
})
@ -599,6 +600,7 @@ impl Editor {
);
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
self.refresh_colors(false, None, window, cx);
}
pub fn scroll_position(&self, cx: &mut Context<Self>) -> gpui::Point<f32> {

View file

@ -5,7 +5,7 @@ use std::{rc::Rc, sync::LazyLock};
pub use crate::rust_analyzer_ext::expand_macro_recursively;
use crate::{
DisplayPoint, Editor, EditorMode, FoldPlaceholder, MultiBuffer,
DisplayPoint, Editor, EditorMode, FoldPlaceholder, MultiBuffer, SelectionEffects,
display_map::{
Block, BlockPlacement, CustomBlockId, DisplayMap, DisplayRow, DisplaySnapshot,
ToDisplayPoint,
@ -93,7 +93,9 @@ pub fn select_ranges(
) {
let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true);
assert_eq!(editor.text(cx), unmarked_text);
editor.change_selections(None, window, cx, |s| s.select_ranges(text_ranges));
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges(text_ranges)
});
}
#[track_caller]

View file

@ -1,5 +1,5 @@
use crate::{
AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer, RowExt,
AnchorRangeExt, DisplayPoint, Editor, MultiBuffer, RowExt,
display_map::{HighlightKey, ToDisplayPoint},
};
use buffer_diff::DiffHunkStatusKind;
@ -362,7 +362,7 @@ impl EditorTestContext {
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
self.editor.update_in(&mut self.cx, |editor, window, cx| {
editor.set_text(unmarked_text, window, cx);
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
editor.change_selections(Default::default(), window, cx, |s| {
s.select_ranges(selection_ranges)
})
});
@ -379,7 +379,7 @@ impl EditorTestContext {
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
self.editor.update_in(&mut self.cx, |editor, window, cx| {
assert_eq!(editor.text(cx), unmarked_text);
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
editor.change_selections(Default::default(), window, cx, |s| {
s.select_ranges(selection_ranges)
})
});

View file

@ -221,6 +221,9 @@ impl ExampleContext {
ThreadEvent::ShowError(thread_error) => {
tx.try_send(Err(anyhow!(thread_error.clone()))).ok();
}
ThreadEvent::RetriesFailed { .. } => {
// Ignore retries failed events
}
ThreadEvent::Stopped(reason) => match reason {
Ok(StopReason::EndTurn) => {
tx.close_channel();

View file

@ -259,6 +259,36 @@ async fn copy_extension_resources(
}
}
if !manifest.debug_adapters.is_empty() {
for (debug_adapter, entry) in &manifest.debug_adapters {
let schema_path = entry.schema_path.clone().unwrap_or_else(|| {
PathBuf::from("debug_adapter_schemas".to_owned())
.join(debug_adapter.as_ref())
.with_extension("json")
});
let parent = schema_path
.parent()
.with_context(|| format!("invalid empty schema path for {debug_adapter}"))?;
fs::create_dir_all(output_dir.join(parent))?;
copy_recursive(
fs.as_ref(),
&extension_path.join(&schema_path),
&output_dir.join(&schema_path),
CopyOptions {
overwrite: true,
ignore_if_exists: false,
},
)
.await
.with_context(|| {
format!(
"failed to copy debug adapter schema '{}'",
schema_path.display()
)
})?;
}
}
Ok(())
}

View file

@ -60,6 +60,7 @@ const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[
("r", &["r", "R"]),
("racket", &["rkt"]),
("rescript", &["res", "resi"]),
("rst", &["rst"]),
("ruby", &["rb", "erb"]),
("scheme", &["scm"]),
("scss", &["scss"]),

View file

@ -74,7 +74,7 @@ impl FakeGitRepository {
impl GitRepository for FakeGitRepository {
fn reload_index(&self) {}
fn load_index_text(&self, path: RepoPath) -> BoxFuture<Option<String>> {
fn load_index_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>> {
async {
self.with_state_async(false, move |state| {
state
@ -89,7 +89,7 @@ impl GitRepository for FakeGitRepository {
.boxed()
}
fn load_committed_text(&self, path: RepoPath) -> BoxFuture<Option<String>> {
fn load_committed_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>> {
async {
self.with_state_async(false, move |state| {
state
@ -108,7 +108,7 @@ impl GitRepository for FakeGitRepository {
&self,
_commit: String,
_cx: AsyncApp,
) -> BoxFuture<Result<git::repository::CommitDiff>> {
) -> BoxFuture<'_, Result<git::repository::CommitDiff>> {
unimplemented!()
}
@ -117,7 +117,7 @@ impl GitRepository for FakeGitRepository {
path: RepoPath,
content: Option<String>,
_env: Arc<HashMap<String, String>>,
) -> BoxFuture<anyhow::Result<()>> {
) -> BoxFuture<'_, anyhow::Result<()>> {
self.with_state_async(true, move |state| {
if let Some(message) = &state.simulated_index_write_error_message {
anyhow::bail!("{message}");
@ -134,7 +134,7 @@ impl GitRepository for FakeGitRepository {
None
}
fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<Result<Vec<Option<String>>>> {
fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<'_, Result<Vec<Option<String>>>> {
self.with_state_async(false, |state| {
Ok(revs
.into_iter()
@ -143,7 +143,7 @@ impl GitRepository for FakeGitRepository {
})
}
fn show(&self, commit: String) -> BoxFuture<Result<CommitDetails>> {
fn show(&self, commit: String) -> BoxFuture<'_, Result<CommitDetails>> {
async {
Ok(CommitDetails {
sha: commit.into(),
@ -158,7 +158,7 @@ impl GitRepository for FakeGitRepository {
_commit: String,
_mode: ResetMode,
_env: Arc<HashMap<String, String>>,
) -> BoxFuture<Result<()>> {
) -> BoxFuture<'_, Result<()>> {
unimplemented!()
}
@ -167,7 +167,7 @@ impl GitRepository for FakeGitRepository {
_commit: String,
_paths: Vec<RepoPath>,
_env: Arc<HashMap<String, String>>,
) -> BoxFuture<Result<()>> {
) -> BoxFuture<'_, Result<()>> {
unimplemented!()
}
@ -179,11 +179,11 @@ impl GitRepository for FakeGitRepository {
self.common_dir_path.clone()
}
fn merge_message(&self) -> BoxFuture<Option<String>> {
fn merge_message(&self) -> BoxFuture<'_, Option<String>> {
async move { None }.boxed()
}
fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<Result<GitStatus>> {
fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<'_, Result<GitStatus>> {
let workdir_path = self.dot_git_path.parent().unwrap();
// Load gitignores
@ -314,7 +314,7 @@ impl GitRepository for FakeGitRepository {
async move { result? }.boxed()
}
fn branches(&self) -> BoxFuture<Result<Vec<Branch>>> {
fn branches(&self) -> BoxFuture<'_, Result<Vec<Branch>>> {
self.with_state_async(false, move |state| {
let current_branch = &state.current_branch_name;
Ok(state
@ -330,21 +330,21 @@ impl GitRepository for FakeGitRepository {
})
}
fn change_branch(&self, name: String) -> BoxFuture<Result<()>> {
fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
self.with_state_async(true, |state| {
state.current_branch_name = Some(name);
Ok(())
})
}
fn create_branch(&self, name: String) -> BoxFuture<Result<()>> {
fn create_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
self.with_state_async(true, move |state| {
state.branches.insert(name.to_owned());
Ok(())
})
}
fn blame(&self, path: RepoPath, _content: Rope) -> BoxFuture<Result<git::blame::Blame>> {
fn blame(&self, path: RepoPath, _content: Rope) -> BoxFuture<'_, Result<git::blame::Blame>> {
self.with_state_async(false, move |state| {
state
.blames
@ -358,7 +358,7 @@ impl GitRepository for FakeGitRepository {
&self,
_paths: Vec<RepoPath>,
_env: Arc<HashMap<String, String>>,
) -> BoxFuture<Result<()>> {
) -> BoxFuture<'_, Result<()>> {
unimplemented!()
}
@ -366,7 +366,7 @@ impl GitRepository for FakeGitRepository {
&self,
_paths: Vec<RepoPath>,
_env: Arc<HashMap<String, String>>,
) -> BoxFuture<Result<()>> {
) -> BoxFuture<'_, Result<()>> {
unimplemented!()
}
@ -376,7 +376,7 @@ impl GitRepository for FakeGitRepository {
_name_and_email: Option<(gpui::SharedString, gpui::SharedString)>,
_options: CommitOptions,
_env: Arc<HashMap<String, String>>,
) -> BoxFuture<Result<()>> {
) -> BoxFuture<'_, Result<()>> {
unimplemented!()
}
@ -388,7 +388,7 @@ impl GitRepository for FakeGitRepository {
_askpass: AskPassDelegate,
_env: Arc<HashMap<String, String>>,
_cx: AsyncApp,
) -> BoxFuture<Result<git::repository::RemoteCommandOutput>> {
) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
unimplemented!()
}
@ -399,7 +399,7 @@ impl GitRepository for FakeGitRepository {
_askpass: AskPassDelegate,
_env: Arc<HashMap<String, String>>,
_cx: AsyncApp,
) -> BoxFuture<Result<git::repository::RemoteCommandOutput>> {
) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
unimplemented!()
}
@ -409,19 +409,19 @@ impl GitRepository for FakeGitRepository {
_askpass: AskPassDelegate,
_env: Arc<HashMap<String, String>>,
_cx: AsyncApp,
) -> BoxFuture<Result<git::repository::RemoteCommandOutput>> {
) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
unimplemented!()
}
fn get_remotes(&self, _branch: Option<String>) -> BoxFuture<Result<Vec<Remote>>> {
fn get_remotes(&self, _branch: Option<String>) -> BoxFuture<'_, Result<Vec<Remote>>> {
unimplemented!()
}
fn check_for_pushed_commit(&self) -> BoxFuture<Result<Vec<gpui::SharedString>>> {
fn check_for_pushed_commit(&self) -> BoxFuture<'_, Result<Vec<gpui::SharedString>>> {
future::ready(Ok(Vec::new())).boxed()
}
fn diff(&self, _diff: git::repository::DiffType) -> BoxFuture<Result<String>> {
fn diff(&self, _diff: git::repository::DiffType) -> BoxFuture<'_, Result<String>> {
unimplemented!()
}
@ -429,7 +429,10 @@ impl GitRepository for FakeGitRepository {
unimplemented!()
}
fn restore_checkpoint(&self, _checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>> {
fn restore_checkpoint(
&self,
_checkpoint: GitRepositoryCheckpoint,
) -> BoxFuture<'_, Result<()>> {
unimplemented!()
}
@ -437,7 +440,7 @@ impl GitRepository for FakeGitRepository {
&self,
_left: GitRepositoryCheckpoint,
_right: GitRepositoryCheckpoint,
) -> BoxFuture<Result<bool>> {
) -> BoxFuture<'_, Result<bool>> {
unimplemented!()
}
@ -445,7 +448,7 @@ impl GitRepository for FakeGitRepository {
&self,
_base_checkpoint: GitRepositoryCheckpoint,
_target_checkpoint: GitRepositoryCheckpoint,
) -> BoxFuture<Result<String>> {
) -> BoxFuture<'_, Result<String>> {
unimplemented!()
}
}

View file

@ -303,25 +303,25 @@ pub trait GitRepository: Send + Sync {
/// Returns the contents of an entry in the repository's index, or None if there is no entry for the given path.
///
/// Also returns `None` for symlinks.
fn load_index_text(&self, path: RepoPath) -> BoxFuture<Option<String>>;
fn load_index_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>>;
/// Returns the contents of an entry in the repository's HEAD, or None if HEAD does not exist or has no entry for the given path.
///
/// Also returns `None` for symlinks.
fn load_committed_text(&self, path: RepoPath) -> BoxFuture<Option<String>>;
fn load_committed_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>>;
fn set_index_text(
&self,
path: RepoPath,
content: Option<String>,
env: Arc<HashMap<String, String>>,
) -> BoxFuture<anyhow::Result<()>>;
) -> BoxFuture<'_, anyhow::Result<()>>;
/// Returns the URL of the remote with the given name.
fn remote_url(&self, name: &str) -> Option<String>;
/// Resolve a list of refs to SHAs.
fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<Result<Vec<Option<String>>>>;
fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<'_, Result<Vec<Option<String>>>>;
fn head_sha(&self) -> BoxFuture<'_, Option<String>> {
async move {
@ -335,33 +335,33 @@ pub trait GitRepository: Send + Sync {
.boxed()
}
fn merge_message(&self) -> BoxFuture<Option<String>>;
fn merge_message(&self) -> BoxFuture<'_, Option<String>>;
fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<Result<GitStatus>>;
fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<'_, Result<GitStatus>>;
fn branches(&self) -> BoxFuture<Result<Vec<Branch>>>;
fn branches(&self) -> BoxFuture<'_, Result<Vec<Branch>>>;
fn change_branch(&self, name: String) -> BoxFuture<Result<()>>;
fn create_branch(&self, name: String) -> BoxFuture<Result<()>>;
fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>>;
fn create_branch(&self, name: String) -> BoxFuture<'_, Result<()>>;
fn reset(
&self,
commit: String,
mode: ResetMode,
env: Arc<HashMap<String, String>>,
) -> BoxFuture<Result<()>>;
) -> BoxFuture<'_, Result<()>>;
fn checkout_files(
&self,
commit: String,
paths: Vec<RepoPath>,
env: Arc<HashMap<String, String>>,
) -> BoxFuture<Result<()>>;
) -> BoxFuture<'_, Result<()>>;
fn show(&self, commit: String) -> BoxFuture<Result<CommitDetails>>;
fn show(&self, commit: String) -> BoxFuture<'_, Result<CommitDetails>>;
fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<Result<CommitDiff>>;
fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture<Result<crate::blame::Blame>>;
fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<'_, Result<CommitDiff>>;
fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture<'_, Result<crate::blame::Blame>>;
/// Returns the absolute path to the repository. For worktrees, this will be the path to the
/// worktree's gitdir within the main repository (typically `.git/worktrees/<name>`).
@ -376,7 +376,7 @@ pub trait GitRepository: Send + Sync {
&self,
paths: Vec<RepoPath>,
env: Arc<HashMap<String, String>>,
) -> BoxFuture<Result<()>>;
) -> BoxFuture<'_, Result<()>>;
/// Updates the index to match HEAD at the given paths.
///
/// If any of the paths were previously staged but do not exist in HEAD, they will be removed from the index.
@ -384,7 +384,7 @@ pub trait GitRepository: Send + Sync {
&self,
paths: Vec<RepoPath>,
env: Arc<HashMap<String, String>>,
) -> BoxFuture<Result<()>>;
) -> BoxFuture<'_, Result<()>>;
fn commit(
&self,
@ -392,7 +392,7 @@ pub trait GitRepository: Send + Sync {
name_and_email: Option<(SharedString, SharedString)>,
options: CommitOptions,
env: Arc<HashMap<String, String>>,
) -> BoxFuture<Result<()>>;
) -> BoxFuture<'_, Result<()>>;
fn push(
&self,
@ -404,7 +404,7 @@ pub trait GitRepository: Send + Sync {
// This method takes an AsyncApp to ensure it's invoked on the main thread,
// otherwise git-credentials-manager won't work.
cx: AsyncApp,
) -> BoxFuture<Result<RemoteCommandOutput>>;
) -> BoxFuture<'_, Result<RemoteCommandOutput>>;
fn pull(
&self,
@ -415,7 +415,7 @@ pub trait GitRepository: Send + Sync {
// This method takes an AsyncApp to ensure it's invoked on the main thread,
// otherwise git-credentials-manager won't work.
cx: AsyncApp,
) -> BoxFuture<Result<RemoteCommandOutput>>;
) -> BoxFuture<'_, Result<RemoteCommandOutput>>;
fn fetch(
&self,
@ -425,35 +425,35 @@ pub trait GitRepository: Send + Sync {
// This method takes an AsyncApp to ensure it's invoked on the main thread,
// otherwise git-credentials-manager won't work.
cx: AsyncApp,
) -> BoxFuture<Result<RemoteCommandOutput>>;
) -> BoxFuture<'_, Result<RemoteCommandOutput>>;
fn get_remotes(&self, branch_name: Option<String>) -> BoxFuture<Result<Vec<Remote>>>;
fn get_remotes(&self, branch_name: Option<String>) -> BoxFuture<'_, Result<Vec<Remote>>>;
/// returns a list of remote branches that contain HEAD
fn check_for_pushed_commit(&self) -> BoxFuture<Result<Vec<SharedString>>>;
fn check_for_pushed_commit(&self) -> BoxFuture<'_, Result<Vec<SharedString>>>;
/// Run git diff
fn diff(&self, diff: DiffType) -> BoxFuture<Result<String>>;
fn diff(&self, diff: DiffType) -> BoxFuture<'_, Result<String>>;
/// Creates a checkpoint for the repository.
fn checkpoint(&self) -> BoxFuture<'static, Result<GitRepositoryCheckpoint>>;
/// Resets to a previously-created checkpoint.
fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>>;
fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<'_, Result<()>>;
/// Compares two checkpoints, returning true if they are equal
fn compare_checkpoints(
&self,
left: GitRepositoryCheckpoint,
right: GitRepositoryCheckpoint,
) -> BoxFuture<Result<bool>>;
) -> BoxFuture<'_, Result<bool>>;
/// Computes a diff between two checkpoints.
fn diff_checkpoints(
&self,
base_checkpoint: GitRepositoryCheckpoint,
target_checkpoint: GitRepositoryCheckpoint,
) -> BoxFuture<Result<String>>;
) -> BoxFuture<'_, Result<String>>;
}
pub enum DiffType {
@ -1032,32 +1032,39 @@ impl GitRepository for RealGitRepository {
fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
let repo = self.repository.clone();
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
let executor = self.executor.clone();
let branch = self.executor.spawn(async move {
let repo = repo.lock();
let branch = if let Ok(branch) = repo.find_branch(&name, BranchType::Local) {
branch
} else if let Ok(revision) = repo.find_branch(&name, BranchType::Remote) {
let (_, branch_name) = name.split_once("/").context("Unexpected branch format")?;
let revision = revision.get();
let branch_commit = revision.peel_to_commit()?;
let mut branch = repo.branch(&branch_name, &branch_commit, false)?;
branch.set_upstream(Some(&name))?;
branch
} else {
anyhow::bail!("Branch not found");
};
Ok(branch
.name()?
.context("cannot checkout anonymous branch")?
.to_string())
});
self.executor
.spawn(async move {
let repo = repo.lock();
let branch = if let Ok(branch) = repo.find_branch(&name, BranchType::Local) {
branch
} else if let Ok(revision) = repo.find_branch(&name, BranchType::Remote) {
let (_, branch_name) =
name.split_once("/").context("Unexpected branch format")?;
let revision = revision.get();
let branch_commit = revision.peel_to_commit()?;
let mut branch = repo.branch(&branch_name, &branch_commit, false)?;
branch.set_upstream(Some(&name))?;
branch
} else {
anyhow::bail!("Branch not found");
};
let branch = branch.await?;
let revision = branch.get();
let as_tree = revision.peel_to_tree()?;
repo.checkout_tree(as_tree.as_object(), None)?;
repo.set_head(
revision
.name()
.context("Branch name could not be retrieved")?,
)?;
Ok(())
GitBinary::new(git_binary_path, working_directory?, executor)
.run(&["checkout", &branch])
.await?;
anyhow::Ok(())
})
.boxed()
}
@ -2268,7 +2275,7 @@ mod tests {
impl RealGitRepository {
/// Force a Git garbage collection on the repository.
fn gc(&self) -> BoxFuture<Result<()>> {
fn gc(&self) -> BoxFuture<'_, Result<()>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
let executor = self.executor.clone();

View file

@ -245,7 +245,7 @@ impl PickerDelegate for BranchListDelegate {
type ListItem = ListItem;
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Select branch...".into()
"Select branch".into()
}
fn editor_position(&self) -> PickerEditorPosition {
@ -439,44 +439,43 @@ impl PickerDelegate for BranchListDelegate {
})
.unwrap_or_else(|| (None, None));
let branch_name = if entry.is_new {
h_flex()
.gap_1()
.child(
Icon::new(IconName::Plus)
.size(IconSize::Small)
.color(Color::Muted),
)
.child(
Label::new(format!("Create branch \"{}\"", entry.branch.name()))
.single_line()
.truncate(),
)
.into_any_element()
} else {
HighlightedLabel::new(entry.branch.name().to_owned(), entry.positions.clone())
.truncate()
.into_any_element()
};
Some(
ListItem::new(SharedString::from(format!("vcs-menu-{ix}")))
.inset(true)
.spacing(match self.style {
BranchListStyle::Modal => ListItemSpacing::default(),
BranchListStyle::Popover => ListItemSpacing::ExtraDense,
})
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(
v_flex()
.w_full()
.overflow_hidden()
.child(
h_flex()
.w_full()
.flex_shrink()
.overflow_x_hidden()
.gap_2()
.gap_6()
.justify_between()
.child(div().flex_shrink().overflow_x_hidden().child(
if entry.is_new {
Label::new(format!(
"Create branch \"{}\"",
entry.branch.name()
))
.single_line()
.into_any_element()
} else {
HighlightedLabel::new(
entry.branch.name().to_owned(),
entry.positions.clone(),
)
.truncate()
.into_any_element()
},
))
.when_some(commit_time, |el, commit_time| {
el.child(
.overflow_x_hidden()
.child(branch_name)
.when_some(commit_time, |label, commit_time| {
label.child(
Label::new(commit_time)
.size(LabelSize::Small)
.color(Color::Muted)

View file

@ -1,6 +1,6 @@
use anyhow::{Context as _, Result};
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
use editor::{Editor, EditorEvent, MultiBuffer};
use editor::{Editor, EditorEvent, MultiBuffer, SelectionEffects};
use git::repository::{CommitDetails, CommitDiff, CommitSummary, RepoPath};
use gpui::{
AnyElement, AnyView, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter,
@ -154,7 +154,7 @@ impl CommitView {
});
editor.update(cx, |editor, cx| {
editor.disable_header_for_buffer(metadata_buffer_id.unwrap(), cx);
editor.change_selections(None, window, cx, |selections| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
selections.select_ranges(vec![0..0]);
});
});

View file

@ -83,7 +83,6 @@ actions!(
FocusEditor,
FocusChanges,
ToggleFillCoAuthors,
GenerateCommitMessage
]
);

View file

@ -8,7 +8,7 @@ use anyhow::Result;
use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus};
use collections::HashSet;
use editor::{
Editor, EditorEvent,
Editor, EditorEvent, SelectionEffects,
actions::{GoToHunk, GoToPreviousHunk},
scroll::Autoscroll,
};
@ -255,9 +255,14 @@ impl ProjectDiff {
fn move_to_path(&mut self, path_key: PathKey, window: &mut Window, cx: &mut Context<Self>) {
if let Some(position) = self.multibuffer.read(cx).location_for_path(&path_key, cx) {
self.editor.update(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::focused()), window, cx, |s| {
s.select_ranges([position..position]);
})
editor.change_selections(
SelectionEffects::scroll(Autoscroll::focused()),
window,
cx,
|s| {
s.select_ranges([position..position]);
},
)
});
} else {
self.pending_scroll = Some(path_key);
@ -463,7 +468,7 @@ impl ProjectDiff {
self.editor.update(cx, |editor, cx| {
if was_empty {
editor.change_selections(None, window, cx, |selections| {
editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
// TODO select the very beginning (possibly inside a deletion)
selections.select_ranges([0..0])
});

View file

@ -2,8 +2,8 @@ pub mod cursor_position;
use cursor_position::{LineIndicatorFormat, UserCaretPosition};
use editor::{
Anchor, Editor, MultiBufferSnapshot, RowHighlightOptions, ToOffset, ToPoint, actions::Tab,
scroll::Autoscroll,
Anchor, Editor, MultiBufferSnapshot, RowHighlightOptions, SelectionEffects, ToOffset, ToPoint,
actions::Tab, scroll::Autoscroll,
};
use gpui::{
App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, SharedString, Styled,
@ -249,9 +249,12 @@ impl GoToLine {
let Some(start) = self.anchor_from_query(&snapshot, cx) else {
return;
};
editor.change_selections(Some(Autoscroll::center()), window, cx, |s| {
s.select_anchor_ranges([start..start])
});
editor.change_selections(
SelectionEffects::scroll(Autoscroll::center()),
window,
cx,
|s| s.select_anchor_ranges([start..start]),
);
editor.focus_handle(cx).focus(window);
cx.notify()
});

View file

@ -48,6 +48,8 @@ macro_rules! actions {
/// actions!(editor, [MoveUp, MoveDown, MoveLeft, MoveRight, Newline]);
/// ```
///
/// Registering the actions with the same name will result in a panic during `App` creation.
///
/// # Derive Macro
///
/// More complex data types can also be actions, by using the derive macro for `Action`:
@ -280,14 +282,27 @@ impl ActionRegistry {
}
fn insert_action(&mut self, action: MacroActionData) {
let name = action.name;
if self.by_name.contains_key(name) {
panic!(
"Action with name `{name}` already registered \
(might be registered in `#[action(deprecated_aliases = [...])]`."
);
}
self.by_name.insert(
action.name,
name,
ActionData {
build: action.build,
json_schema: action.json_schema,
},
);
for &alias in action.deprecated_aliases {
if self.by_name.contains_key(alias) {
panic!(
"Action with name `{alias}` already registered. \
`{alias}` is specified in `#[action(deprecated_aliases = [...])]` for action `{name}`."
);
}
self.by_name.insert(
alias,
ActionData {
@ -295,14 +310,13 @@ impl ActionRegistry {
json_schema: action.json_schema,
},
);
self.deprecated_aliases.insert(alias, action.name);
self.deprecated_aliases.insert(alias, name);
self.all_names.push(alias);
}
self.names_by_type_id.insert(action.type_id, action.name);
self.all_names.push(action.name);
self.names_by_type_id.insert(action.type_id, name);
self.all_names.push(name);
if let Some(deprecation_msg) = action.deprecation_message {
self.deprecation_messages
.insert(action.name, deprecation_msg);
self.deprecation_messages.insert(name, deprecation_msg);
}
}

View file

@ -214,32 +214,6 @@ impl<T: ?Sized> DerefMut for ArenaBox<T> {
}
}
pub struct ArenaRef<T: ?Sized>(ArenaBox<T>);
impl<T: ?Sized> From<ArenaBox<T>> for ArenaRef<T> {
fn from(value: ArenaBox<T>) -> Self {
ArenaRef(value)
}
}
impl<T: ?Sized> Clone for ArenaRef<T> {
fn clone(&self) -> Self {
Self(ArenaBox {
ptr: self.0.ptr,
valid: self.0.valid.clone(),
})
}
}
impl<T: ?Sized> Deref for ArenaRef<T> {
type Target = T;
#[inline(always)]
fn deref(&self) -> &Self::Target {
self.0.deref()
}
}
#[cfg(test)]
mod tests {
use std::{cell::Cell, rc::Rc};

View file

@ -432,7 +432,7 @@ mod tests {
actions!(
test_only,
[
A, B, C, D, E, F, G, // Don't wrap, test the trailing comma
H, I, J, K, L, M, N, // Don't wrap, test the trailing comma
]
);
}

View file

@ -151,7 +151,7 @@ pub fn guess_compositor() -> &'static str {
pub(crate) fn current_platform(_headless: bool) -> Rc<dyn Platform> {
Rc::new(
WindowsPlatform::new()
.inspect_err(|err| show_error("Error: Zed failed to launch", err.to_string()))
.inspect_err(|err| show_error("Failed to launch", err.to_string()))
.unwrap(),
)
}

View file

@ -29,14 +29,14 @@ pub unsafe fn new_renderer(
}
impl rwh::HasWindowHandle for RawWindow {
fn window_handle(&self) -> Result<rwh::WindowHandle, rwh::HandleError> {
fn window_handle(&self) -> Result<rwh::WindowHandle<'_>, rwh::HandleError> {
let view = NonNull::new(self.view).unwrap();
let handle = rwh::AppKitWindowHandle::new(view);
Ok(unsafe { rwh::WindowHandle::borrow_raw(handle.into()) })
}
}
impl rwh::HasDisplayHandle for RawWindow {
fn display_handle(&self) -> Result<rwh::DisplayHandle, rwh::HandleError> {
fn display_handle(&self) -> Result<rwh::DisplayHandle<'_>, rwh::HandleError> {
let handle = rwh::AppKitDisplayHandle::new();
Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) })
}

View file

@ -252,11 +252,11 @@ impl Drop for WaylandWindow {
}
impl WaylandWindow {
fn borrow(&self) -> Ref<WaylandWindowState> {
fn borrow(&self) -> Ref<'_, WaylandWindowState> {
self.0.state.borrow()
}
fn borrow_mut(&self) -> RefMut<WaylandWindowState> {
fn borrow_mut(&self) -> RefMut<'_, WaylandWindowState> {
self.0.state.borrow_mut()
}
@ -699,12 +699,14 @@ impl WaylandWindowStatePtr {
}
}
if let PlatformInput::KeyDown(event) = input {
if let Some(key_char) = &event.keystroke.key_char {
let mut state = self.state.borrow_mut();
if let Some(mut input_handler) = state.input_handler.take() {
drop(state);
input_handler.replace_text_in_range(None, key_char);
self.state.borrow_mut().input_handler = Some(input_handler);
if event.keystroke.modifiers.is_subset_of(&Modifiers::shift()) {
if let Some(key_char) = &event.keystroke.key_char {
let mut state = self.state.borrow_mut();
if let Some(mut input_handler) = state.input_handler.take() {
drop(state);
input_handler.replace_text_in_range(None, key_char);
self.state.borrow_mut().input_handler = Some(input_handler);
}
}
}
}

Some files were not shown because too many files have changed in this diff Show more