diff --git a/.github/ISSUE_TEMPLATE/10_bug_report.yml b/.github/ISSUE_TEMPLATE/10_bug_report.yml index 1bf6c80e40..e132eca1e5 100644 --- a/.github/ISSUE_TEMPLATE/10_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/10_bug_report.yml @@ -14,7 +14,7 @@ body: ### Description diff --git a/.github/actionlint.yml b/.github/actionlint.yml index 6d8e0107e9..0ee6af8a1d 100644 --- a/.github/actionlint.yml +++ b/.github/actionlint.yml @@ -19,27 +19,14 @@ self-hosted-runner: - namespace-profile-16x32-ubuntu-2004-arm - namespace-profile-32x64-ubuntu-2004-arm # Namespace Ubuntu 22.04 (Everything else) + - namespace-profile-2x4-ubuntu-2204 - namespace-profile-4x8-ubuntu-2204 - namespace-profile-8x16-ubuntu-2204 - namespace-profile-16x32-ubuntu-2204 - namespace-profile-32x64-ubuntu-2204 - # Namespace Ubuntu 24.04 (like ubuntu-latest) - - namespace-profile-2x4-ubuntu-2404 # Namespace Limited Preview - namespace-profile-8x16-ubuntu-2004-arm-m4 - namespace-profile-8x32-ubuntu-2004-arm-m4 # Self Hosted Runners - self-mini-macos - self-32vcpu-windows-2022 - -# Disable shellcheck because it doesn't like powershell -# This should have been triggered with initial rollout of actionlint -# but https://github.com/zed-industries/zed/pull/36693 -# somehow caused actionlint to actually check those windows jobs -# where previously they were being skipped. Likely caused by an -# unknown bug in actionlint where parsing of `runs-on: [ ]` -# breaks something else. (yuck) -paths: - .github/workflows/{ci,release_nightly}.yml: - ignore: - - "shellcheck" diff --git a/.github/workflows/bump_collab_staging.yml b/.github/workflows/bump_collab_staging.yml index d400905b4d..d8eaa6019e 100644 --- a/.github/workflows/bump_collab_staging.yml +++ b/.github/workflows/bump_collab_staging.yml @@ -8,7 +8,7 @@ on: jobs: update-collab-staging-tag: if: github.repository_owner == 'zed-industries' - runs-on: namespace-profile-2x4-ubuntu-2404 + runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a34833d0fd..f4ba227168 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: run_nix: ${{ steps.filter.outputs.run_nix }} run_actionlint: ${{ steps.filter.outputs.run_actionlint }} runs-on: - - namespace-profile-2x4-ubuntu-2404 + - ubuntu-latest steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -237,7 +237,7 @@ jobs: uses: ./.github/actions/build_docs actionlint: - runs-on: namespace-profile-2x4-ubuntu-2404 + runs-on: ubuntu-latest if: github.repository_owner == 'zed-industries' && needs.job_spec.outputs.run_actionlint == 'true' needs: [job_spec] steps: @@ -418,7 +418,7 @@ jobs: if: | github.repository_owner == 'zed-industries' && needs.job_spec.outputs.run_tests == 'true' - runs-on: [self-32vcpu-windows-2022] + runs-on: [self-hosted, Windows, X64] steps: - name: Environment Setup run: | @@ -458,7 +458,7 @@ jobs: tests_pass: name: Tests Pass - runs-on: namespace-profile-2x4-ubuntu-2404 + runs-on: ubuntu-latest needs: - job_spec - style @@ -784,7 +784,7 @@ jobs: bundle-windows-x64: timeout-minutes: 120 name: Create a Windows installer - runs-on: [self-32vcpu-windows-2022] + runs-on: [self-hosted, Windows, X64] if: contains(github.event.pull_request.labels.*.name, 'run-bundling') # if: (startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling')) needs: [windows_tests] diff --git a/.github/workflows/danger.yml b/.github/workflows/danger.yml index 3f84179278..15c82643ae 100644 --- a/.github/workflows/danger.yml +++ b/.github/workflows/danger.yml @@ -12,7 +12,7 @@ on: jobs: danger: if: github.repository_owner == 'zed-industries' - runs-on: namespace-profile-2x4-ubuntu-2404 + runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index 2026ee7b73..0cc6737a45 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -59,7 +59,7 @@ jobs: timeout-minutes: 60 name: Run tests on Windows if: github.repository_owner == 'zed-industries' - runs-on: [self-32vcpu-windows-2022] + runs-on: [self-hosted, Windows, X64] steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -206,6 +206,9 @@ jobs: runs-on: github-8vcpu-ubuntu-2404 needs: tests name: Build Zed on FreeBSD + # env: + # MYTOKEN : ${{ secrets.MYTOKEN }} + # MYTOKEN2: "value2" steps: - uses: actions/checkout@v4 - name: Build FreeBSD remote-server @@ -240,6 +243,7 @@ jobs: bundle-nix: name: Build and cache Nix package + if: false needs: tests secrets: inherit uses: ./.github/workflows/nix.yml @@ -248,7 +252,7 @@ jobs: timeout-minutes: 60 name: Create a Windows installer if: github.repository_owner == 'zed-industries' - runs-on: [self-32vcpu-windows-2022] + runs-on: [self-hosted, Windows, X64] needs: windows-tests env: AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }} @@ -290,7 +294,7 @@ jobs: update-nightly-tag: name: Update nightly tag if: github.repository_owner == 'zed-industries' - runs-on: namespace-profile-2x4-ubuntu-2404 + runs-on: ubuntu-latest needs: - bundle-mac - bundle-linux-x86 diff --git a/.github/workflows/script_checks.yml b/.github/workflows/script_checks.yml index 5dbfc9cb7f..c32a433e46 100644 --- a/.github/workflows/script_checks.yml +++ b/.github/workflows/script_checks.yml @@ -12,7 +12,7 @@ jobs: shellcheck: name: "ShellCheck Scripts" if: github.repository_owner == 'zed-industries' - runs-on: namespace-profile-2x4-ubuntu-2404 + runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 diff --git a/Cargo.lock b/Cargo.lock index 42649b137f..37853f82ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1384,11 +1384,10 @@ version = "0.1.0" dependencies = [ "anyhow", "collections", + "derive_more", "gpui", + "parking_lot", "rodio", - "schemars", - "serde", - "settings", "util", "workspace-hack", ] @@ -4054,7 +4053,6 @@ dependencies = [ name = "crashes" version = "0.1.0" dependencies = [ - "bincode", "crash-handler", "log", "mach2 0.5.0", @@ -4064,7 +4062,6 @@ dependencies = [ "serde", "serde_json", "smol", - "system_specs", "workspace-hack", ] @@ -4686,6 +4683,7 @@ dependencies = [ "component", "ctor", "editor", + "futures 0.3.31", "gpui", "indoc", "language", @@ -5722,10 +5720,14 @@ dependencies = [ name = "feedback" version = "0.1.0" dependencies = [ + "client", "editor", "gpui", + "human_bytes", "menu", - "system_specs", + "release_channel", + "serde", + "sysinfo", "ui", "urlencoding", "util", @@ -8468,7 +8470,6 @@ dependencies = [ "theme", "ui", "util", - "util_macros", "workspace", "workspace-hack", "zed_actions", @@ -9605,7 +9606,6 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", - "audio", "collections", "core-foundation 0.10.0", "core-video", @@ -9628,7 +9628,6 @@ dependencies = [ "scap", "serde", "serde_json", - "settings", "sha2", "simplelog", "smallvec", @@ -11615,12 +11614,6 @@ dependencies = [ "hmac", ] -[[package]] -name = "pciid-parser" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0008e816fcdaf229cdd540e9b6ca2dc4a10d65c31624abb546c6420a02846e61" - [[package]] name = "pem" version = "3.0.5" @@ -13521,7 +13514,6 @@ dependencies = [ "smol", "sysinfo", "telemetry_events", - "thiserror 2.0.12", "toml 0.8.20", "unindent", "util", @@ -16140,21 +16132,6 @@ dependencies = [ "winx", ] -[[package]] -name = "system_specs" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "gpui", - "human_bytes", - "pciid-parser", - "release_channel", - "serde", - "sysinfo", - "workspace-hack", -] - [[package]] name = "tab_switcher" version = "0.1.0" @@ -19789,6 +19766,7 @@ dependencies = [ "any_vec", "anyhow", "async-recursion", + "bincode", "call", "client", "clock", @@ -19807,7 +19785,6 @@ dependencies = [ "node_runtime", "parking_lot", "postage", - "pretty_assertions", "project", "remote", "schemars", @@ -20396,7 +20373,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.202.0" +version = "0.201.4" dependencies = [ "acp_tools", "activity_indicator", @@ -20414,7 +20391,6 @@ dependencies = [ "auto_update", "auto_update_ui", "backtrace", - "bincode", "breadcrumbs", "call", "channel", @@ -20513,7 +20489,6 @@ dependencies = [ "supermaven", "svg_preview", "sysinfo", - "system_specs", "tab_switcher", "task", "tasks_ui", diff --git a/Cargo.toml b/Cargo.toml index 6ec243a9b9..6cd6dec791 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -156,7 +156,6 @@ members = [ "crates/streaming_diff", "crates/sum_tree", "crates/supermaven", - "crates/system_specs", "crates/supermaven_api", "crates/svg_preview", "crates/tab_switcher", @@ -384,7 +383,6 @@ streaming_diff = { path = "crates/streaming_diff" } sum_tree = { path = "crates/sum_tree" } supermaven = { path = "crates/supermaven" } supermaven_api = { path = "crates/supermaven_api" } -system_specs = { path = "crates/system_specs" } tab_switcher = { path = "crates/tab_switcher" } task = { path = "crates/task" } tasks_ui = { path = "crates/tasks_ui" } @@ -453,7 +451,6 @@ aws-sdk-bedrockruntime = { version = "1.80.0", features = [ aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] } aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] } base64 = "0.22" -bincode = "1.2.1" bitflags = "2.6.0" blade-graphics = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" } blade-macros = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" } @@ -497,7 +494,6 @@ handlebars = "4.3" heck = "0.5" heed = { version = "0.21.0", features = ["read-txn-no-tls"] } hex = "0.4.3" -human_bytes = "0.4.1" html5ever = "0.27.0" http = "1.1" http-body = "1.0" @@ -537,7 +533,6 @@ palette = { version = "0.7.5", default-features = false, features = ["std"] } parking_lot = "0.12.1" partial-json-fixer = "0.5.3" parse_int = "0.9" -pciid-parser = "0.8.0" pathdiff = "0.2" pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" } pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "845945b830297a50de0e24020b980a65e4820559" } @@ -808,12 +803,6 @@ unexpected_cfgs = { level = "allow" } dbg_macro = "deny" todo = "deny" -# This is not a style lint, see https://github.com/rust-lang/rust-clippy/pull/15454 -# Remove when the lint gets promoted to `suspicious`. -declare_interior_mutable_const = "deny" - -redundant_clone = "deny" - # We currently do not restrict any style rules # as it slows down shipping code to Zed. # diff --git a/Procfile.web b/Procfile.web deleted file mode 100644 index 8140555144..0000000000 --- a/Procfile.web +++ /dev/null @@ -1,2 +0,0 @@ -postgrest_llm: postgrest crates/collab/postgrest_llm.conf -website: cd ../zed.dev; npm run dev -- --port=3000 diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 3cca560c00..955e68f5a9 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -16,6 +16,7 @@ "up": "menu::SelectPrevious", "enter": "menu::Confirm", "ctrl-enter": "menu::SecondaryConfirm", + "ctrl-escape": "menu::Cancel", "ctrl-c": "menu::Cancel", "escape": "menu::Cancel", "alt-shift-enter": "menu::Restart", @@ -40,7 +41,7 @@ "shift-f11": "debugger::StepOut", "f11": "zed::ToggleFullScreen", "ctrl-alt-z": "edit_prediction::RateCompletions", - "ctrl-alt-shift-i": "edit_prediction::ToggleMenu", + "ctrl-shift-i": "edit_prediction::ToggleMenu", "ctrl-alt-l": "lsp_tool::ToggleMenu" } }, @@ -120,7 +121,7 @@ "alt-g m": "git::OpenModifiedFiles", "menu": "editor::OpenContextMenu", "shift-f10": "editor::OpenContextMenu", - "ctrl-alt-shift-e": "editor::ToggleEditPrediction", + "ctrl-shift-e": "editor::ToggleEditPrediction", "f9": "editor::ToggleBreakpoint", "shift-f9": "editor::EditLogBreakpoint" } @@ -855,7 +856,7 @@ "ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }], "ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }], "alt-ctrl-r": "project_panel::RevealInFileManager", - "ctrl-shift-enter": "workspace::OpenWithSystem", + "ctrl-shift-enter": "project_panel::OpenWithSystem", "alt-d": "project_panel::CompareMarkedFiles", "shift-find": "project_panel::NewSearchInDirectory", "ctrl-alt-shift-f": "project_panel::NewSearchInDirectory", @@ -1194,16 +1195,9 @@ "ctrl-1": "onboarding::ActivateBasicsPage", "ctrl-2": "onboarding::ActivateEditingPage", "ctrl-3": "onboarding::ActivateAISetupPage", - "ctrl-enter": "onboarding::Finish", - "alt-shift-l": "onboarding::SignIn", + "ctrl-escape": "onboarding::Finish", + "alt-tab": "onboarding::SignIn", "alt-shift-a": "onboarding::OpenAccount" } - }, - { - "context": "InvalidBuffer", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-enter": "workspace::OpenWithSystem" - } } ] diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index e72f4174ff..8b18299a91 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -915,7 +915,7 @@ "cmd-backspace": ["project_panel::Trash", { "skip_prompt": true }], "cmd-delete": ["project_panel::Delete", { "skip_prompt": false }], "alt-cmd-r": "project_panel::RevealInFileManager", - "ctrl-shift-enter": "workspace::OpenWithSystem", + "ctrl-shift-enter": "project_panel::OpenWithSystem", "alt-d": "project_panel::CompareMarkedFiles", "cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }], "cmd-alt-shift-f": "project_panel::NewSearchInDirectory", @@ -1301,12 +1301,5 @@ "alt-tab": "onboarding::SignIn", "alt-shift-a": "onboarding::OpenAccount" } - }, - { - "context": "InvalidBuffer", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-enter": "workspace::OpenWithSystem" - } } ] diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json deleted file mode 100644 index c7a6c3149c..0000000000 --- a/assets/keymaps/default-windows.json +++ /dev/null @@ -1,1260 +0,0 @@ -[ - // Standard Windows bindings - { - "use_key_equivalents": true, - "bindings": { - "home": "menu::SelectFirst", - "shift-pageup": "menu::SelectFirst", - "pageup": "menu::SelectFirst", - "end": "menu::SelectLast", - "shift-pagedown": "menu::SelectLast", - "pagedown": "menu::SelectLast", - "ctrl-n": "menu::SelectNext", - "tab": "menu::SelectNext", - "down": "menu::SelectNext", - "ctrl-p": "menu::SelectPrevious", - "shift-tab": "menu::SelectPrevious", - "up": "menu::SelectPrevious", - "enter": "menu::Confirm", - "ctrl-enter": "menu::SecondaryConfirm", - "ctrl-escape": "menu::Cancel", - "ctrl-c": "menu::Cancel", - "escape": "menu::Cancel", - "shift-alt-enter": "menu::Restart", - "alt-enter": ["picker::ConfirmInput", { "secondary": false }], - "ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }], - "ctrl-shift-w": "workspace::CloseWindow", - "shift-escape": "workspace::ToggleZoom", - "open": "workspace::Open", - "ctrl-o": "workspace::Open", - "ctrl-=": ["zed::IncreaseBufferFontSize", { "persist": false }], - "ctrl-shift-=": ["zed::IncreaseBufferFontSize", { "persist": false }], - "ctrl--": ["zed::DecreaseBufferFontSize", { "persist": false }], - "ctrl-0": ["zed::ResetBufferFontSize", { "persist": false }], - "ctrl-,": "zed::OpenSettings", - "ctrl-q": "zed::Quit", - "f4": "debugger::Start", - "shift-f5": "debugger::Stop", - "ctrl-shift-f5": "debugger::RerunSession", - "f6": "debugger::Pause", - "f7": "debugger::StepOver", - "ctrl-f11": "debugger::StepInto", - "shift-f11": "debugger::StepOut", - "f11": "zed::ToggleFullScreen", - "ctrl-shift-i": "edit_prediction::ToggleMenu", - "shift-alt-l": "lsp_tool::ToggleMenu" - } - }, - { - "context": "Picker || menu", - "use_key_equivalents": true, - "bindings": { - "up": "menu::SelectPrevious", - "down": "menu::SelectNext" - } - }, - { - "context": "Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "editor::Cancel", - "shift-backspace": "editor::Backspace", - "backspace": "editor::Backspace", - "delete": "editor::Delete", - "tab": "editor::Tab", - "shift-tab": "editor::Backtab", - "ctrl-k": "editor::CutToEndOfLine", - "ctrl-k ctrl-q": "editor::Rewrap", - "ctrl-k q": "editor::Rewrap", - "ctrl-backspace": "editor::DeleteToPreviousWordStart", - "ctrl-delete": "editor::DeleteToNextWordEnd", - "cut": "editor::Cut", - "shift-delete": "editor::Cut", - "ctrl-x": "editor::Cut", - "copy": "editor::Copy", - "ctrl-insert": "editor::Copy", - "ctrl-c": "editor::Copy", - "paste": "editor::Paste", - "shift-insert": "editor::Paste", - "ctrl-v": "editor::Paste", - "undo": "editor::Undo", - "ctrl-z": "editor::Undo", - "redo": "editor::Redo", - "ctrl-y": "editor::Redo", - "ctrl-shift-z": "editor::Redo", - "up": "editor::MoveUp", - "ctrl-up": "editor::LineUp", - "ctrl-down": "editor::LineDown", - "pageup": "editor::MovePageUp", - "alt-pageup": "editor::PageUp", - "shift-pageup": "editor::SelectPageUp", - "home": ["editor::MoveToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }], - "down": "editor::MoveDown", - "pagedown": "editor::MovePageDown", - "alt-pagedown": "editor::PageDown", - "shift-pagedown": "editor::SelectPageDown", - "end": ["editor::MoveToEndOfLine", { "stop_at_soft_wraps": true }], - "left": "editor::MoveLeft", - "right": "editor::MoveRight", - "ctrl-left": "editor::MoveToPreviousWordStart", - "ctrl-right": "editor::MoveToNextWordEnd", - "ctrl-home": "editor::MoveToBeginning", - "ctrl-end": "editor::MoveToEnd", - "shift-up": "editor::SelectUp", - "shift-down": "editor::SelectDown", - "shift-left": "editor::SelectLeft", - "shift-right": "editor::SelectRight", - "ctrl-shift-left": "editor::SelectToPreviousWordStart", - "ctrl-shift-right": "editor::SelectToNextWordEnd", - "ctrl-shift-home": "editor::SelectToBeginning", - "ctrl-shift-end": "editor::SelectToEnd", - "ctrl-a": "editor::SelectAll", - "ctrl-l": "editor::SelectLine", - "shift-alt-f": "editor::Format", - "shift-alt-o": "editor::OrganizeImports", - "shift-home": ["editor::SelectToBeginningOfLine", { "stop_at_soft_wraps": true, "stop_at_indent": true }], - "shift-end": ["editor::SelectToEndOfLine", { "stop_at_soft_wraps": true }], - "ctrl-alt-space": "editor::ShowCharacterPalette", - "ctrl-;": "editor::ToggleLineNumbers", - "ctrl-'": "editor::ToggleSelectedDiffHunks", - "ctrl-\"": "editor::ExpandAllDiffHunks", - "ctrl-i": "editor::ShowSignatureHelp", - "alt-g b": "git::Blame", - "alt-g m": "git::OpenModifiedFiles", - "menu": "editor::OpenContextMenu", - "shift-f10": "editor::OpenContextMenu", - "ctrl-shift-e": "editor::ToggleEditPrediction", - "f9": "editor::ToggleBreakpoint", - "shift-f9": "editor::EditLogBreakpoint" - } - }, - { - "context": "Editor && mode == full", - "use_key_equivalents": true, - "bindings": { - "shift-enter": "editor::Newline", - "enter": "editor::Newline", - "ctrl-enter": "editor::NewlineAbove", - "ctrl-shift-enter": "editor::NewlineBelow", - "ctrl-k ctrl-z": "editor::ToggleSoftWrap", - "ctrl-k z": "editor::ToggleSoftWrap", - "find": "buffer_search::Deploy", - "ctrl-f": "buffer_search::Deploy", - "ctrl-h": "buffer_search::DeployReplace", - "ctrl-shift-.": "assistant::QuoteSelection", - "ctrl-shift-,": "assistant::InsertIntoEditor", - "shift-alt-e": "editor::SelectEnclosingSymbol", - "ctrl-shift-backspace": "editor::GoToPreviousChange", - "ctrl-shift-alt-backspace": "editor::GoToNextChange", - "alt-enter": "editor::OpenSelectionsInMultibuffer" - } - }, - { - "context": "Editor && mode == full && edit_prediction", - "use_key_equivalents": true, - "bindings": { - "alt-]": "editor::NextEditPrediction", - "alt-[": "editor::PreviousEditPrediction" - } - }, - { - "context": "Editor && !edit_prediction", - "use_key_equivalents": true, - "bindings": { - "alt-\\": "editor::ShowEditPrediction" - } - }, - { - "context": "Editor && mode == auto_height", - "use_key_equivalents": true, - "bindings": { - "ctrl-enter": "editor::Newline", - "shift-enter": "editor::Newline", - "ctrl-shift-enter": "editor::NewlineBelow" - } - }, - { - "context": "Markdown", - "use_key_equivalents": true, - "bindings": { - "copy": "markdown::Copy", - "ctrl-c": "markdown::Copy" - } - }, - { - "context": "Editor && jupyter && !ContextEditor", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-enter": "repl::Run", - "ctrl-alt-enter": "repl::RunInPlace" - } - }, - { - "context": "Editor && !agent_diff", - "use_key_equivalents": true, - "bindings": { - "ctrl-k ctrl-r": "git::Restore", - "alt-y": "git::StageAndNext", - "shift-alt-y": "git::UnstageAndNext" - } - }, - { - "context": "Editor && editor_agent_diff", - "use_key_equivalents": true, - "bindings": { - "ctrl-y": "agent::Keep", - "ctrl-n": "agent::Reject", - "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll", - "ctrl-shift-r": "agent::OpenAgentDiff" - } - }, - { - "context": "AgentDiff", - "use_key_equivalents": true, - "bindings": { - "ctrl-y": "agent::Keep", - "ctrl-n": "agent::Reject", - "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll" - } - }, - { - "context": "ContextEditor > Editor", - "use_key_equivalents": true, - "bindings": { - "ctrl-enter": "assistant::Assist", - "ctrl-s": "workspace::Save", - "save": "workspace::Save", - "ctrl-shift-,": "assistant::InsertIntoEditor", - "shift-enter": "assistant::Split", - "ctrl-r": "assistant::CycleMessageRole", - "enter": "assistant::ConfirmCommand", - "alt-enter": "editor::Newline", - "ctrl-k c": "assistant::CopyCode", - "ctrl-g": "search::SelectNextMatch", - "ctrl-shift-g": "search::SelectPreviousMatch", - "ctrl-k l": "agent::OpenRulesLibrary" - } - }, - { - "context": "AgentPanel", - "use_key_equivalents": true, - "bindings": { - "ctrl-n": "agent::NewThread", - "shift-alt-n": "agent::NewTextThread", - "ctrl-shift-h": "agent::OpenHistory", - "shift-alt-c": "agent::OpenSettings", - "shift-alt-p": "agent::OpenRulesLibrary", - "ctrl-i": "agent::ToggleProfileSelector", - "shift-alt-/": "agent::ToggleModelSelector", - "ctrl-shift-a": "agent::ToggleContextPicker", - "ctrl-shift-j": "agent::ToggleNavigationMenu", - "ctrl-shift-i": "agent::ToggleOptionsMenu", - // "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu", - "shift-alt-escape": "agent::ExpandMessageEditor", - "ctrl-shift-.": "assistant::QuoteSelection", - "shift-alt-e": "agent::RemoveAllContext", - "ctrl-shift-e": "project_panel::ToggleFocus", - "ctrl-shift-enter": "agent::ContinueThread", - "super-ctrl-b": "agent::ToggleBurnMode", - "alt-enter": "agent::ContinueWithBurnMode" - } - }, - { - "context": "AgentPanel > NavigationMenu", - "use_key_equivalents": true, - "bindings": { - "shift-backspace": "agent::DeleteRecentlyOpenThread" - } - }, - { - "context": "AgentPanel > Markdown", - "use_key_equivalents": true, - "bindings": { - "copy": "markdown::CopyAsMarkdown", - "ctrl-c": "markdown::CopyAsMarkdown" - } - }, - { - "context": "AgentPanel && prompt_editor", - "use_key_equivalents": true, - "bindings": { - "ctrl-n": "agent::NewTextThread", - "ctrl-alt-t": "agent::NewThread" - } - }, - { - "context": "AgentPanel && external_agent_thread", - "use_key_equivalents": true, - "bindings": { - "ctrl-n": "agent::NewExternalAgentThread", - "ctrl-alt-t": "agent::NewThread" - } - }, - { - "context": "MessageEditor && !Picker > Editor && !use_modifier_to_send", - "use_key_equivalents": true, - "bindings": { - "enter": "agent::Chat", - "ctrl-enter": "agent::ChatWithFollow", - "ctrl-i": "agent::ToggleProfileSelector", - "ctrl-shift-r": "agent::OpenAgentDiff", - "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll" - } - }, - { - "context": "MessageEditor && !Picker > Editor && use_modifier_to_send", - "use_key_equivalents": true, - "bindings": { - "ctrl-enter": "agent::Chat", - "enter": "editor::Newline", - "ctrl-i": "agent::ToggleProfileSelector", - "ctrl-shift-r": "agent::OpenAgentDiff", - "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll" - } - }, - { - "context": "EditMessageEditor > Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel", - "enter": "menu::Confirm", - "alt-enter": "editor::Newline" - } - }, - { - "context": "AgentFeedbackMessageEditor > Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel", - "enter": "menu::Confirm", - "alt-enter": "editor::Newline" - } - }, - { - "context": "ContextStrip", - "use_key_equivalents": true, - "bindings": { - "up": "agent::FocusUp", - "right": "agent::FocusRight", - "left": "agent::FocusLeft", - "down": "agent::FocusDown", - "backspace": "agent::RemoveFocusedContext", - "enter": "agent::AcceptSuggestedContext" - } - }, - { - "context": "AcpThread > Editor", - "use_key_equivalents": true, - "bindings": { - "enter": "agent::Chat", - "ctrl-shift-r": "agent::OpenAgentDiff", - "ctrl-shift-y": "agent::KeepAll", - "ctrl-shift-n": "agent::RejectAll" - } - }, - { - "context": "ThreadHistory", - "use_key_equivalents": true, - "bindings": { - "backspace": "agent::RemoveSelectedThread" - } - }, - { - "context": "PromptLibrary", - "use_key_equivalents": true, - "bindings": { - "new": "rules_library::NewRule", - "ctrl-n": "rules_library::NewRule", - "ctrl-shift-s": "rules_library::ToggleDefaultRule" - } - }, - { - "context": "BufferSearchBar", - "use_key_equivalents": true, - "bindings": { - "escape": "buffer_search::Dismiss", - "tab": "buffer_search::FocusEditor", - "enter": "search::SelectNextMatch", - "shift-enter": "search::SelectPreviousMatch", - "alt-enter": "search::SelectAllMatches", - "find": "search::FocusSearch", - "ctrl-f": "search::FocusSearch", - "ctrl-h": "search::ToggleReplace", - "ctrl-l": "search::ToggleSelection" - } - }, - { - "context": "BufferSearchBar && in_replace > Editor", - "use_key_equivalents": true, - "bindings": { - "enter": "search::ReplaceNext", - "ctrl-enter": "search::ReplaceAll" - } - }, - { - "context": "BufferSearchBar && !in_replace > Editor", - "use_key_equivalents": true, - "bindings": { - "up": "search::PreviousHistoryQuery", - "down": "search::NextHistoryQuery" - } - }, - { - "context": "ProjectSearchBar", - "use_key_equivalents": true, - "bindings": { - "escape": "project_search::ToggleFocus", - "shift-find": "search::FocusSearch", - "ctrl-shift-f": "search::FocusSearch", - "ctrl-shift-h": "search::ToggleReplace", - "alt-r": "search::ToggleRegex" // vscode - } - }, - { - "context": "ProjectSearchBar > Editor", - "use_key_equivalents": true, - "bindings": { - "up": "search::PreviousHistoryQuery", - "down": "search::NextHistoryQuery" - } - }, - { - "context": "ProjectSearchBar && in_replace > Editor", - "use_key_equivalents": true, - "bindings": { - "enter": "search::ReplaceNext", - "ctrl-alt-enter": "search::ReplaceAll" - } - }, - { - "context": "ProjectSearchView", - "use_key_equivalents": true, - "bindings": { - "escape": "project_search::ToggleFocus", - "ctrl-shift-h": "search::ToggleReplace", - "alt-r": "search::ToggleRegex" // vscode - } - }, - { - "context": "Pane", - "use_key_equivalents": true, - "bindings": { - "alt-1": ["pane::ActivateItem", 0], - "alt-2": ["pane::ActivateItem", 1], - "alt-3": ["pane::ActivateItem", 2], - "alt-4": ["pane::ActivateItem", 3], - "alt-5": ["pane::ActivateItem", 4], - "alt-6": ["pane::ActivateItem", 5], - "alt-7": ["pane::ActivateItem", 6], - "alt-8": ["pane::ActivateItem", 7], - "alt-9": ["pane::ActivateItem", 8], - "alt-0": "pane::ActivateLastItem", - "ctrl-pageup": "pane::ActivatePreviousItem", - "ctrl-pagedown": "pane::ActivateNextItem", - "ctrl-shift-pageup": "pane::SwapItemLeft", - "ctrl-shift-pagedown": "pane::SwapItemRight", - "ctrl-f4": ["pane::CloseActiveItem", { "close_pinned": false }], - "ctrl-w": ["pane::CloseActiveItem", { "close_pinned": false }], - "ctrl-shift-alt-t": ["pane::CloseOtherItems", { "close_pinned": false }], - "ctrl-shift-alt-w": "workspace::CloseInactiveTabsAndPanes", - "ctrl-k e": ["pane::CloseItemsToTheLeft", { "close_pinned": false }], - "ctrl-k t": ["pane::CloseItemsToTheRight", { "close_pinned": false }], - "ctrl-k u": ["pane::CloseCleanItems", { "close_pinned": false }], - "ctrl-k w": ["pane::CloseAllItems", { "close_pinned": false }], - "ctrl-k ctrl-w": "workspace::CloseAllItemsAndPanes", - "back": "pane::GoBack", - "alt--": "pane::GoBack", - "alt-=": "pane::GoForward", - "forward": "pane::GoForward", - "f3": "search::SelectNextMatch", - "shift-f3": "search::SelectPreviousMatch", - "shift-find": "project_search::ToggleFocus", - "ctrl-shift-f": "project_search::ToggleFocus", - "shift-alt-h": "search::ToggleReplace", - "alt-l": "search::ToggleSelection", - "alt-enter": "search::SelectAllMatches", - "alt-c": "search::ToggleCaseSensitive", - "alt-w": "search::ToggleWholeWord", - "alt-find": "project_search::ToggleFilters", - "alt-f": "project_search::ToggleFilters", - "alt-r": "search::ToggleRegex", - // "ctrl-shift-alt-x": "search::ToggleRegex", - "ctrl-k shift-enter": "pane::TogglePinTab" - } - }, - // Bindings from VS Code - { - "context": "Editor", - "use_key_equivalents": true, - "bindings": { - "ctrl-[": "editor::Outdent", - "ctrl-]": "editor::Indent", - "ctrl-shift-alt-up": "editor::AddSelectionAbove", // Insert Cursor Above - "ctrl-shift-alt-down": "editor::AddSelectionBelow", // Insert Cursor Below - "ctrl-shift-k": "editor::DeleteLine", - "alt-up": "editor::MoveLineUp", - "alt-down": "editor::MoveLineDown", - "shift-alt-up": "editor::DuplicateLineUp", - "shift-alt-down": "editor::DuplicateLineDown", - "shift-alt-right": "editor::SelectLargerSyntaxNode", // Expand Selection - "shift-alt-left": "editor::SelectSmallerSyntaxNode", // Shrink Selection - "ctrl-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection - "ctrl-f2": "editor::SelectAllMatches", // Select all occurrences of current word - "ctrl-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand - "ctrl-shift-down": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch - "ctrl-shift-up": ["editor::SelectPrevious", { "replace_newest": false }], // editor.action.addSelectionToPreviousFindMatch - "ctrl-k ctrl-d": ["editor::SelectNext", { "replace_newest": true }], // editor.action.moveSelectionToNextFindMatch / find_under_expand_skip - "ctrl-k ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch - "ctrl-k ctrl-i": "editor::Hover", - "ctrl-k ctrl-b": "editor::BlameHover", - "ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }], - "f8": ["editor::GoToDiagnostic", { "severity": { "min": "hint", "max": "error" } }], - "shift-f8": ["editor::GoToPreviousDiagnostic", { "severity": { "min": "hint", "max": "error" } }], - "f2": "editor::Rename", - "f12": "editor::GoToDefinition", - "alt-f12": "editor::GoToDefinitionSplit", - "ctrl-shift-f10": "editor::GoToDefinitionSplit", - "ctrl-f12": "editor::GoToImplementation", - "shift-f12": "editor::GoToTypeDefinition", - "ctrl-alt-f12": "editor::GoToTypeDefinitionSplit", - "shift-alt-f12": "editor::FindAllReferences", - "ctrl-m": "editor::MoveToEnclosingBracket", // from jetbrains - "ctrl-shift-\\": "editor::MoveToEnclosingBracket", - "ctrl-shift-[": "editor::Fold", - "ctrl-shift-]": "editor::UnfoldLines", - "ctrl-k ctrl-l": "editor::ToggleFold", - "ctrl-k ctrl-[": "editor::FoldRecursive", - "ctrl-k ctrl-]": "editor::UnfoldRecursive", - "ctrl-k ctrl-1": ["editor::FoldAtLevel", 1], - "ctrl-k ctrl-2": ["editor::FoldAtLevel", 2], - "ctrl-k ctrl-3": ["editor::FoldAtLevel", 3], - "ctrl-k ctrl-4": ["editor::FoldAtLevel", 4], - "ctrl-k ctrl-5": ["editor::FoldAtLevel", 5], - "ctrl-k ctrl-6": ["editor::FoldAtLevel", 6], - "ctrl-k ctrl-7": ["editor::FoldAtLevel", 7], - "ctrl-k ctrl-8": ["editor::FoldAtLevel", 8], - "ctrl-k ctrl-9": ["editor::FoldAtLevel", 9], - "ctrl-k ctrl-0": "editor::FoldAll", - "ctrl-k ctrl-j": "editor::UnfoldAll", - "ctrl-space": "editor::ShowCompletions", - "ctrl-shift-space": "editor::ShowWordCompletions", - "ctrl-.": "editor::ToggleCodeActions", - "ctrl-k r": "editor::RevealInFileManager", - "ctrl-k p": "editor::CopyPath", - "ctrl-\\": "pane::SplitRight", - "ctrl-shift-alt-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", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-o": "outline::Toggle", - "ctrl-g": "go_to_line::Toggle" - } - }, - { - "context": "Workspace", - "use_key_equivalents": true, - "bindings": { - "alt-open": ["projects::OpenRecent", { "create_new_window": false }], - // Change the default action on `menu::Confirm` by setting the parameter - // "ctrl-alt-o": ["projects::OpenRecent", { "create_new_window": true }], - "ctrl-r": ["projects::OpenRecent", { "create_new_window": false }], - "shift-alt-open": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }], - // Change to open path modal for existing remote connection by setting the parameter - // "ctrl-shift-alt-o": "["projects::OpenRemote", { "from_existing_connection": true }]", - "ctrl-shift-alt-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }], - "shift-alt-b": "branches::OpenRecent", - "shift-alt-enter": "toast::RunAction", - "ctrl-shift-`": "workspace::NewTerminal", - "save": "workspace::Save", - "ctrl-s": "workspace::Save", - "ctrl-k ctrl-shift-s": "workspace::SaveWithoutFormat", - "shift-save": "workspace::SaveAs", - "ctrl-shift-s": "workspace::SaveAs", - "new": "workspace::NewFile", - "ctrl-n": "workspace::NewFile", - "shift-new": "workspace::NewWindow", - "ctrl-shift-n": "workspace::NewWindow", - "ctrl-`": "terminal_panel::ToggleFocus", - "f10": ["app_menu::OpenApplicationMenu", "Zed"], - "alt-1": ["workspace::ActivatePane", 0], - "alt-2": ["workspace::ActivatePane", 1], - "alt-3": ["workspace::ActivatePane", 2], - "alt-4": ["workspace::ActivatePane", 3], - "alt-5": ["workspace::ActivatePane", 4], - "alt-6": ["workspace::ActivatePane", 5], - "alt-7": ["workspace::ActivatePane", 6], - "alt-8": ["workspace::ActivatePane", 7], - "alt-9": ["workspace::ActivatePane", 8], - "ctrl-alt-b": "workspace::ToggleRightDock", - "ctrl-b": "workspace::ToggleLeftDock", - "ctrl-j": "workspace::ToggleBottomDock", - "ctrl-shift-y": "workspace::CloseAllDocks", - "alt-r": "workspace::ResetActiveDockSize", - // For 0px parameter, uses UI font size value. - "shift-alt--": ["workspace::DecreaseActiveDockSize", { "px": 0 }], - "shift-alt-=": ["workspace::IncreaseActiveDockSize", { "px": 0 }], - "shift-alt-0": "workspace::ResetOpenDocksSize", - "ctrl-shift-alt--": ["workspace::DecreaseOpenDocksSize", { "px": 0 }], - "ctrl-shift-alt-=": ["workspace::IncreaseOpenDocksSize", { "px": 0 }], - "shift-find": "pane::DeploySearch", - "ctrl-shift-f": "pane::DeploySearch", - "ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }], - "ctrl-shift-t": "pane::ReopenClosedItem", - "ctrl-k ctrl-s": "zed::OpenKeymapEditor", - "ctrl-k ctrl-t": "theme_selector::Toggle", - "ctrl-alt-super-p": "settings_profile_selector::Toggle", - "ctrl-t": "project_symbols::Toggle", - "ctrl-p": "file_finder::Toggle", - "ctrl-tab": "tab_switcher::Toggle", - "ctrl-shift-tab": ["tab_switcher::Toggle", { "select_last": true }], - "ctrl-e": "file_finder::Toggle", - "f1": "command_palette::Toggle", - "ctrl-shift-p": "command_palette::Toggle", - "ctrl-shift-m": "diagnostics::Deploy", - "ctrl-shift-e": "project_panel::ToggleFocus", - "ctrl-shift-b": "outline_panel::ToggleFocus", - "ctrl-shift-g": "git_panel::ToggleFocus", - "ctrl-shift-d": "debug_panel::ToggleFocus", - "ctrl-shift-/": "agent::ToggleFocus", - "alt-save": "workspace::SaveAll", - "ctrl-k s": "workspace::SaveAll", - "ctrl-k m": "language_selector::Toggle", - "escape": "workspace::Unfollow", - "ctrl-k ctrl-left": "workspace::ActivatePaneLeft", - "ctrl-k ctrl-right": "workspace::ActivatePaneRight", - "ctrl-k ctrl-up": "workspace::ActivatePaneUp", - "ctrl-k ctrl-down": "workspace::ActivatePaneDown", - "ctrl-k shift-left": "workspace::SwapPaneLeft", - "ctrl-k shift-right": "workspace::SwapPaneRight", - "ctrl-k shift-up": "workspace::SwapPaneUp", - "ctrl-k shift-down": "workspace::SwapPaneDown", - "ctrl-shift-x": "zed::Extensions", - "ctrl-shift-r": "task::Rerun", - "alt-t": "task::Rerun", - "shift-alt-t": "task::Spawn", - "shift-alt-r": ["task::Spawn", { "reveal_target": "center" }], - // also possible to spawn tasks by name: - // "foo-bar": ["task::Spawn", { "task_name": "MyTask", "reveal_target": "dock" }] - // or by tag: - // "foo-bar": ["task::Spawn", { "task_tag": "MyTag" }], - "f5": "debugger::Rerun", - "ctrl-f4": "workspace::CloseActiveDock", - "ctrl-w": "workspace::CloseActiveDock" - } - }, - { - "context": "Workspace && debugger_running", - "use_key_equivalents": true, - "bindings": { - "f5": "zed::NoAction" - } - }, - { - "context": "Workspace && debugger_stopped", - "use_key_equivalents": true, - "bindings": { - "f5": "debugger::Continue" - } - }, - { - "context": "ApplicationMenu", - "use_key_equivalents": true, - "bindings": { - "f10": "menu::Cancel", - "left": "app_menu::ActivateMenuLeft", - "right": "app_menu::ActivateMenuRight" - } - }, - // Bindings from Sublime Text - { - "context": "Editor", - "use_key_equivalents": true, - "bindings": { - "ctrl-u": "editor::UndoSelection", - "ctrl-shift-u": "editor::RedoSelection", - "ctrl-shift-j": "editor::JoinLines", - "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart", - "shift-alt-h": "editor::DeleteToPreviousSubwordStart", - "ctrl-alt-delete": "editor::DeleteToNextSubwordEnd", - "shift-alt-d": "editor::DeleteToNextSubwordEnd", - "ctrl-alt-left": "editor::MoveToPreviousSubwordStart", - "ctrl-alt-right": "editor::MoveToNextSubwordEnd", - "ctrl-shift-alt-left": "editor::SelectToPreviousSubwordStart", - "ctrl-shift-alt-right": "editor::SelectToNextSubwordEnd" - } - }, - // Bindings from Atom - { - "context": "Pane", - "use_key_equivalents": true, - "bindings": { - "ctrl-k up": "pane::SplitUp", - "ctrl-k down": "pane::SplitDown", - "ctrl-k left": "pane::SplitLeft", - "ctrl-k right": "pane::SplitRight" - } - }, - // Bindings that should be unified with bindings for more general actions - { - "context": "Editor && renaming", - "use_key_equivalents": true, - "bindings": { - "enter": "editor::ConfirmRename" - } - }, - { - "context": "Editor && showing_completions", - "use_key_equivalents": true, - "bindings": { - "enter": "editor::ConfirmCompletion", - "shift-enter": "editor::ConfirmCompletionReplace", - "tab": "editor::ComposeCompletion" - } - }, - // Bindings for accepting edit predictions - // - // alt-l is provided as an alternative to tab/alt-tab. and will be displayed in the UI. This is - // because alt-tab may not be available, as it is often used for window switching. - { - "context": "Editor && edit_prediction", - "use_key_equivalents": true, - "bindings": { - "alt-tab": "editor::AcceptEditPrediction", - "alt-l": "editor::AcceptEditPrediction", - "tab": "editor::AcceptEditPrediction", - "alt-right": "editor::AcceptPartialEditPrediction" - } - }, - { - "context": "Editor && edit_prediction_conflict", - "use_key_equivalents": true, - "bindings": { - "alt-tab": "editor::AcceptEditPrediction", - "alt-l": "editor::AcceptEditPrediction", - "alt-right": "editor::AcceptPartialEditPrediction" - } - }, - { - "context": "Editor && showing_code_actions", - "use_key_equivalents": true, - "bindings": { - "enter": "editor::ConfirmCodeAction" - } - }, - { - "context": "Editor && (showing_code_actions || showing_completions)", - "use_key_equivalents": true, - "bindings": { - "ctrl-p": "editor::ContextMenuPrevious", - "up": "editor::ContextMenuPrevious", - "ctrl-n": "editor::ContextMenuNext", - "down": "editor::ContextMenuNext", - "pageup": "editor::ContextMenuFirst", - "pagedown": "editor::ContextMenuLast" - } - }, - { - "context": "Editor && showing_signature_help && !showing_completions", - "use_key_equivalents": true, - "bindings": { - "up": "editor::SignatureHelpPrevious", - "down": "editor::SignatureHelpNext" - } - }, - // Custom bindings - { - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-alt-f": "workspace::FollowNextCollaborator", - // Only available in debug builds: opens an element inspector for development. - "shift-alt-i": "dev::ToggleInspector" - } - }, - { - "context": "!Terminal", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-c": "collab_panel::ToggleFocus" - } - }, - { - "context": "!ContextEditor > Editor && mode == full", - "use_key_equivalents": true, - "bindings": { - "alt-enter": "editor::OpenExcerpts", - "shift-enter": "editor::ExpandExcerpts", - "ctrl-alt-enter": "editor::OpenExcerptsSplit", - "ctrl-shift-e": "pane::RevealInProjectPanel", - "ctrl-f8": "editor::GoToHunk", - "ctrl-shift-f8": "editor::GoToPreviousHunk", - "ctrl-enter": "assistant::InlineAssist", - "ctrl-shift-;": "editor::ToggleInlayHints" - } - }, - { - "context": "PromptEditor", - "use_key_equivalents": true, - "bindings": { - "ctrl-[": "agent::CyclePreviousInlineAssist", - "ctrl-]": "agent::CycleNextInlineAssist", - "shift-alt-e": "agent::RemoveAllContext" - } - }, - { - "context": "Prompt", - "use_key_equivalents": true, - "bindings": { - "left": "menu::SelectPrevious", - "right": "menu::SelectNext", - "h": "menu::SelectPrevious", - "l": "menu::SelectNext" - } - }, - { - "context": "ProjectSearchBar && !in_replace", - "use_key_equivalents": true, - "bindings": { - "ctrl-enter": "project_search::SearchInNew" - } - }, - { - "context": "OutlinePanel && not_editing", - "use_key_equivalents": true, - "bindings": { - "left": "outline_panel::CollapseSelectedEntry", - "right": "outline_panel::ExpandSelectedEntry", - "alt-copy": "outline_panel::CopyPath", - "shift-alt-c": "outline_panel::CopyPath", - "shift-alt-copy": "workspace::CopyRelativePath", - "ctrl-shift-alt-c": "workspace::CopyRelativePath", - "ctrl-alt-r": "outline_panel::RevealInFileManager", - "space": "outline_panel::OpenSelectedEntry", - "shift-down": "menu::SelectNext", - "shift-up": "menu::SelectPrevious", - "alt-enter": "editor::OpenExcerpts", - "ctrl-alt-enter": "editor::OpenExcerptsSplit" - } - }, - { - "context": "ProjectPanel", - "use_key_equivalents": true, - "bindings": { - "left": "project_panel::CollapseSelectedEntry", - "right": "project_panel::ExpandSelectedEntry", - "new": "project_panel::NewFile", - "ctrl-n": "project_panel::NewFile", - "alt-new": "project_panel::NewDirectory", - "alt-n": "project_panel::NewDirectory", - "cut": "project_panel::Cut", - "ctrl-x": "project_panel::Cut", - "copy": "project_panel::Copy", - "ctrl-insert": "project_panel::Copy", - "ctrl-c": "project_panel::Copy", - "paste": "project_panel::Paste", - "shift-insert": "project_panel::Paste", - "ctrl-v": "project_panel::Paste", - "alt-copy": "project_panel::CopyPath", - "shift-alt-c": "project_panel::CopyPath", - "shift-alt-copy": "workspace::CopyRelativePath", - "ctrl-k ctrl-shift-c": "workspace::CopyRelativePath", - "enter": "project_panel::Rename", - "f2": "project_panel::Rename", - "backspace": ["project_panel::Trash", { "skip_prompt": false }], - "delete": ["project_panel::Trash", { "skip_prompt": false }], - "shift-delete": ["project_panel::Delete", { "skip_prompt": false }], - "ctrl-backspace": ["project_panel::Delete", { "skip_prompt": false }], - "ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }], - "ctrl-alt-r": "project_panel::RevealInFileManager", - "ctrl-shift-enter": "project_panel::OpenWithSystem", - "alt-d": "project_panel::CompareMarkedFiles", - "shift-find": "project_panel::NewSearchInDirectory", - "ctrl-k ctrl-shift-f": "project_panel::NewSearchInDirectory", - "shift-down": "menu::SelectNext", - "shift-up": "menu::SelectPrevious", - "escape": "menu::Cancel" - } - }, - { - "context": "ProjectPanel && not_editing", - "use_key_equivalents": true, - "bindings": { - "space": "project_panel::Open" - } - }, - { - "context": "GitPanel && ChangesList", - "use_key_equivalents": true, - "bindings": { - "up": "menu::SelectPrevious", - "down": "menu::SelectNext", - "enter": "menu::Confirm", - "alt-y": "git::StageFile", - "shift-alt-y": "git::UnstageFile", - "space": "git::ToggleStaged", - "shift-space": "git::StageRange", - "tab": "git_panel::FocusEditor", - "shift-tab": "git_panel::FocusEditor", - "escape": "git_panel::ToggleFocus", - "alt-enter": "menu::SecondaryConfirm", - "delete": ["git::RestoreFile", { "skip_prompt": false }], - "backspace": ["git::RestoreFile", { "skip_prompt": false }], - "shift-delete": ["git::RestoreFile", { "skip_prompt": false }], - "ctrl-backspace": ["git::RestoreFile", { "skip_prompt": false }], - "ctrl-delete": ["git::RestoreFile", { "skip_prompt": false }] - } - }, - { - "context": "GitPanel && CommitEditor", - "use_key_equivalents": true, - "bindings": { - "escape": "git::Cancel" - } - }, - { - "context": "GitCommit > Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel", - "enter": "editor::Newline", - "ctrl-enter": "git::Commit", - "ctrl-shift-enter": "git::Amend", - "alt-l": "git::GenerateCommitMessage" - } - }, - { - "context": "GitPanel", - "use_key_equivalents": true, - "bindings": { - "ctrl-g ctrl-g": "git::Fetch", - "ctrl-g up": "git::Push", - "ctrl-g down": "git::Pull", - "ctrl-g shift-up": "git::ForcePush", - "ctrl-g d": "git::Diff", - "ctrl-g backspace": "git::RestoreTrackedFiles", - "ctrl-g shift-backspace": "git::TrashUntrackedFiles", - "ctrl-space": "git::StageAll", - "ctrl-shift-space": "git::UnstageAll", - "ctrl-enter": "git::Commit", - "ctrl-shift-enter": "git::Amend" - } - }, - { - "context": "GitDiff > Editor", - "use_key_equivalents": true, - "bindings": { - "ctrl-enter": "git::Commit", - "ctrl-shift-enter": "git::Amend", - "ctrl-space": "git::StageAll", - "ctrl-shift-space": "git::UnstageAll" - } - }, - { - "context": "AskPass > Editor", - "use_key_equivalents": true, - "bindings": { - "enter": "menu::Confirm" - } - }, - { - "context": "CommitEditor > Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "git_panel::FocusChanges", - "tab": "git_panel::FocusChanges", - "shift-tab": "git_panel::FocusChanges", - "enter": "editor::Newline", - "ctrl-enter": "git::Commit", - "ctrl-shift-enter": "git::Amend", - "alt-up": "git_panel::FocusChanges", - "alt-l": "git::GenerateCommitMessage" - } - }, - { - "context": "DebugPanel", - "use_key_equivalents": true, - "bindings": { - "ctrl-t": "debugger::ToggleThreadPicker", - "ctrl-i": "debugger::ToggleSessionPicker", - "shift-alt-escape": "debugger::ToggleExpandItem" - } - }, - { - "context": "VariableList", - "use_key_equivalents": true, - "bindings": { - "left": "variable_list::CollapseSelectedEntry", - "right": "variable_list::ExpandSelectedEntry", - "enter": "variable_list::EditVariable", - "ctrl-c": "variable_list::CopyVariableValue", - "ctrl-alt-c": "variable_list::CopyVariableName", - "delete": "variable_list::RemoveWatch", - "backspace": "variable_list::RemoveWatch", - "alt-enter": "variable_list::AddWatch" - } - }, - { - "context": "BreakpointList", - "use_key_equivalents": true, - "bindings": { - "space": "debugger::ToggleEnableBreakpoint", - "backspace": "debugger::UnsetBreakpoint", - "left": "debugger::PreviousBreakpointProperty", - "right": "debugger::NextBreakpointProperty" - } - }, - { - "context": "CollabPanel && not_editing", - "use_key_equivalents": true, - "bindings": { - "ctrl-backspace": "collab_panel::Remove", - "space": "menu::Confirm" - } - }, - { - "context": "CollabPanel", - "use_key_equivalents": true, - "bindings": { - "alt-up": "collab_panel::MoveChannelUp", - "alt-down": "collab_panel::MoveChannelDown" - } - }, - { - "context": "(CollabPanel && editing) > Editor", - "use_key_equivalents": true, - "bindings": { - "space": "collab_panel::InsertSpace" - } - }, - { - "context": "ChannelModal", - "use_key_equivalents": true, - "bindings": { - "tab": "channel_modal::ToggleMode" - } - }, - { - "context": "Picker > Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel", - "up": "menu::SelectPrevious", - "down": "menu::SelectNext", - "tab": "picker::ConfirmCompletion", - "alt-enter": ["picker::ConfirmInput", { "secondary": false }] - } - }, - { - "context": "ChannelModal > Picker > Editor", - "use_key_equivalents": true, - "bindings": { - "tab": "channel_modal::ToggleMode" - } - }, - { - "context": "FileFinder || (FileFinder > Picker > Editor)", - "use_key_equivalents": true, - "bindings": { - "ctrl-p": "file_finder::Toggle", - "ctrl-shift-a": "file_finder::ToggleSplitMenu", - "ctrl-shift-i": "file_finder::ToggleFilterMenu" - } - }, - { - "context": "FileFinder || (FileFinder > Picker > Editor) || (FileFinder > Picker > menu)", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-p": "file_finder::SelectPrevious", - "ctrl-j": "pane::SplitDown", - "ctrl-k": "pane::SplitUp", - "ctrl-h": "pane::SplitLeft", - "ctrl-l": "pane::SplitRight" - } - }, - { - "context": "TabSwitcher", - "use_key_equivalents": true, - "bindings": { - "ctrl-shift-tab": "menu::SelectPrevious", - "ctrl-up": "menu::SelectPrevious", - "ctrl-down": "menu::SelectNext", - "ctrl-backspace": "tab_switcher::CloseSelectedItem" - } - }, - { - "context": "Terminal", - "use_key_equivalents": true, - "bindings": { - "ctrl-alt-space": "terminal::ShowCharacterPalette", - "copy": "terminal::Copy", - "ctrl-insert": "terminal::Copy", - "ctrl-shift-c": "terminal::Copy", - "paste": "terminal::Paste", - "shift-insert": "terminal::Paste", - "ctrl-shift-v": "terminal::Paste", - "ctrl-enter": "assistant::InlineAssist", - "alt-b": ["terminal::SendText", "\u001bb"], - "alt-f": ["terminal::SendText", "\u001bf"], - "alt-.": ["terminal::SendText", "\u001b."], - "ctrl-delete": ["terminal::SendText", "\u001bd"], - // Overrides for conflicting keybindings - "ctrl-b": ["terminal::SendKeystroke", "ctrl-b"], - "ctrl-c": ["terminal::SendKeystroke", "ctrl-c"], - "ctrl-e": ["terminal::SendKeystroke", "ctrl-e"], - "ctrl-o": ["terminal::SendKeystroke", "ctrl-o"], - "ctrl-w": ["terminal::SendKeystroke", "ctrl-w"], - "ctrl-backspace": ["terminal::SendKeystroke", "ctrl-w"], - "ctrl-shift-a": "editor::SelectAll", - "find": "buffer_search::Deploy", - "ctrl-shift-f": "buffer_search::Deploy", - "ctrl-shift-l": "terminal::Clear", - "ctrl-shift-w": "pane::CloseActiveItem", - "up": ["terminal::SendKeystroke", "up"], - "pageup": ["terminal::SendKeystroke", "pageup"], - "down": ["terminal::SendKeystroke", "down"], - "pagedown": ["terminal::SendKeystroke", "pagedown"], - "escape": ["terminal::SendKeystroke", "escape"], - "enter": ["terminal::SendKeystroke", "enter"], - "shift-pageup": "terminal::ScrollPageUp", - "shift-pagedown": "terminal::ScrollPageDown", - "shift-up": "terminal::ScrollLineUp", - "shift-down": "terminal::ScrollLineDown", - "shift-home": "terminal::ScrollToTop", - "shift-end": "terminal::ScrollToBottom", - "ctrl-shift-space": "terminal::ToggleViMode", - "ctrl-shift-r": "terminal::RerunTask", - "ctrl-alt-r": "terminal::RerunTask", - "alt-t": "terminal::RerunTask" - } - }, - { - "context": "ZedPredictModal", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel" - } - }, - { - "context": "ConfigureContextServerModal > Editor", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel", - "enter": "editor::Newline", - "ctrl-enter": "menu::Confirm" - } - }, - { - "context": "OnboardingAiConfigurationModal", - "use_key_equivalents": true, - "bindings": { - "escape": "menu::Cancel" - } - }, - { - "context": "Diagnostics", - "use_key_equivalents": true, - "bindings": { - "ctrl-r": "diagnostics::ToggleDiagnosticsRefresh" - } - }, - { - "context": "DebugConsole > Editor", - "use_key_equivalents": true, - "bindings": { - "enter": "menu::Confirm", - "alt-enter": "console::WatchExpression" - } - }, - { - "context": "RunModal", - "use_key_equivalents": true, - "bindings": { - "ctrl-tab": "pane::ActivateNextItem", - "ctrl-shift-tab": "pane::ActivatePreviousItem" - } - }, - { - "context": "MarkdownPreview", - "use_key_equivalents": true, - "bindings": { - "pageup": "markdown::MovePageUp", - "pagedown": "markdown::MovePageDown" - } - }, - { - "context": "KeymapEditor", - "use_key_equivalents": true, - "bindings": { - "ctrl-f": "search::FocusSearch", - "alt-find": "keymap_editor::ToggleKeystrokeSearch", - "alt-f": "keymap_editor::ToggleKeystrokeSearch", - "alt-c": "keymap_editor::ToggleConflictFilter", - "enter": "keymap_editor::EditBinding", - "alt-enter": "keymap_editor::CreateBinding", - "ctrl-c": "keymap_editor::CopyAction", - "ctrl-shift-c": "keymap_editor::CopyContext", - "ctrl-t": "keymap_editor::ShowMatchingKeybinds" - } - }, - { - "context": "KeystrokeInput", - "use_key_equivalents": true, - "bindings": { - "enter": "keystroke_input::StartRecording", - "escape escape escape": "keystroke_input::StopRecording", - "delete": "keystroke_input::ClearKeystrokes" - } - }, - { - "context": "KeybindEditorModal", - "use_key_equivalents": true, - "bindings": { - "ctrl-enter": "menu::Confirm", - "escape": "menu::Cancel" - } - }, - { - "context": "KeybindEditorModal > Editor", - "use_key_equivalents": true, - "bindings": { - "up": "menu::SelectPrevious", - "down": "menu::SelectNext" - } - }, - { - "context": "Onboarding", - "use_key_equivalents": true, - "bindings": { - "ctrl-1": "onboarding::ActivateBasicsPage", - "ctrl-2": "onboarding::ActivateEditingPage", - "ctrl-3": "onboarding::ActivateAISetupPage", - "ctrl-escape": "onboarding::Finish", - "alt-tab": "onboarding::SignIn", - "shift-alt-a": "onboarding::OpenAccount" - } - } -] diff --git a/assets/keymaps/linux/emacs.json b/assets/keymaps/linux/emacs.json index 62910e297b..0ff3796f03 100755 --- a/assets/keymaps/linux/emacs.json +++ b/assets/keymaps/linux/emacs.json @@ -38,7 +38,6 @@ "alt-;": ["editor::ToggleComments", { "advance_downwards": false }], "ctrl-x ctrl-;": "editor::ToggleComments", "alt-.": "editor::GoToDefinition", // xref-find-definitions - "alt-?": "editor::FindAllReferences", // xref-find-references "alt-,": "pane::GoBack", // xref-pop-marker-stack "ctrl-x h": "editor::SelectAll", // mark-whole-buffer "ctrl-d": "editor::Delete", // delete-char diff --git a/assets/keymaps/macos/emacs.json b/assets/keymaps/macos/emacs.json index 62910e297b..0ff3796f03 100755 --- a/assets/keymaps/macos/emacs.json +++ b/assets/keymaps/macos/emacs.json @@ -38,7 +38,6 @@ "alt-;": ["editor::ToggleComments", { "advance_downwards": false }], "ctrl-x ctrl-;": "editor::ToggleComments", "alt-.": "editor::GoToDefinition", // xref-find-definitions - "alt-?": "editor::FindAllReferences", // xref-find-references "alt-,": "pane::GoBack", // xref-pop-marker-stack "ctrl-x h": "editor::SelectAll", // mark-whole-buffer "ctrl-d": "editor::Delete", // delete-char diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 67add61bd3..be6d34a134 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -428,13 +428,11 @@ "g h": "vim::StartOfLine", "g s": "vim::FirstNonWhitespace", // "g s" default behavior is "space s" "g e": "vim::EndOfDocument", - "g .": "vim::HelixGotoLastModification", // go to last modification "g r": "editor::FindAllReferences", // zed specific "g t": "vim::WindowTop", "g c": "vim::WindowMiddle", "g b": "vim::WindowBottom", - "shift-r": "editor::Paste", "x": "editor::SelectLine", "shift-x": "editor::SelectLine", "%": "editor::SelectAll", @@ -821,7 +819,7 @@ "v": "project_panel::OpenPermanent", "p": "project_panel::Open", "x": "project_panel::RevealInFileManager", - "s": "workspace::OpenWithSystem", + "s": "project_panel::OpenWithSystem", "z d": "project_panel::CompareMarkedFiles", "] c": "project_panel::SelectNextGitEntry", "[ c": "project_panel::SelectPrevGitEntry", diff --git a/assets/settings/default.json b/assets/settings/default.json index 804198090f..c290baf003 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -162,12 +162,6 @@ // 2. Always quit the application // "on_last_window_closed": "quit_app", "on_last_window_closed": "platform_default", - // Whether to show padding for zoomed panels. - // When enabled, zoomed center panels (e.g. code editor) will have padding all around, - // while zoomed bottom/left/right panels will have padding to the top/right/left (respectively). - // - // Default: true - "zoomed_padding": true, // Whether to use the system provided dialogs for Open and Save As. // When set to false, Zed will use the built-in keyboard-first pickers. "use_system_path_prompts": true, @@ -653,8 +647,6 @@ // "never" "show": "always" }, - // Whether to enable drag-and-drop operations in the project panel. - "drag_and_drop": true, // Whether to hide the root entry when only one folder is open in the window. "hide_root": false }, @@ -1141,6 +1133,11 @@ // The minimum severity of the diagnostics to show inline. // Inherits editor's diagnostics' max severity settings when `null`. "max_severity": null + }, + "cargo": { + // When enabled, Zed disables rust-analyzer's check on save and starts to query + // Cargo diagnostics separately. + "fetch_cargo_diagnostics": false } }, // Files or globs of files that will be excluded by Zed entirely. They will be skipped during file @@ -1506,11 +1503,6 @@ // // Default: fallback "words": "fallback", - // Minimum number of characters required to automatically trigger word-based completions. - // Before that value, it's still possible to trigger the words-based completion manually with the corresponding editor command. - // - // Default: 3 - "words_min_length": 3, // Whether to fetch LSP completions or not. // // Default: true @@ -1637,9 +1629,6 @@ "allowed": true } }, - "Kotlin": { - "language_servers": ["kotlin-language-server", "!kotlin-lsp", "..."] - }, "LaTeX": { "formatter": "language_server", "language_servers": ["texlab", "..."], @@ -1653,6 +1642,9 @@ "use_on_type_format": false, "allow_rewrap": "anywhere", "soft_wrap": "editor_width", + "completions": { + "words": "disabled" + }, "prettier": { "allowed": true } @@ -1666,6 +1658,9 @@ } }, "Plain Text": { + "completions": { + "words": "disabled" + }, "allow_rewrap": "anywhere" }, "Python": { diff --git a/assets/settings/initial_tasks.json b/assets/settings/initial_tasks.json index 5cead67b6d..a79c550671 100644 --- a/assets/settings/initial_tasks.json +++ b/assets/settings/initial_tasks.json @@ -43,8 +43,8 @@ // "args": ["--login"] // } // } - "shell": "system" + "shell": "system", // Represents the tags for inline runnable indicators, or spawning multiple tasks at once. - // "tags": [] + "tags": [] } ] diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs index 12ae893c31..ef0ac36f6e 100644 --- a/crates/agent_ui/src/acp/message_editor.rs +++ b/crates/agent_ui/src/acp/message_editor.rs @@ -1480,7 +1480,7 @@ impl SemanticsProvider for SlashCommandSemanticsProvider { buffer: &Entity, position: text::Anchor, cx: &mut App, - ) -> Option>>> { + ) -> Option>> { let snapshot = buffer.read(cx).snapshot(); let offset = position.to_offset(&snapshot); let (start, end) = self.range.get()?; @@ -1488,14 +1488,14 @@ impl SemanticsProvider for SlashCommandSemanticsProvider { return None; } let range = snapshot.anchor_after(start)..snapshot.anchor_after(end); - Some(Task::ready(Some(vec![project::Hover { + Some(Task::ready(vec![project::Hover { contents: vec![project::HoverBlock { text: "Slash commands are not supported".into(), kind: project::HoverBlockKind::PlainText, }], range: Some(range), language: None, - }]))) + }])) } fn inline_values( @@ -1545,7 +1545,7 @@ impl SemanticsProvider for SlashCommandSemanticsProvider { _position: text::Anchor, _kind: editor::GotoDefinitionKind, _cx: &mut App, - ) -> Option>>>> { + ) -> Option>>> { None } diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 224f49cc3e..ba7f8065d7 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -230,7 +230,7 @@ impl AgentConfiguration { let is_signed_in = self .workspace .read_with(cx, |workspace, _| { - !workspace.client().status().borrow().is_signed_out() + workspace.client().status().borrow().is_connected() }) .unwrap_or(false); diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index d1cf748733..414ad27b71 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -598,6 +598,17 @@ impl AgentPanel { None }; + // Wait for the Gemini/Native feature flag to be available. + let client = workspace.read_with(cx, |workspace, _| workspace.client().clone())?; + if !client.status().borrow().is_signed_out() { + cx.update(|_, cx| { + cx.wait_for_flag_or_timeout::( + Duration::from_secs(2), + ) + })? + .await; + } + let panel = workspace.update_in(cx, |workspace, window, cx| { let panel = cx.new(|cx| { Self::new( diff --git a/crates/ai_onboarding/src/ai_upsell_card.rs b/crates/ai_onboarding/src/ai_upsell_card.rs index 106dcb0aef..e9639ca075 100644 --- a/crates/ai_onboarding/src/ai_upsell_card.rs +++ b/crates/ai_onboarding/src/ai_upsell_card.rs @@ -12,11 +12,11 @@ use crate::{SignInStatus, YoungAccountBanner, plan_definitions::PlanDefinitions} #[derive(IntoElement, RegisterComponent)] pub struct AiUpsellCard { - sign_in_status: SignInStatus, - sign_in: Arc, - account_too_young: bool, - user_plan: Option, - tab_index: Option, + pub sign_in_status: SignInStatus, + pub sign_in: Arc, + pub account_too_young: bool, + pub user_plan: Option, + pub tab_index: Option, } impl AiUpsellCard { @@ -43,11 +43,6 @@ impl AiUpsellCard { tab_index: None, } } - - pub fn tab_index(mut self, tab_index: Option) -> Self { - self.tab_index = tab_index; - self - } } impl RenderOnce for AiUpsellCard { diff --git a/crates/audio/Cargo.toml b/crates/audio/Cargo.toml index ae7eb52fd3..5146396b92 100644 --- a/crates/audio/Cargo.toml +++ b/crates/audio/Cargo.toml @@ -15,10 +15,9 @@ doctest = false [dependencies] anyhow.workspace = true collections.workspace = true +derive_more.workspace = true gpui.workspace = true -settings.workspace = true -schemars.workspace = true -serde.workspace = true +parking_lot.workspace = true rodio = { workspace = true, features = [ "wav", "playback", "tracing" ] } util.workspace = true workspace-hack.workspace = true diff --git a/crates/audio/src/assets.rs b/crates/audio/src/assets.rs new file mode 100644 index 0000000000..fd5c935d87 --- /dev/null +++ b/crates/audio/src/assets.rs @@ -0,0 +1,54 @@ +use std::{io::Cursor, sync::Arc}; + +use anyhow::{Context as _, Result}; +use collections::HashMap; +use gpui::{App, AssetSource, Global}; +use rodio::{Decoder, Source, source::Buffered}; + +type Sound = Buffered>>>; + +pub struct SoundRegistry { + cache: Arc>>, + assets: Box, +} + +struct GlobalSoundRegistry(Arc); + +impl Global for GlobalSoundRegistry {} + +impl SoundRegistry { + pub fn new(source: impl AssetSource) -> Arc { + Arc::new(Self { + cache: Default::default(), + assets: Box::new(source), + }) + } + + pub fn global(cx: &App) -> Arc { + cx.global::().0.clone() + } + + pub(crate) fn set_global(source: impl AssetSource, cx: &mut App) { + cx.set_global(GlobalSoundRegistry(SoundRegistry::new(source))); + } + + pub fn get(&self, name: &str) -> Result + use<>> { + if let Some(wav) = self.cache.lock().get(name) { + return Ok(wav.clone()); + } + + let path = format!("sounds/{}.wav", name); + let bytes = self + .assets + .load(&path)? + .map(anyhow::Ok) + .with_context(|| format!("No asset available for path {path}"))?? + .into_owned(); + let cursor = Cursor::new(bytes); + let source = Decoder::new(cursor)?.buffered(); + + self.cache.lock().insert(name.to_string(), source.clone()); + + Ok(source) + } +} diff --git a/crates/audio/src/audio.rs b/crates/audio/src/audio.rs index b4f2c24fef..44baa16aa2 100644 --- a/crates/audio/src/audio.rs +++ b/crates/audio/src/audio.rs @@ -1,19 +1,16 @@ -use anyhow::{Context as _, Result, anyhow}; -use collections::HashMap; -use gpui::{App, BorrowAppContext, Global}; -use rodio::{Decoder, OutputStream, OutputStreamBuilder, Source, source::Buffered}; -use settings::Settings; -use std::io::Cursor; +use assets::SoundRegistry; +use derive_more::{Deref, DerefMut}; +use gpui::{App, AssetSource, BorrowAppContext, Global}; +use rodio::{OutputStream, OutputStreamBuilder}; use util::ResultExt; -mod audio_settings; -pub use audio_settings::AudioSettings; +mod assets; -pub fn init(cx: &mut App) { - AudioSettings::register(cx); +pub fn init(source: impl AssetSource, cx: &mut App) { + SoundRegistry::set_global(source, cx); + cx.set_global(GlobalAudio(Audio::new())); } -#[derive(Copy, Clone, Eq, Hash, PartialEq)] pub enum Sound { Joined, Leave, @@ -41,12 +38,18 @@ impl Sound { #[derive(Default)] pub struct Audio { output_handle: Option, - source_cache: HashMap>>>>, } -impl Global for Audio {} +#[derive(Deref, DerefMut)] +struct GlobalAudio(Audio); + +impl Global for GlobalAudio {} impl Audio { + pub fn new() -> Self { + Self::default() + } + fn ensure_output_exists(&mut self) -> Option<&OutputStream> { if self.output_handle.is_none() { self.output_handle = OutputStreamBuilder::open_default_stream().log_err(); @@ -55,51 +58,26 @@ impl Audio { self.output_handle.as_ref() } - pub fn play_source( - source: impl rodio::Source + Send + 'static, - cx: &mut App, - ) -> anyhow::Result<()> { - cx.update_default_global(|this: &mut Self, _cx| { - let output_handle = this - .ensure_output_exists() - .ok_or_else(|| anyhow!("Could not open audio output"))?; - output_handle.mixer().add(source); - Ok(()) - }) - } - pub fn play_sound(sound: Sound, cx: &mut App) { - cx.update_default_global(|this: &mut Self, cx| { - let source = this.sound_source(sound, cx).log_err()?; + if !cx.has_global::() { + return; + } + + cx.update_global::(|this, cx| { let output_handle = this.ensure_output_exists()?; + let source = SoundRegistry::global(cx).get(sound.file()).log_err()?; output_handle.mixer().add(source); Some(()) }); } pub fn end_call(cx: &mut App) { - cx.update_default_global(|this: &mut Self, _cx| { + if !cx.has_global::() { + return; + } + + cx.update_global::(|this, _| { this.output_handle.take(); }); } - - fn sound_source(&mut self, sound: Sound, cx: &App) -> Result> { - if let Some(wav) = self.source_cache.get(&sound) { - return Ok(wav.clone()); - } - - let path = format!("sounds/{}.wav", sound.file()); - let bytes = cx - .asset_source() - .load(&path)? - .map(anyhow::Ok) - .with_context(|| format!("No asset available for path {path}"))?? - .into_owned(); - let cursor = Cursor::new(bytes); - let source = Decoder::new(cursor)?.buffered(); - - self.source_cache.insert(sound, source.clone()); - - Ok(source) - } } diff --git a/crates/audio/src/audio_settings.rs b/crates/audio/src/audio_settings.rs deleted file mode 100644 index 807179881c..0000000000 --- a/crates/audio/src/audio_settings.rs +++ /dev/null @@ -1,33 +0,0 @@ -use anyhow::Result; -use gpui::App; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; - -#[derive(Deserialize, Debug)] -pub struct AudioSettings { - /// Opt into the new audio system. - #[serde(rename = "experimental.rodio_audio", default)] - pub rodio_audio: bool, // default is false -} - -/// Configuration of audio in Zed. -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] -#[serde(default)] -pub struct AudioSettingsContent { - /// Whether to use the experimental audio system - #[serde(rename = "experimental.rodio_audio", default)] - pub rodio_audio: bool, -} - -impl Settings for AudioSettings { - const KEY: Option<&'static str> = Some("audio"); - - type FileContent = AudioSettingsContent; - - fn load(sources: SettingsSources, _cx: &mut App) -> Result { - sources.json_merge() - } - - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} -} diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 2bbe7dd1b5..f9b8a10610 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -66,8 +66,6 @@ pub static IMPERSONATE_LOGIN: LazyLock> = LazyLock::new(|| { .and_then(|s| if s.is_empty() { None } else { Some(s) }) }); -pub static USE_WEB_LOGIN: LazyLock = LazyLock::new(|| std::env::var("ZED_WEB_LOGIN").is_ok()); - pub static ADMIN_API_TOKEN: LazyLock> = LazyLock::new(|| { std::env::var("ZED_ADMIN_API_TOKEN") .ok() @@ -1394,13 +1392,11 @@ impl Client { if let Some((login, token)) = IMPERSONATE_LOGIN.as_ref().zip(ADMIN_API_TOKEN.as_ref()) { - if !*USE_WEB_LOGIN { - eprintln!("authenticate as admin {login}, {token}"); + eprintln!("authenticate as admin {login}, {token}"); - return this - .authenticate_as_admin(http, login.clone(), token.clone()) - .await; - } + return this + .authenticate_as_admin(http, login.clone(), token.clone()) + .await; } // Start an HTTP server to receive the redirect from Zed's sign-in page. diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index a5c1532c75..f3142a0af6 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -76,7 +76,7 @@ static ZED_CLIENT_CHECKSUM_SEED: LazyLock>> = LazyLock::new(|| { pub static MINIDUMP_ENDPOINT: LazyLock> = LazyLock::new(|| { option_env!("ZED_MINIDUMP_ENDPOINT") - .map(str::to_string) + .map(|s| s.to_owned()) .or_else(|| env::var("ZED_MINIDUMP_ENDPOINT").ok()) }); diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index d23eb37519..1f8174dbc3 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -46,6 +46,11 @@ impl ProjectId { } } +#[derive( + Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize, +)] +pub struct DevServerProjectId(pub u64); + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct ParticipantIndex(pub u32); diff --git a/crates/collab/migrations/20250821133754_add_orb_subscription_status_and_period_to_billing_subscriptions.sql b/crates/collab/migrations/20250821133754_add_orb_subscription_status_and_period_to_billing_subscriptions.sql deleted file mode 100644 index 89a42ab82b..0000000000 --- a/crates/collab/migrations/20250821133754_add_orb_subscription_status_and_period_to_billing_subscriptions.sql +++ /dev/null @@ -1,4 +0,0 @@ -alter table billing_subscriptions - add column orb_subscription_status text, - add column orb_current_billing_period_start_date timestamp without time zone, - add column orb_current_billing_period_end_date timestamp without time zone; diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 73f327166a..06eb68610f 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -400,8 +400,6 @@ impl Server { .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(multi_lsp_query) - .add_request_handler(lsp_query) - .add_message_handler(broadcast_project_message_from_host::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) .add_request_handler(forward_mutating_project_request::) @@ -912,9 +910,7 @@ impl Server { user_id=field::Empty, login=field::Empty, impersonator=field::Empty, - // todo(lsp) remove after Zed Stable hits v0.204.x multi_lsp_query_request=field::Empty, - lsp_query_request=field::Empty, release_channel=field::Empty, { TOTAL_DURATION_MS }=field::Empty, { PROCESSING_DURATION_MS }=field::Empty, @@ -2360,7 +2356,6 @@ where Ok(()) } -// todo(lsp) remove after Zed Stable hits v0.204.x async fn multi_lsp_query( request: MultiLspQuery, response: Response, @@ -2371,21 +2366,6 @@ async fn multi_lsp_query( forward_mutating_project_request(request, response, session).await } -async fn lsp_query( - request: proto::LspQuery, - response: Response, - session: MessageContext, -) -> Result<()> { - let (name, should_write) = request.query_name_and_write_permissions(); - tracing::Span::current().record("lsp_query_request", name); - tracing::info!("lsp_query message received"); - if should_write { - forward_mutating_project_request(request, response, session).await - } else { - forward_read_only_project_request(request, response, session).await - } -} - /// Notify other participants that a new buffer has been created async fn create_buffer_for_peer( request: proto::CreateBufferForPeer, diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 59d66f1821..1b0c581983 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -15,14 +15,13 @@ use editor::{ }, }; use fs::Fs; -use futures::{SinkExt, StreamExt, channel::mpsc, lock::Mutex}; +use futures::{StreamExt, lock::Mutex}; use gpui::{App, Rgba, TestAppContext, UpdateGlobal, VisualContext, VisualTestContext}; use indoc::indoc; use language::{ FakeLspAdapter, language_settings::{AllLanguageSettings, InlayHintSettings}, }; -use lsp::LSP_REQUEST_TIMEOUT; use project::{ ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT, lsp_store::lsp_ext_command::{ExpandedMacro, LspExtExpandMacro}, @@ -1018,211 +1017,6 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T }) } -#[gpui::test] -async fn test_slow_lsp_server(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { - let mut server = TestServer::start(cx_a.executor()).await; - let client_a = server.create_client(cx_a, "user_a").await; - let client_b = server.create_client(cx_b, "user_b").await; - server - .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)]) - .await; - let active_call_a = cx_a.read(ActiveCall::global); - cx_b.update(editor::init); - - let command_name = "test_command"; - let capabilities = lsp::ServerCapabilities { - code_lens_provider: Some(lsp::CodeLensOptions { - resolve_provider: None, - }), - execute_command_provider: Some(lsp::ExecuteCommandOptions { - commands: vec![command_name.to_string()], - ..lsp::ExecuteCommandOptions::default() - }), - ..lsp::ServerCapabilities::default() - }; - client_a.language_registry().add(rust_lang()); - let mut fake_language_servers = client_a.language_registry().register_fake_lsp( - "Rust", - FakeLspAdapter { - capabilities: capabilities.clone(), - ..FakeLspAdapter::default() - }, - ); - client_b.language_registry().add(rust_lang()); - client_b.language_registry().register_fake_lsp_adapter( - "Rust", - FakeLspAdapter { - capabilities, - ..FakeLspAdapter::default() - }, - ); - - client_a - .fs() - .insert_tree( - path!("/dir"), - json!({ - "one.rs": "const ONE: usize = 1;" - }), - ) - .await; - let (project_a, worktree_id) = client_a.build_local_project(path!("/dir"), cx_a).await; - let project_id = active_call_a - .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) - .await - .unwrap(); - let project_b = client_b.join_remote_project(project_id, cx_b).await; - - let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); - let editor_b = workspace_b - .update_in(cx_b, |workspace, window, cx| { - workspace.open_path((worktree_id, "one.rs"), None, true, window, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - let (lsp_store_b, buffer_b) = editor_b.update(cx_b, |editor, cx| { - let lsp_store = editor.project().unwrap().read(cx).lsp_store(); - let buffer = editor.buffer().read(cx).as_singleton().unwrap(); - (lsp_store, buffer) - }); - let fake_language_server = fake_language_servers.next().await.unwrap(); - cx_a.run_until_parked(); - cx_b.run_until_parked(); - - let long_request_time = LSP_REQUEST_TIMEOUT / 2; - let (request_started_tx, mut request_started_rx) = mpsc::unbounded(); - let requests_started = Arc::new(AtomicUsize::new(0)); - let requests_completed = Arc::new(AtomicUsize::new(0)); - let _lens_requests = fake_language_server - .set_request_handler::({ - let request_started_tx = request_started_tx.clone(); - let requests_started = requests_started.clone(); - let requests_completed = requests_completed.clone(); - move |params, cx| { - let mut request_started_tx = request_started_tx.clone(); - let requests_started = requests_started.clone(); - let requests_completed = requests_completed.clone(); - async move { - assert_eq!( - params.text_document.uri.as_str(), - uri!("file:///dir/one.rs") - ); - requests_started.fetch_add(1, atomic::Ordering::Release); - request_started_tx.send(()).await.unwrap(); - cx.background_executor().timer(long_request_time).await; - let i = requests_completed.fetch_add(1, atomic::Ordering::Release) + 1; - Ok(Some(vec![lsp::CodeLens { - range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 9)), - command: Some(lsp::Command { - title: format!("LSP Command {i}"), - command: command_name.to_string(), - arguments: None, - }), - data: None, - }])) - } - } - }); - - // Move cursor to a location, this should trigger the code lens call. - editor_b.update_in(cx_b, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges([7..7]) - }); - }); - let () = request_started_rx.next().await.unwrap(); - assert_eq!( - requests_started.load(atomic::Ordering::Acquire), - 1, - "Selection change should have initiated the first request" - ); - assert_eq!( - requests_completed.load(atomic::Ordering::Acquire), - 0, - "Slow requests should be running still" - ); - let _first_task = lsp_store_b.update(cx_b, |lsp_store, cx| { - lsp_store - .forget_code_lens_task(buffer_b.read(cx).remote_id()) - .expect("Should have the fetch task started") - }); - - editor_b.update_in(cx_b, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges([1..1]) - }); - }); - let () = request_started_rx.next().await.unwrap(); - assert_eq!( - requests_started.load(atomic::Ordering::Acquire), - 2, - "Selection change should have initiated the second request" - ); - assert_eq!( - requests_completed.load(atomic::Ordering::Acquire), - 0, - "Slow requests should be running still" - ); - let _second_task = lsp_store_b.update(cx_b, |lsp_store, cx| { - lsp_store - .forget_code_lens_task(buffer_b.read(cx).remote_id()) - .expect("Should have the fetch task started for the 2nd time") - }); - - editor_b.update_in(cx_b, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges([2..2]) - }); - }); - let () = request_started_rx.next().await.unwrap(); - assert_eq!( - requests_started.load(atomic::Ordering::Acquire), - 3, - "Selection change should have initiated the third request" - ); - assert_eq!( - requests_completed.load(atomic::Ordering::Acquire), - 0, - "Slow requests should be running still" - ); - - _first_task.await.unwrap(); - _second_task.await.unwrap(); - cx_b.run_until_parked(); - assert_eq!( - requests_started.load(atomic::Ordering::Acquire), - 3, - "No selection changes should trigger no more code lens requests" - ); - assert_eq!( - requests_completed.load(atomic::Ordering::Acquire), - 3, - "After enough time, all 3 LSP requests should have been served by the language server" - ); - let resulting_lens_actions = editor_b - .update(cx_b, |editor, cx| { - let lsp_store = editor.project().unwrap().read(cx).lsp_store(); - lsp_store.update(cx, |lsp_store, cx| { - lsp_store.code_lens_actions(&buffer_b, cx) - }) - }) - .await - .unwrap() - .unwrap(); - assert_eq!( - resulting_lens_actions.len(), - 1, - "Should have fetched one code lens action, but got: {resulting_lens_actions:?}" - ); - assert_eq!( - resulting_lens_actions.first().unwrap().lsp_action.title(), - "LSP Command 3", - "Only the final code lens action should be in the data" - ) -} - #[gpui::test(iterations = 10)] async fn test_language_server_statuses(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { let mut server = TestServer::start(cx_a.executor()).await; diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 1e0c915bcb..d9fd8ffeb2 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -970,7 +970,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T // the follow. workspace_b.update_in(cx_b, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.activate_previous_item(&Default::default(), window, cx); + pane.activate_prev_item(true, window, cx); }); }); executor.run_until_parked(); @@ -1073,7 +1073,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T // Client A cycles through some tabs. workspace_a.update_in(cx_a, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.activate_previous_item(&Default::default(), window, cx); + pane.activate_prev_item(true, window, cx); }); }); executor.run_until_parked(); @@ -1117,7 +1117,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T workspace_a.update_in(cx_a, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.activate_previous_item(&Default::default(), window, cx); + pane.activate_prev_item(true, window, cx); }); }); executor.run_until_parked(); @@ -1164,7 +1164,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T workspace_a.update_in(cx_a, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.activate_previous_item(&Default::default(), window, cx); + pane.activate_prev_item(true, window, cx); }); }); executor.run_until_parked(); diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 5c73253048..e01736f0ef 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -4850,7 +4850,6 @@ async fn test_definition( let definitions_1 = project_b .update(cx_b, |p, cx| p.definitions(&buffer_b, 23, cx)) .await - .unwrap() .unwrap(); cx_b.read(|cx| { assert_eq!( @@ -4886,7 +4885,6 @@ async fn test_definition( let definitions_2 = project_b .update(cx_b, |p, cx| p.definitions(&buffer_b, 33, cx)) .await - .unwrap() .unwrap(); cx_b.read(|cx| { assert_eq!(definitions_2.len(), 1); @@ -4924,7 +4922,6 @@ async fn test_definition( let type_definitions = project_b .update(cx_b, |p, cx| p.type_definitions(&buffer_b, 7, cx)) .await - .unwrap() .unwrap(); cx_b.read(|cx| { assert_eq!( @@ -5063,7 +5060,7 @@ async fn test_references( ]))) .unwrap(); - let references = references.await.unwrap().unwrap(); + let references = references.await.unwrap(); executor.run_until_parked(); project_b.read_with(cx_b, |project, cx| { // User is informed that a request is no longer pending. @@ -5107,7 +5104,7 @@ async fn test_references( lsp_response_tx .unbounded_send(Err(anyhow!("can't find references"))) .unwrap(); - assert_eq!(references.await.unwrap().unwrap(), []); + assert_eq!(references.await.unwrap(), []); // User is informed that the request is no longer pending. executor.run_until_parked(); @@ -5508,8 +5505,7 @@ async fn test_lsp_hover( // Request hover information as the guest. let mut hovers = project_b .update(cx_b, |p, cx| p.hover(&buffer_b, 22, cx)) - .await - .unwrap(); + .await; assert_eq!( hovers.len(), 2, @@ -5768,7 +5764,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it( definitions = project_b.update(cx_b, |p, cx| p.definitions(&buffer_b1, 23, cx)); } - let definitions = definitions.await.unwrap().unwrap(); + let definitions = definitions.await.unwrap(); assert_eq!( definitions.len(), 1, diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index d85a6610a5..cd37549783 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2905,8 +2905,6 @@ impl CollabPanel { h_flex().absolute().right(rems(0.)).h_full().child( h_flex() .h_full() - .bg(cx.theme().colors().background) - .rounded_l_sm() .gap_1() .px_1() .child( @@ -2922,7 +2920,8 @@ impl CollabPanel { .on_click(cx.listener(move |this, _, window, cx| { this.join_channel_chat(channel_id, window, cx) })) - .tooltip(Tooltip::text("Open channel chat")), + .tooltip(Tooltip::text("Open channel chat")) + .visible_on_hover(""), ) .child( IconButton::new("channel_notes", IconName::Reader) @@ -2937,9 +2936,9 @@ impl CollabPanel { .on_click(cx.listener(move |this, _, window, cx| { this.open_channel_notes(channel_id, window, cx) })) - .tooltip(Tooltip::text("Open channel notes")), - ) - .visible_on_hover(""), + .tooltip(Tooltip::text("Open channel notes")) + .visible_on_hover(""), + ), ), ) .tooltip({ diff --git a/crates/command_palette/src/persistence.rs b/crates/command_palette/src/persistence.rs index 01cf403083..5be97c36bc 100644 --- a/crates/command_palette/src/persistence.rs +++ b/crates/command_palette/src/persistence.rs @@ -1,10 +1,7 @@ use anyhow::Result; use db::{ - query, - sqlez::{ - bindable::Column, domain::Domain, statement::Statement, - thread_safe_connection::ThreadSafeConnection, - }, + define_connection, query, + sqlez::{bindable::Column, statement::Statement}, sqlez_macros::sql, }; use serde::{Deserialize, Serialize}; @@ -53,11 +50,8 @@ impl Column for SerializedCommandInvocation { } } -pub struct CommandPaletteDB(ThreadSafeConnection); - -impl Domain for CommandPaletteDB { - const NAME: &str = stringify!(CommandPaletteDB); - const MIGRATIONS: &[&str] = &[sql!( +define_connection!(pub static ref COMMAND_PALETTE_HISTORY: CommandPaletteDB<()> = + &[sql!( CREATE TABLE IF NOT EXISTS command_invocations( id INTEGER PRIMARY KEY AUTOINCREMENT, command_name TEXT NOT NULL, @@ -65,9 +59,7 @@ impl Domain for CommandPaletteDB { last_invoked INTEGER DEFAULT (unixepoch()) NOT NULL ) STRICT; )]; -} - -db::static_connection!(COMMAND_PALETTE_HISTORY, CommandPaletteDB, []); +); impl CommandPaletteDB { pub async fn write_command_invocation( diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index 52d75175e5..9308500ed4 100644 --- a/crates/copilot/src/copilot_completion_provider.rs +++ b/crates/copilot/src/copilot_completion_provider.rs @@ -301,7 +301,6 @@ mod tests { init_test(cx, |settings| { settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Disabled, - words_min_length: 0, lsp: true, lsp_fetch_timeout_ms: 0, lsp_insert_mode: LspInsertMode::Insert, @@ -534,7 +533,6 @@ mod tests { init_test(cx, |settings| { settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Disabled, - words_min_length: 0, lsp: true, lsp_fetch_timeout_ms: 0, lsp_insert_mode: LspInsertMode::Insert, diff --git a/crates/crashes/Cargo.toml b/crates/crashes/Cargo.toml index 370f0bb5f6..f12913d1cb 100644 --- a/crates/crashes/Cargo.toml +++ b/crates/crashes/Cargo.toml @@ -6,7 +6,6 @@ edition.workspace = true license = "GPL-3.0-or-later" [dependencies] -bincode.workspace = true crash-handler.workspace = true log.workspace = true minidumper.workspace = true @@ -15,7 +14,6 @@ release_channel.workspace = true smol.workspace = true serde.workspace = true serde_json.workspace = true -system_specs.workspace = true workspace-hack.workspace = true [target.'cfg(target_os = "macos")'.dependencies] diff --git a/crates/crashes/src/crashes.rs b/crates/crashes/src/crashes.rs index f7bc96bff9..b1afc5ae45 100644 --- a/crates/crashes/src/crashes.rs +++ b/crates/crashes/src/crashes.rs @@ -127,7 +127,6 @@ unsafe fn suspend_all_other_threads() { pub struct CrashServer { initialization_params: OnceLock, panic_info: OnceLock, - active_gpu: OnceLock, has_connection: Arc, } @@ -136,8 +135,6 @@ pub struct CrashInfo { pub init: InitCrashHandler, pub panic: Option, pub minidump_error: Option, - pub gpus: Vec, - pub active_gpu: Option, } #[derive(Debug, Deserialize, Serialize, Clone)] @@ -146,6 +143,7 @@ pub struct InitCrashHandler { pub zed_version: String, pub release_channel: String, pub commit_sha: String, + // pub gpu: String, } #[derive(Deserialize, Serialize, Debug, Clone)] @@ -180,18 +178,6 @@ impl minidumper::ServerHandler for CrashServer { Err(e) => Some(format!("{e:?}")), }; - #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] - let gpus = vec![]; - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - let gpus = match system_specs::read_gpu_info_from_sys_class_drm() { - Ok(gpus) => gpus, - Err(err) => { - log::warn!("Failed to collect GPU information for crash report: {err}"); - vec![] - } - }; - let crash_info = CrashInfo { init: self .initialization_params @@ -200,8 +186,6 @@ impl minidumper::ServerHandler for CrashServer { .clone(), panic: self.panic_info.get().cloned(), minidump_error, - active_gpu: self.active_gpu.get().cloned(), - gpus, }; let crash_data_path = paths::logs_dir() @@ -227,13 +211,6 @@ impl minidumper::ServerHandler for CrashServer { serde_json::from_slice::(&buffer).expect("invalid panic data"); self.panic_info.set(panic_data).expect("already panicked"); } - 3 => { - let gpu_specs: system_specs::GpuSpecs = - bincode::deserialize(&buffer).expect("gpu specs"); - self.active_gpu - .set(gpu_specs) - .expect("already set active gpu"); - } _ => { panic!("invalid message kind"); } @@ -310,7 +287,6 @@ pub fn crash_server(socket: &Path) { initialization_params: OnceLock::new(), panic_info: OnceLock::new(), has_connection, - active_gpu: OnceLock::new(), }), &shutdown, Some(CRASH_HANDLER_PING_TIMEOUT), diff --git a/crates/db/src/db.rs b/crates/db/src/db.rs index 0802bd8bb7..8b790cbec8 100644 --- a/crates/db/src/db.rs +++ b/crates/db/src/db.rs @@ -110,14 +110,11 @@ pub async fn open_test_db(db_name: &str) -> ThreadSafeConnection { } /// Implements a basic DB wrapper for a given domain -/// -/// Arguments: -/// - static variable name for connection -/// - type of connection wrapper -/// - dependencies, whose migrations should be run prior to this domain's migrations #[macro_export] -macro_rules! static_connection { - ($id:ident, $t:ident, [ $($d:ty),* ] $(, $global:ident)?) => { +macro_rules! define_connection { + (pub static ref $id:ident: $t:ident<()> = $migrations:expr; $($global:ident)?) => { + pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection); + impl ::std::ops::Deref for $t { type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection; @@ -126,6 +123,16 @@ macro_rules! static_connection { } } + impl $crate::sqlez::domain::Domain for $t { + fn name() -> &'static str { + stringify!($t) + } + + fn migrations() -> &'static [&'static str] { + $migrations + } + } + impl $t { #[cfg(any(test, feature = "test-support"))] pub async fn open_test_db(name: &'static str) -> Self { @@ -135,8 +142,7 @@ macro_rules! static_connection { #[cfg(any(test, feature = "test-support"))] pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| { - #[allow(unused_parens)] - $t($crate::smol::block_on($crate::open_test_db::<($($d,)* $t)>(stringify!($id)))) + $t($crate::smol::block_on($crate::open_test_db::<$t>(stringify!($id)))) }); #[cfg(not(any(test, feature = "test-support")))] @@ -147,10 +153,46 @@ macro_rules! static_connection { } else { $crate::RELEASE_CHANNEL.dev_name() }; - #[allow(unused_parens)] - $t($crate::smol::block_on($crate::open_db::<($($d,)* $t)>(db_dir, scope))) + $t($crate::smol::block_on($crate::open_db::<$t>(db_dir, scope))) }); - } + }; + (pub static ref $id:ident: $t:ident<$($d:ty),+> = $migrations:expr; $($global:ident)?) => { + pub struct $t($crate::sqlez::thread_safe_connection::ThreadSafeConnection); + + impl ::std::ops::Deref for $t { + type Target = $crate::sqlez::thread_safe_connection::ThreadSafeConnection; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl $crate::sqlez::domain::Domain for $t { + fn name() -> &'static str { + stringify!($t) + } + + fn migrations() -> &'static [&'static str] { + $migrations + } + } + + #[cfg(any(test, feature = "test-support"))] + pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| { + $t($crate::smol::block_on($crate::open_test_db::<($($d),+, $t)>(stringify!($id)))) + }); + + #[cfg(not(any(test, feature = "test-support")))] + pub static $id: std::sync::LazyLock<$t> = std::sync::LazyLock::new(|| { + let db_dir = $crate::database_dir(); + let scope = if false $(|| stringify!($global) == "global")? { + "global" + } else { + $crate::RELEASE_CHANNEL.dev_name() + }; + $t($crate::smol::block_on($crate::open_db::<($($d),+, $t)>(db_dir, scope))) + }); + }; } pub fn write_and_log(cx: &App, db_write: impl FnOnce() -> F + Send + 'static) @@ -177,12 +219,17 @@ mod tests { enum BadDB {} impl Domain for BadDB { - const NAME: &str = "db_tests"; - const MIGRATIONS: &[&str] = &[ - sql!(CREATE TABLE test(value);), - // failure because test already exists - sql!(CREATE TABLE test(value);), - ]; + fn name() -> &'static str { + "db_tests" + } + + fn migrations() -> &'static [&'static str] { + &[ + sql!(CREATE TABLE test(value);), + // failure because test already exists + sql!(CREATE TABLE test(value);), + ] + } } let tempdir = tempfile::Builder::new() @@ -204,15 +251,25 @@ mod tests { enum CorruptedDB {} impl Domain for CorruptedDB { - const NAME: &str = "db_tests"; - const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)]; + fn name() -> &'static str { + "db_tests" + } + + fn migrations() -> &'static [&'static str] { + &[sql!(CREATE TABLE test(value);)] + } } enum GoodDB {} impl Domain for GoodDB { - const NAME: &str = "db_tests"; //Notice same name - const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)]; + fn name() -> &'static str { + "db_tests" //Notice same name + } + + fn migrations() -> &'static [&'static str] { + &[sql!(CREATE TABLE test2(value);)] //But different migration + } } let tempdir = tempfile::Builder::new() @@ -248,16 +305,25 @@ mod tests { enum CorruptedDB {} impl Domain for CorruptedDB { - const NAME: &str = "db_tests"; + fn name() -> &'static str { + "db_tests" + } - const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test(value);)]; + fn migrations() -> &'static [&'static str] { + &[sql!(CREATE TABLE test(value);)] + } } enum GoodDB {} impl Domain for GoodDB { - const NAME: &str = "db_tests"; //Notice same name - const MIGRATIONS: &[&str] = &[sql!(CREATE TABLE test2(value);)]; // But different migration + fn name() -> &'static str { + "db_tests" //Notice same name + } + + fn migrations() -> &'static [&'static str] { + &[sql!(CREATE TABLE test2(value);)] //But different migration + } } let tempdir = tempfile::Builder::new() diff --git a/crates/db/src/kvp.rs b/crates/db/src/kvp.rs index 8ea877b35b..256b789c9b 100644 --- a/crates/db/src/kvp.rs +++ b/crates/db/src/kvp.rs @@ -2,26 +2,16 @@ use gpui::App; use sqlez_macros::sql; use util::ResultExt as _; -use crate::{ - query, - sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, - write_and_log, -}; +use crate::{define_connection, query, write_and_log}; -pub struct KeyValueStore(crate::sqlez::thread_safe_connection::ThreadSafeConnection); - -impl Domain for KeyValueStore { - const NAME: &str = stringify!(KeyValueStore); - - const MIGRATIONS: &[&str] = &[sql!( +define_connection!(pub static ref KEY_VALUE_STORE: KeyValueStore<()> = + &[sql!( CREATE TABLE IF NOT EXISTS kv_store( key TEXT PRIMARY KEY, value TEXT NOT NULL ) STRICT; )]; -} - -crate::static_connection!(KEY_VALUE_STORE, KeyValueStore, []); +); pub trait Dismissable { const KEY: &'static str; @@ -101,19 +91,15 @@ mod tests { } } -pub struct GlobalKeyValueStore(ThreadSafeConnection); - -impl Domain for GlobalKeyValueStore { - const NAME: &str = stringify!(GlobalKeyValueStore); - const MIGRATIONS: &[&str] = &[sql!( +define_connection!(pub static ref GLOBAL_KEY_VALUE_STORE: GlobalKeyValueStore<()> = + &[sql!( CREATE TABLE IF NOT EXISTS kv_store( key TEXT PRIMARY KEY, value TEXT NOT NULL ) STRICT; )]; -} - -crate::static_connection!(GLOBAL_KEY_VALUE_STORE, GlobalKeyValueStore, [], global); + global +); impl GlobalKeyValueStore { query! { diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 9991395f35..0574091851 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -916,10 +916,7 @@ impl RunningState { let task_store = project.read(cx).task_store().downgrade(); let weak_project = project.downgrade(); let weak_workspace = workspace.downgrade(); - let ssh_info = project - .read(cx) - .ssh_client() - .and_then(|it| it.read(cx).ssh_info()); + let is_local = project.read(cx).is_local(); cx.spawn_in(window, async move |this, cx| { let DebugScenario { @@ -1003,7 +1000,7 @@ impl RunningState { None }; - let builder = ShellBuilder::new(ssh_info.as_ref().map(|info| &*info.shell), &task.resolved.shell); + let builder = ShellBuilder::new(is_local, &task.resolved.shell); let command_label = builder.command_label(&task.resolved.command_label); let (command, args) = builder.build(task.resolved.command.clone(), &task.resolved.args); diff --git a/crates/diagnostics/Cargo.toml b/crates/diagnostics/Cargo.toml index fd678078e8..53b5792e10 100644 --- a/crates/diagnostics/Cargo.toml +++ b/crates/diagnostics/Cargo.toml @@ -18,6 +18,7 @@ collections.workspace = true component.workspace = true ctor.workspace = true editor.workspace = true +futures.workspace = true gpui.workspace = true indoc.workspace = true language.workspace = true diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 1c27e820a0..2e20118381 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -13,6 +13,7 @@ use editor::{ DEFAULT_MULTIBUFFER_CONTEXT, Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey, display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId}, }; +use futures::future::join_all; use gpui::{ AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable, Global, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, @@ -23,6 +24,7 @@ use language::{ }; use project::{ DiagnosticSummary, Project, ProjectPath, + lsp_store::rust_analyzer_ext::{cancel_flycheck, run_flycheck}, project_settings::{DiagnosticSeverity, ProjectSettings}, }; use settings::Settings; @@ -77,10 +79,17 @@ pub(crate) struct ProjectDiagnosticsEditor { paths_to_update: BTreeSet, include_warnings: bool, update_excerpts_task: Option>>, + cargo_diagnostics_fetch: CargoDiagnosticsFetchState, diagnostic_summary_update: Task<()>, _subscription: Subscription, } +struct CargoDiagnosticsFetchState { + fetch_task: Option>, + cancel_task: Option>, + diagnostic_sources: Arc>, +} + impl EventEmitter for ProjectDiagnosticsEditor {} const DIAGNOSTICS_UPDATE_DELAY: Duration = Duration::from_millis(50); @@ -251,7 +260,11 @@ impl ProjectDiagnosticsEditor { ) }); this.diagnostics.clear(); - this.update_all_excerpts(window, cx); + this.update_all_diagnostics(false, window, cx); + }) + .detach(); + cx.observe_release(&cx.entity(), |editor, _, cx| { + editor.stop_cargo_diagnostics_fetch(cx); }) .detach(); @@ -268,10 +281,15 @@ impl ProjectDiagnosticsEditor { editor, paths_to_update: Default::default(), update_excerpts_task: None, + cargo_diagnostics_fetch: CargoDiagnosticsFetchState { + fetch_task: None, + cancel_task: None, + diagnostic_sources: Arc::new(Vec::new()), + }, diagnostic_summary_update: Task::ready(()), _subscription: project_event_subscription, }; - this.update_all_excerpts(window, cx); + this.update_all_diagnostics(true, window, cx); this } @@ -355,10 +373,20 @@ impl ProjectDiagnosticsEditor { window: &mut Window, cx: &mut Context, ) { - if self.update_excerpts_task.is_some() { + let fetch_cargo_diagnostics = ProjectSettings::get_global(cx) + .diagnostics + .fetch_cargo_diagnostics(); + + if fetch_cargo_diagnostics { + if self.cargo_diagnostics_fetch.fetch_task.is_some() { + self.stop_cargo_diagnostics_fetch(cx); + } else { + self.update_all_diagnostics(false, window, cx); + } + } else if self.update_excerpts_task.is_some() { self.update_excerpts_task = None; } else { - self.update_all_excerpts(window, cx); + self.update_all_diagnostics(false, window, cx); } cx.notify(); } @@ -376,6 +404,73 @@ impl ProjectDiagnosticsEditor { } } + fn update_all_diagnostics( + &mut self, + first_launch: bool, + window: &mut Window, + cx: &mut Context, + ) { + let cargo_diagnostics_sources = self.cargo_diagnostics_sources(cx); + if cargo_diagnostics_sources.is_empty() { + self.update_all_excerpts(window, cx); + } else if first_launch && !self.summary.is_empty() { + self.update_all_excerpts(window, cx); + } else { + self.fetch_cargo_diagnostics(Arc::new(cargo_diagnostics_sources), cx); + } + } + + fn fetch_cargo_diagnostics( + &mut self, + diagnostics_sources: Arc>, + cx: &mut Context, + ) { + let project = self.project.clone(); + self.cargo_diagnostics_fetch.cancel_task = None; + self.cargo_diagnostics_fetch.fetch_task = None; + self.cargo_diagnostics_fetch.diagnostic_sources = diagnostics_sources.clone(); + if self.cargo_diagnostics_fetch.diagnostic_sources.is_empty() { + return; + } + + self.cargo_diagnostics_fetch.fetch_task = Some(cx.spawn(async move |editor, cx| { + let mut fetch_tasks = Vec::new(); + for buffer_path in diagnostics_sources.iter().cloned() { + if cx + .update(|cx| { + fetch_tasks.push(run_flycheck(project.clone(), buffer_path, cx)); + }) + .is_err() + { + break; + } + } + + let _ = join_all(fetch_tasks).await; + editor + .update(cx, |editor, _| { + editor.cargo_diagnostics_fetch.fetch_task = None; + }) + .ok(); + })); + } + + fn stop_cargo_diagnostics_fetch(&mut self, cx: &mut App) { + self.cargo_diagnostics_fetch.fetch_task = None; + let mut cancel_gasks = Vec::new(); + for buffer_path in std::mem::take(&mut self.cargo_diagnostics_fetch.diagnostic_sources) + .iter() + .cloned() + { + cancel_gasks.push(cancel_flycheck(self.project.clone(), buffer_path, cx)); + } + + self.cargo_diagnostics_fetch.cancel_task = Some(cx.background_spawn(async move { + let _ = join_all(cancel_gasks).await; + log::info!("Finished fetching cargo diagnostics"); + })); + } + /// Enqueue an update of all excerpts. Updates all paths that either /// currently have diagnostics or are currently present in this view. fn update_all_excerpts(&mut self, window: &mut Window, cx: &mut Context) { @@ -600,6 +695,30 @@ impl ProjectDiagnosticsEditor { }) }) } + + pub fn cargo_diagnostics_sources(&self, cx: &App) -> Vec { + let fetch_cargo_diagnostics = ProjectSettings::get_global(cx) + .diagnostics + .fetch_cargo_diagnostics(); + if !fetch_cargo_diagnostics { + return Vec::new(); + } + self.project + .read(cx) + .worktrees(cx) + .filter_map(|worktree| { + let _cargo_toml_entry = worktree.read(cx).entry_for_path("Cargo.toml")?; + let rust_file_entry = worktree.read(cx).entries(false, 0).find(|entry| { + entry + .path + .extension() + .and_then(|extension| extension.to_str()) + == Some("rs") + })?; + self.project.read(cx).path_for_entry(rust_file_entry.id, cx) + }) + .collect() + } } impl Focusable for ProjectDiagnosticsEditor { diff --git a/crates/diagnostics/src/toolbar_controls.rs b/crates/diagnostics/src/toolbar_controls.rs index 404db39164..e77b80115f 100644 --- a/crates/diagnostics/src/toolbar_controls.rs +++ b/crates/diagnostics/src/toolbar_controls.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use crate::{ProjectDiagnosticsEditor, ToggleDiagnosticsRefresh}; use gpui::{Context, Entity, EventEmitter, ParentElement, Render, WeakEntity, Window}; use ui::prelude::*; @@ -13,18 +15,26 @@ impl Render for ToolbarControls { let mut include_warnings = false; let mut has_stale_excerpts = false; let mut is_updating = false; + let cargo_diagnostics_sources = Arc::new(self.diagnostics().map_or(Vec::new(), |editor| { + editor.read(cx).cargo_diagnostics_sources(cx) + })); + let fetch_cargo_diagnostics = !cargo_diagnostics_sources.is_empty(); if let Some(editor) = self.diagnostics() { let diagnostics = editor.read(cx); include_warnings = diagnostics.include_warnings; has_stale_excerpts = !diagnostics.paths_to_update.is_empty(); - is_updating = diagnostics.update_excerpts_task.is_some() - || diagnostics - .project - .read(cx) - .language_servers_running_disk_based_diagnostics(cx) - .next() - .is_some(); + is_updating = if fetch_cargo_diagnostics { + diagnostics.cargo_diagnostics_fetch.fetch_task.is_some() + } else { + diagnostics.update_excerpts_task.is_some() + || diagnostics + .project + .read(cx) + .language_servers_running_disk_based_diagnostics(cx) + .next() + .is_some() + }; } let tooltip = if include_warnings { @@ -54,6 +64,7 @@ impl Render for ToolbarControls { .on_click(cx.listener(move |toolbar_controls, _, _, cx| { if let Some(diagnostics) = toolbar_controls.diagnostics() { diagnostics.update(cx, |diagnostics, cx| { + diagnostics.stop_cargo_diagnostics_fetch(cx); diagnostics.update_excerpts_task = None; cx.notify(); }); @@ -65,7 +76,7 @@ impl Render for ToolbarControls { IconButton::new("refresh-diagnostics", IconName::ArrowCircle) .icon_color(Color::Info) .shape(IconButtonShape::Square) - .disabled(!has_stale_excerpts) + .disabled(!has_stale_excerpts && !fetch_cargo_diagnostics) .tooltip(Tooltip::for_action_title( "Refresh diagnostics", &ToggleDiagnosticsRefresh, @@ -73,8 +84,17 @@ impl Render for ToolbarControls { .on_click(cx.listener({ move |toolbar_controls, _, window, cx| { if let Some(diagnostics) = toolbar_controls.diagnostics() { + let cargo_diagnostics_sources = + Arc::clone(&cargo_diagnostics_sources); diagnostics.update(cx, move |diagnostics, cx| { - diagnostics.update_all_excerpts(window, cx); + if fetch_cargo_diagnostics { + diagnostics.fetch_cargo_diagnostics( + cargo_diagnostics_sources, + cx, + ); + } else { + diagnostics.update_all_excerpts(window, cx); + } }); } } diff --git a/crates/docs_preprocessor/src/main.rs b/crates/docs_preprocessor/src/main.rs index c8c3dc54b7..33158577c4 100644 --- a/crates/docs_preprocessor/src/main.rs +++ b/crates/docs_preprocessor/src/main.rs @@ -19,10 +19,6 @@ static KEYMAP_LINUX: LazyLock = LazyLock::new(|| { load_keymap("keymaps/default-linux.json").expect("Failed to load Linux keymap") }); -static KEYMAP_WINDOWS: LazyLock = LazyLock::new(|| { - load_keymap("keymaps/default-windows.json").expect("Failed to load Windows keymap") -}); - static ALL_ACTIONS: LazyLock> = LazyLock::new(dump_all_gpui_actions); const FRONT_MATTER_COMMENT: &str = ""; @@ -103,7 +99,6 @@ fn handle_preprocessing() -> Result<()> { let mut errors = HashSet::::new(); handle_frontmatter(&mut book, &mut errors); - template_big_table_of_actions(&mut book); template_and_validate_keybindings(&mut book, &mut errors); template_and_validate_actions(&mut book, &mut errors); @@ -152,18 +147,6 @@ fn handle_frontmatter(book: &mut Book, errors: &mut HashSet) }); } -fn template_big_table_of_actions(book: &mut Book) { - for_each_chapter_mut(book, |chapter| { - let needle = "{#ACTIONS_TABLE#}"; - if let Some(start) = chapter.content.rfind(needle) { - chapter.content.replace_range( - start..start + needle.len(), - &generate_big_table_of_actions(), - ); - } - }); -} - fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet) { let regex = Regex::new(r"\{#kb (.*?)\}").unwrap(); @@ -220,7 +203,6 @@ fn find_binding(os: &str, action: &str) -> Option { let keymap = match os { "macos" => &KEYMAP_MACOS, "linux" | "freebsd" => &KEYMAP_LINUX, - "windows" => &KEYMAP_WINDOWS, _ => unreachable!("Not a valid OS: {}", os), }; @@ -295,7 +277,6 @@ struct ActionDef { name: &'static str, human_name: String, deprecated_aliases: &'static [&'static str], - docs: Option<&'static str>, } fn dump_all_gpui_actions() -> Vec { @@ -304,7 +285,6 @@ fn dump_all_gpui_actions() -> Vec { name: action.name, human_name: command_palette::humanize_action_name(action.name), deprecated_aliases: action.deprecated_aliases, - docs: action.documentation, }) .collect::>(); @@ -438,54 +418,3 @@ fn title_regex() -> &'static Regex { static TITLE_REGEX: OnceLock = OnceLock::new(); TITLE_REGEX.get_or_init(|| Regex::new(r"\s*(.*?)\s*").unwrap()) } - -fn generate_big_table_of_actions() -> String { - let actions = &*ALL_ACTIONS; - let mut output = String::new(); - - let mut actions_sorted = actions.iter().collect::>(); - actions_sorted.sort_by_key(|a| a.name); - - // Start the definition list with custom styling for better spacing - output.push_str("
\n"); - - for action in actions_sorted.into_iter() { - // Add the humanized action name as the term with margin - output.push_str( - "
", - ); - output.push_str(&action.human_name); - output.push_str("
\n"); - - // Add the definition with keymap name and description - output.push_str("
\n"); - - // Add the description, escaping HTML if needed - if let Some(description) = action.docs { - output.push_str( - &description - .replace("&", "&") - .replace("<", "<") - .replace(">", ">"), - ); - output.push_str("
\n"); - } - output.push_str("Keymap Name: "); - output.push_str(action.name); - output.push_str("
\n"); - if !action.deprecated_aliases.is_empty() { - output.push_str("Deprecated Aliases:"); - for alias in action.deprecated_aliases.iter() { - output.push_str(""); - output.push_str(alias); - output.push_str(", "); - } - } - output.push_str("\n
\n"); - } - - // Close the definition list - output.push_str("
\n"); - - output -} diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 80680ae9c0..6b9a6b0f4a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1898,60 +1898,6 @@ impl Editor { editor.update_lsp_data(false, Some(*buffer_id), window, cx); } } - - project::Event::EntryRenamed(transaction) => { - let Some(workspace) = editor.workspace() else { - return; - }; - let Some(active_editor) = workspace.read(cx).active_item_as::(cx) - else { - return; - }; - if active_editor.entity_id() == cx.entity_id() { - let edited_buffers_already_open = { - let other_editors: Vec> = workspace - .read(cx) - .panes() - .iter() - .flat_map(|pane| pane.read(cx).items_of_type::()) - .filter(|editor| editor.entity_id() != cx.entity_id()) - .collect(); - - transaction.0.keys().all(|buffer| { - other_editors.iter().any(|editor| { - let multi_buffer = editor.read(cx).buffer(); - multi_buffer.read(cx).is_singleton() - && multi_buffer.read(cx).as_singleton().map_or( - false, - |singleton| { - singleton.entity_id() == buffer.entity_id() - }, - ) - }) - }) - }; - - if !edited_buffers_already_open { - let workspace = workspace.downgrade(); - let transaction = transaction.clone(); - cx.defer_in(window, move |_, window, cx| { - cx.spawn_in(window, async move |editor, cx| { - Self::open_project_transaction( - &editor, - workspace, - transaction, - "Rename".to_string(), - cx, - ) - .await - .ok() - }) - .detach(); - }); - } - } - } - _ => {} }, )); @@ -2588,7 +2534,7 @@ impl Editor { || binding .keystrokes() .first() - .is_some_and(|keystroke| keystroke.display_modifiers.modified()) + .is_some_and(|keystroke| keystroke.modifiers.modified()) })) } @@ -5574,11 +5520,6 @@ impl Editor { .as_ref() .is_none_or(|query| !query.chars().any(|c| c.is_digit(10))); - let omit_word_completions = match &query { - Some(query) => query.chars().count() < completion_settings.words_min_length, - None => completion_settings.words_min_length != 0, - }; - let (mut words, provider_responses) = match &provider { Some(provider) => { let provider_responses = provider.completions( @@ -5590,11 +5531,9 @@ impl Editor { cx, ); - let words = match (omit_word_completions, completion_settings.words) { - (true, _) | (_, WordsCompletionMode::Disabled) => { - Task::ready(BTreeMap::default()) - } - (false, WordsCompletionMode::Enabled | WordsCompletionMode::Fallback) => cx + let words = match completion_settings.words { + WordsCompletionMode::Disabled => Task::ready(BTreeMap::default()), + WordsCompletionMode::Enabled | WordsCompletionMode::Fallback => cx .background_spawn(async move { buffer_snapshot.words_in_range(WordsQuery { fuzzy_contents: None, @@ -5606,20 +5545,16 @@ impl Editor { (words, provider_responses) } - None => { - let words = if omit_word_completions { - Task::ready(BTreeMap::default()) - } else { - cx.background_spawn(async move { - buffer_snapshot.words_in_range(WordsQuery { - fuzzy_contents: None, - range: word_search_range, - skip_digits, - }) + None => ( + cx.background_spawn(async move { + buffer_snapshot.words_in_range(WordsQuery { + fuzzy_contents: None, + range: word_search_range, + skip_digits, }) - }; - (words, Task::ready(Ok(Vec::new()))) - } + }), + Task::ready(Ok(Vec::new())), + ), }; let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order; @@ -6345,7 +6280,7 @@ impl Editor { } pub async fn open_project_transaction( - editor: &WeakEntity, + this: &WeakEntity, workspace: WeakEntity, transaction: ProjectTransaction, title: String, @@ -6363,7 +6298,7 @@ impl Editor { if let Some((buffer, transaction)) = entries.first() { if entries.len() == 1 { - let excerpt = editor.update(cx, |editor, cx| { + let excerpt = this.update(cx, |editor, cx| { editor .buffer() .read(cx) @@ -7686,16 +7621,16 @@ impl Editor { .keystroke() { modifiers_held = modifiers_held - || (&accept_keystroke.display_modifiers == modifiers - && accept_keystroke.display_modifiers.modified()); + || (&accept_keystroke.modifiers == modifiers + && accept_keystroke.modifiers.modified()); }; if let Some(accept_partial_keystroke) = self .accept_edit_prediction_keybind(true, window, cx) .keystroke() { modifiers_held = modifiers_held - || (&accept_partial_keystroke.display_modifiers == modifiers - && accept_partial_keystroke.display_modifiers.modified()); + || (&accept_partial_keystroke.modifiers == modifiers + && accept_partial_keystroke.modifiers.modified()); } if modifiers_held { @@ -9044,7 +8979,7 @@ impl Editor { let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac; - let modifiers_color = if accept_keystroke.display_modifiers == window.modifiers() { + let modifiers_color = if accept_keystroke.modifiers == window.modifiers() { Color::Accent } else { Color::Muted @@ -9056,19 +8991,19 @@ impl Editor { .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) .text_size(TextSize::XSmall.rems(cx)) .child(h_flex().children(ui::render_modifiers( - &accept_keystroke.display_modifiers, + &accept_keystroke.modifiers, PlatformStyle::platform(), Some(modifiers_color), Some(IconSize::XSmall.rems().into()), true, ))) .when(is_platform_style_mac, |parent| { - parent.child(accept_keystroke.display_key.clone()) + parent.child(accept_keystroke.key.clone()) }) .when(!is_platform_style_mac, |parent| { parent.child( Key::new( - util::capitalize(&accept_keystroke.display_key), + util::capitalize(&accept_keystroke.key), Some(Color::Default), ) .size(Some(IconSize::XSmall.rems().into())), @@ -9171,7 +9106,7 @@ impl Editor { max_width: Pixels, cursor_point: Point, style: &EditorStyle, - accept_keystroke: Option<&gpui::KeybindingKeystroke>, + accept_keystroke: Option<&gpui::Keystroke>, _window: &Window, cx: &mut Context, ) -> Option { @@ -9249,7 +9184,7 @@ impl Editor { accept_keystroke.as_ref(), |el, accept_keystroke| { el.child(h_flex().children(ui::render_modifiers( - &accept_keystroke.display_modifiers, + &accept_keystroke.modifiers, PlatformStyle::platform(), Some(Color::Default), Some(IconSize::XSmall.rems().into()), @@ -9319,7 +9254,7 @@ impl Editor { .child(completion), ) .when_some(accept_keystroke, |el, accept_keystroke| { - if !accept_keystroke.display_modifiers.modified() { + if !accept_keystroke.modifiers.modified() { return el; } @@ -9338,7 +9273,7 @@ impl Editor { .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) .when(is_platform_style_mac, |parent| parent.gap_1()) .child(h_flex().children(ui::render_modifiers( - &accept_keystroke.display_modifiers, + &accept_keystroke.modifiers, PlatformStyle::platform(), Some(if !has_completion { Color::Muted @@ -9779,9 +9714,6 @@ impl Editor { } pub fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context) { - if self.read_only(cx) { - return; - } self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { this.select_autoclose_pair(window, cx); @@ -9875,9 +9807,6 @@ impl Editor { } pub fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context) { - if self.read_only(cx) { - return; - } self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { this.change_selections(Default::default(), window, cx, |s| { @@ -15730,9 +15659,7 @@ impl Editor { }; cx.spawn_in(window, async move |editor, cx| { - let Some(definitions) = definitions.await? else { - return Ok(Navigated::No); - }; + let definitions = definitions.await?; let navigated = editor .update_in(cx, |editor, window, cx| { editor.navigate_to_hover_links( @@ -16074,9 +16001,7 @@ impl Editor { } }); - let Some(locations) = references.await? else { - return anyhow::Ok(Navigated::No); - }; + let locations = references.await?; if locations.is_empty() { return anyhow::Ok(Navigated::No); } @@ -21858,7 +21783,7 @@ pub trait SemanticsProvider { buffer: &Entity, position: text::Anchor, cx: &mut App, - ) -> Option>>>; + ) -> Option>>; fn inline_values( &self, @@ -21897,7 +21822,7 @@ pub trait SemanticsProvider { position: text::Anchor, kind: GotoDefinitionKind, cx: &mut App, - ) -> Option>>>>; + ) -> Option>>>; fn range_for_rename( &self, @@ -22010,13 +21935,7 @@ impl CodeActionProvider for Entity { Ok(code_lens_actions .context("code lens fetch")? .into_iter() - .flatten() - .chain( - code_actions - .context("code action fetch")? - .into_iter() - .flatten(), - ) + .chain(code_actions.context("code action fetch")?) .collect()) }) }) @@ -22311,7 +22230,7 @@ impl SemanticsProvider for Entity { buffer: &Entity, position: text::Anchor, cx: &mut App, - ) -> Option>>> { + ) -> Option>> { Some(self.update(cx, |project, cx| project.hover(buffer, position, cx))) } @@ -22332,7 +22251,7 @@ impl SemanticsProvider for Entity { position: text::Anchor, kind: GotoDefinitionKind, cx: &mut App, - ) -> Option>>>> { + ) -> Option>>> { Some(self.update(cx, |project, cx| match kind { GotoDefinitionKind::Symbol => project.definitions(buffer, position, cx), GotoDefinitionKind::Declaration => project.declarations(buffer, position, cx), diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 2cfdb92593..96261fdb2c 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -57,9 +57,7 @@ use util::{ use workspace::{ CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry, OpenOptions, ViewId, - invalid_buffer_view::InvalidBufferView, item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions}, - register_project_item, }; #[gpui::test] @@ -12239,7 +12237,6 @@ async fn test_completion_mode(cx: &mut TestAppContext) { settings.defaults.completions = Some(CompletionSettings { lsp_insert_mode, words: WordsCompletionMode::Disabled, - words_min_length: 0, lsp: true, lsp_fetch_timeout_ms: 0, }); @@ -12298,7 +12295,6 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) update_test_language_settings(&mut cx, |settings| { settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Disabled, - words_min_length: 0, // set the opposite here to ensure that the action is overriding the default behavior lsp_insert_mode: LspInsertMode::Insert, lsp: true, @@ -12335,7 +12331,6 @@ async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) update_test_language_settings(&mut cx, |settings| { settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Disabled, - words_min_length: 0, // set the opposite here to ensure that the action is overriding the default behavior lsp_insert_mode: LspInsertMode::Replace, lsp: true, @@ -13077,7 +13072,6 @@ async fn test_word_completion(cx: &mut TestAppContext) { init_test(cx, |language_settings| { language_settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Fallback, - words_min_length: 0, lsp: true, lsp_fetch_timeout_ms: 10, lsp_insert_mode: LspInsertMode::Insert, @@ -13174,7 +13168,6 @@ async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext init_test(cx, |language_settings| { language_settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Enabled, - words_min_length: 0, lsp: true, lsp_fetch_timeout_ms: 0, lsp_insert_mode: LspInsertMode::Insert, @@ -13238,7 +13231,6 @@ async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) { init_test(cx, |language_settings| { language_settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Disabled, - words_min_length: 0, lsp: true, lsp_fetch_timeout_ms: 0, lsp_insert_mode: LspInsertMode::Insert, @@ -13312,7 +13304,6 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) { init_test(cx, |language_settings| { language_settings.defaults.completions = Some(CompletionSettings { words: WordsCompletionMode::Fallback, - words_min_length: 0, lsp: false, lsp_fetch_timeout_ms: 0, lsp_insert_mode: LspInsertMode::Insert, @@ -13370,56 +13361,6 @@ async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) { }); } -#[gpui::test] -async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) { - init_test(cx, |language_settings| { - language_settings.defaults.completions = Some(CompletionSettings { - words: WordsCompletionMode::Enabled, - words_min_length: 3, - lsp: true, - lsp_fetch_timeout_ms: 0, - lsp_insert_mode: LspInsertMode::Insert, - }); - }); - - let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await; - cx.set_state(indoc! {"ˇ - wow - wowen - wowser - "}); - cx.simulate_keystroke("w"); - cx.executor().run_until_parked(); - cx.update_editor(|editor, _, _| { - if editor.context_menu.borrow_mut().is_some() { - panic!( - "expected completion menu to be hidden, as words completion threshold is not met" - ); - } - }); - - cx.simulate_keystroke("o"); - cx.executor().run_until_parked(); - cx.update_editor(|editor, _, _| { - if editor.context_menu.borrow_mut().is_some() { - panic!( - "expected completion menu to be hidden, as words completion threshold is not met still" - ); - } - }); - - cx.simulate_keystroke("w"); - cx.executor().run_until_parked(); - cx.update_editor(|editor, _, _| { - if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref() - { - assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word"); - } else { - panic!("expected completion menu to be open after the word completions threshold is met"); - } - }); -} - fn gen_text_edit(params: &CompletionParams, text: &str) -> Option { let position = || lsp::Position { line: params.text_document_position.position.line, @@ -22715,7 +22656,7 @@ async fn test_invisible_worktree_servers(cx: &mut TestAppContext) { .await .unwrap(); pane.update_in(cx, |pane, window, cx| { - pane.navigate_backward(&Default::default(), window, cx); + pane.navigate_backward(window, cx); }); cx.run_until_parked(); pane.update(cx, |pane, cx| { @@ -24302,7 +24243,7 @@ async fn test_document_colors(cx: &mut TestAppContext) { workspace .update(cx, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.navigate_backward(&Default::default(), window, cx); + pane.navigate_backward(window, cx); }) }) .unwrap(); @@ -24350,41 +24291,6 @@ async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) { }); } -#[gpui::test] -async fn test_non_utf_8_opens(cx: &mut TestAppContext) { - init_test(cx, |_| {}); - - cx.update(|cx| { - register_project_item::(cx); - }); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree("/root1", json!({})).await; - fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd]) - .await; - - let project = Project::test(fs, ["/root1".as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let worktree_id = project.update(cx, |project, cx| { - project.worktrees(cx).next().unwrap().read(cx).id() - }); - - let handle = workspace - .update_in(cx, |workspace, window, cx| { - let project_path = (worktree_id, "one.pdf"); - workspace.open_path(project_path, None, true, window, cx) - }) - .await - .unwrap(); - - assert_eq!( - handle.to_any().entity_type(), - TypeId::of::() - ); -} - #[track_caller] fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec { editor diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 91034829f7..797b0d6634 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -43,10 +43,10 @@ use gpui::{ Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, - KeybindingKeystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent, - MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, - ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, - Style, Styled, TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill, + Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent, + MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollHandle, + ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, + TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background, transparent_black, }; @@ -74,7 +74,6 @@ use std::{ fmt::{self, Write}, iter, mem, ops::{Deref, Range}, - path::{self, Path}, rc::Rc, sync::Arc, time::{Duration, Instant}, @@ -90,8 +89,8 @@ use unicode_segmentation::UnicodeSegmentation; use util::post_inc; use util::{RangeExt, ResultExt, debug_panic}; use workspace::{ - CollaboratorId, ItemSettings, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace, - item::Item, notifications::NotifyTaskExt, + CollaboratorId, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace, item::Item, + notifications::NotifyTaskExt, }; /// Determines what kinds of highlights should be applied to a lines background. @@ -3603,187 +3602,171 @@ impl EditorElement { let focus_handle = editor.focus_handle(cx); let colors = cx.theme().colors(); - let header = div() - .p_1() - .w_full() - .h(FILE_HEADER_HEIGHT as f32 * window.line_height()) - .child( - h_flex() - .size_full() - .gap_2() - .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667))) - .pl_0p5() - .pr_5() - .rounded_sm() - .when(is_sticky, |el| el.shadow_md()) - .border_1() - .map(|div| { - let border_color = if is_selected - && is_folded - && focus_handle.contains_focused(window, cx) - { - colors.border_focused - } else { - colors.border - }; - div.border_color(border_color) - }) - .bg(colors.editor_subheader_background) - .hover(|style| style.bg(colors.element_hover)) - .map(|header| { - let editor = self.editor.clone(); - let buffer_id = for_excerpt.buffer_id; - let toggle_chevron_icon = - FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path); - header.child( - div() - .hover(|style| style.bg(colors.element_selected)) - .rounded_xs() - .child( - ButtonLike::new("toggle-buffer-fold") - .style(ui::ButtonStyle::Transparent) - .height(px(28.).into()) - .width(px(28.)) - .children(toggle_chevron_icon) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::with_meta_in( - "Toggle Excerpt Fold", - Some(&ToggleFold), - "Alt+click to toggle all", - &focus_handle, - window, - cx, - ) - } - }) - .on_click(move |event, window, cx| { - if event.modifiers().alt { - // Alt+click toggles all buffers - editor.update(cx, |editor, cx| { - editor.toggle_fold_all( - &ToggleFoldAll, + let header = + div() + .p_1() + .w_full() + .h(FILE_HEADER_HEIGHT as f32 * window.line_height()) + .child( + h_flex() + .size_full() + .gap_2() + .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667))) + .pl_0p5() + .pr_5() + .rounded_sm() + .when(is_sticky, |el| el.shadow_md()) + .border_1() + .map(|div| { + let border_color = if is_selected + && is_folded + && focus_handle.contains_focused(window, cx) + { + colors.border_focused + } else { + colors.border + }; + div.border_color(border_color) + }) + .bg(colors.editor_subheader_background) + .hover(|style| style.bg(colors.element_hover)) + .map(|header| { + let editor = self.editor.clone(); + let buffer_id = for_excerpt.buffer_id; + let toggle_chevron_icon = + FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path); + header.child( + div() + .hover(|style| style.bg(colors.element_selected)) + .rounded_xs() + .child( + ButtonLike::new("toggle-buffer-fold") + .style(ui::ButtonStyle::Transparent) + .height(px(28.).into()) + .width(px(28.)) + .children(toggle_chevron_icon) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |window, cx| { + Tooltip::with_meta_in( + "Toggle Excerpt Fold", + Some(&ToggleFold), + "Alt+click to toggle all", + &focus_handle, window, cx, - ); - }); - } else { - // Regular click toggles single buffer - if is_folded { + ) + } + }) + .on_click(move |event, window, cx| { + if event.modifiers().alt { + // Alt+click toggles all buffers editor.update(cx, |editor, cx| { - editor.unfold_buffer(buffer_id, cx); + editor.toggle_fold_all( + &ToggleFoldAll, + window, + cx, + ); }); } else { - editor.update(cx, |editor, cx| { - editor.fold_buffer(buffer_id, cx); - }); - } - } - }), - ), - ) - }) - .children( - editor - .addons - .values() - .filter_map(|addon| { - addon.render_buffer_header_controls(for_excerpt, window, cx) - }) - .take(1), - ) - .child( - h_flex() - .size(Pixels(12.0)) - .justify_center() - .children(indicator), - ) - .child( - h_flex() - .cursor_pointer() - .id("path header block") - .size_full() - .justify_between() - .overflow_hidden() - .child( - h_flex() - .gap_2() - .map(|path_header| { - let filename = filename - .map(SharedString::from) - .unwrap_or_else(|| "untitled".into()); - - path_header - .when(ItemSettings::get_global(cx).file_icons, |el| { - let path = path::Path::new(filename.as_str()); - let icon = FileIcons::get_icon(path, cx) - .unwrap_or_default(); - let icon = - Icon::from_path(icon).color(Color::Muted); - el.child(icon) - }) - .child(Label::new(filename).single_line().when_some( - file_status, - |el, status| { - el.color(if status.is_conflicted() { - Color::Conflict - } else if status.is_modified() { - Color::Modified - } else if status.is_deleted() { - Color::Disabled + // Regular click toggles single buffer + if is_folded { + editor.update(cx, |editor, cx| { + editor.unfold_buffer(buffer_id, cx); + }); } else { - Color::Created - }) - .when(status.is_deleted(), |el| { - el.strikethrough() - }) + editor.update(cx, |editor, cx| { + editor.fold_buffer(buffer_id, cx); + }); + } + } + }), + ), + ) + }) + .children( + editor + .addons + .values() + .filter_map(|addon| { + addon.render_buffer_header_controls(for_excerpt, window, cx) + }) + .take(1), + ) + .children(indicator) + .child( + h_flex() + .cursor_pointer() + .id("path header block") + .size_full() + .justify_between() + .overflow_hidden() + .child( + h_flex() + .gap_2() + .child( + Label::new( + filename + .map(SharedString::from) + .unwrap_or_else(|| "untitled".into()), + ) + .single_line() + .when_some(file_status, |el, status| { + el.color(if status.is_conflicted() { + Color::Conflict + } else if status.is_modified() { + Color::Modified + } else if status.is_deleted() { + Color::Disabled + } else { + Color::Created + }) + .when(status.is_deleted(), |el| el.strikethrough()) + }), + ) + .when_some(parent_path, |then, path| { + then.child(div().child(path).text_color( + if file_status.is_some_and(FileStatus::is_deleted) { + colors.text_disabled + } else { + colors.text_muted }, )) - }) - .when_some(parent_path, |then, path| { - then.child(div().child(path).text_color( - if file_status.is_some_and(FileStatus::is_deleted) { - colors.text_disabled - } else { - colors.text_muted - }, - )) - }), - ) - .when( - can_open_excerpts && is_selected && relative_path.is_some(), - |el| { - el.child( - h_flex() - .id("jump-to-file-button") - .gap_2p5() - .child(Label::new("Jump To File")) - .children( - KeyBinding::for_action_in( - &OpenExcerpts, - &focus_handle, - window, - cx, - ) - .map(|binding| binding.into_any_element()), - ), - ) - }, - ) - .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) - .on_click(window.listener_for(&self.editor, { - move |editor, e: &ClickEvent, window, cx| { - editor.open_excerpts_common( - Some(jump_data.clone()), - e.modifiers().secondary(), - window, - cx, - ); - } - })), - ), - ); + }), + ) + .when( + can_open_excerpts && is_selected && relative_path.is_some(), + |el| { + el.child( + h_flex() + .id("jump-to-file-button") + .gap_2p5() + .child(Label::new("Jump To File")) + .children( + KeyBinding::for_action_in( + &OpenExcerpts, + &focus_handle, + window, + cx, + ) + .map(|binding| binding.into_any_element()), + ), + ) + }, + ) + .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) + .on_click(window.listener_for(&self.editor, { + move |editor, e: &ClickEvent, window, cx| { + editor.open_excerpts_common( + Some(jump_data.clone()), + e.modifiers().secondary(), + window, + cx, + ); + } + })), + ), + ); let file = for_excerpt.buffer.file().cloned(); let editor = self.editor.clone(); @@ -3799,31 +3782,25 @@ impl EditorElement { && let Some(worktree) = project.read(cx).worktree_for_id(file.worktree_id(cx), cx) { - let worktree = worktree.read(cx); let relative_path = file.path(); - let entry_for_path = worktree.entry_for_path(relative_path); - let abs_path = entry_for_path.map(|e| { - e.canonical_path.as_deref().map_or_else( - || worktree.abs_path().join(relative_path), - Path::to_path_buf, - ) - }); - let has_relative_path = worktree.root_entry().is_some_and(Entry::is_dir); + let entry_for_path = worktree.read(cx).entry_for_path(relative_path); + let abs_path = entry_for_path.and_then(|e| e.canonical_path.as_deref()); + let has_relative_path = + worktree.read(cx).root_entry().is_some_and(Entry::is_dir); - let parent_abs_path = abs_path - .as_ref() - .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf())); + let parent_abs_path = + abs_path.and_then(|abs_path| Some(abs_path.parent()?.to_path_buf())); let relative_path = has_relative_path .then_some(relative_path) .map(ToOwned::to_owned); let visible_in_project_panel = - relative_path.is_some() && worktree.is_visible(); + relative_path.is_some() && worktree.read(cx).is_visible(); let reveal_in_project_panel = entry_for_path .filter(|_| visible_in_project_panel) .map(|entry| entry.id); menu = menu - .when_some(abs_path, |menu, abs_path| { + .when_some(abs_path.map(ToOwned::to_owned), |menu, abs_path| { menu.entry( "Copy Path", Some(Box::new(zed_actions::workspace::CopyPath)), @@ -7150,7 +7127,7 @@ fn header_jump_data( pub struct AcceptEditPredictionBinding(pub(crate) Option); impl AcceptEditPredictionBinding { - pub fn keystroke(&self) -> Option<&KeybindingKeystroke> { + pub fn keystroke(&self) -> Option<&Keystroke> { if let Some(binding) = self.0.as_ref() { match &binding.keystrokes() { [keystroke, ..] => Some(keystroke), diff --git a/crates/editor/src/highlight_matching_bracket.rs b/crates/editor/src/highlight_matching_bracket.rs index aa4e616924..e38197283d 100644 --- a/crates/editor/src/highlight_matching_bracket.rs +++ b/crates/editor/src/highlight_matching_bracket.rs @@ -1,7 +1,6 @@ use crate::{Editor, RangeToAnchorExt}; -use gpui::{Context, HighlightStyle, Window}; +use gpui::{Context, Window}; use language::CursorShape; -use theme::ActiveTheme; enum MatchingBracketHighlight {} @@ -10,7 +9,7 @@ pub fn refresh_matching_bracket_highlights( window: &mut Window, cx: &mut Context, ) { - editor.clear_highlights::(cx); + editor.clear_background_highlights::(cx); let newest_selection = editor.selections.newest::(cx); // Don't highlight brackets if the selection isn't empty @@ -36,19 +35,12 @@ pub fn refresh_matching_bracket_highlights( .buffer_snapshot .innermost_enclosing_bracket_ranges(head..tail, None) { - editor.highlight_text::( - vec![ + editor.highlight_background::( + &[ opening_range.to_anchors(&snapshot.buffer_snapshot), closing_range.to_anchors(&snapshot.buffer_snapshot), ], - HighlightStyle { - background_color: Some( - cx.theme() - .colors() - .editor_document_highlight_bracket_background, - ), - ..Default::default() - }, + |theme| theme.colors().editor_document_highlight_bracket_background, cx, ) } @@ -112,7 +104,7 @@ mod tests { another_test(1, 2, 3); } "#}); - cx.assert_editor_text_highlights::(indoc! {r#" + cx.assert_editor_background_highlights::(indoc! {r#" pub fn test«(»"Test argument"«)» { another_test(1, 2, 3); } @@ -123,7 +115,7 @@ mod tests { another_test(1, ˇ2, 3); } "#}); - cx.assert_editor_text_highlights::(indoc! {r#" + cx.assert_editor_background_highlights::(indoc! {r#" pub fn test("Test argument") { another_test«(»1, 2, 3«)»; } @@ -134,7 +126,7 @@ mod tests { anotherˇ_test(1, 2, 3); } "#}); - cx.assert_editor_text_highlights::(indoc! {r#" + cx.assert_editor_background_highlights::(indoc! {r#" pub fn test("Test argument") «{» another_test(1, 2, 3); «}» @@ -146,7 +138,7 @@ mod tests { another_test(1, 2, 3); } "#}); - cx.assert_editor_text_highlights::(indoc! {r#" + cx.assert_editor_background_highlights::(indoc! {r#" pub fn test("Test argument") { another_test(1, 2, 3); } @@ -158,8 +150,8 @@ mod tests { another_test(1, 2, 3); } "#}); - cx.assert_editor_text_highlights::(indoc! {r#" - pub fn test«("Test argument") { + cx.assert_editor_background_highlights::(indoc! {r#" + pub fn test("Test argument") { another_test(1, 2, 3); } "#}); diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index 94f49f601a..d10df5154e 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -562,7 +562,7 @@ pub fn show_link_definition( provider.definitions(&buffer, buffer_position, preferred_kind, cx) })?; if let Some(task) = task { - task.await.ok().flatten().map(|definition_result| { + task.await.ok().map(|definition_result| { ( definition_result.iter().find_map(|link| { link.origin.as_ref().and_then(|origin| { diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index fab5345787..28a09e947f 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -428,7 +428,7 @@ fn show_hover( }; let hovers_response = if let Some(hover_request) = hover_request { - hover_request.await.unwrap_or_default() + hover_request.await } else { Vec::new() }; diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index b7110190fd..afc5767de0 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -42,7 +42,6 @@ use ui::{IconDecorationKind, prelude::*}; use util::{ResultExt, TryFutureExt, paths::PathExt}; use workspace::{ CollaboratorId, ItemId, ItemNavHistory, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, - invalid_buffer_view::InvalidBufferView, item::{FollowableItem, Item, ItemEvent, ProjectItem, SaveOptions}, searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle}, }; @@ -1402,16 +1401,6 @@ impl ProjectItem for Editor { editor } - - fn for_broken_project_item( - abs_path: &Path, - is_local: bool, - e: &anyhow::Error, - window: &mut Window, - cx: &mut App, - ) -> Option { - Some(InvalidBufferView::new(abs_path, is_local, e, window, cx)) - } } fn clip_ranges<'a>( diff --git a/crates/editor/src/persistence.rs b/crates/editor/src/persistence.rs index ec7c149b4e..88fde53947 100644 --- a/crates/editor/src/persistence.rs +++ b/crates/editor/src/persistence.rs @@ -1,17 +1,13 @@ use anyhow::Result; -use db::{ - query, - sqlez::{ - bindable::{Bind, Column, StaticColumnCount}, - domain::Domain, - statement::Statement, - }, - sqlez_macros::sql, -}; +use db::sqlez::bindable::{Bind, Column, StaticColumnCount}; +use db::sqlez::statement::Statement; use fs::MTime; use itertools::Itertools as _; use std::path::PathBuf; +use db::sqlez_macros::sql; +use db::{define_connection, query}; + use workspace::{ItemId, WorkspaceDb, WorkspaceId}; #[derive(Clone, Debug, PartialEq, Default)] @@ -87,11 +83,7 @@ impl Column for SerializedEditor { } } -pub struct EditorDb(db::sqlez::thread_safe_connection::ThreadSafeConnection); - -impl Domain for EditorDb { - const NAME: &str = stringify!(EditorDb); - +define_connection!( // Current schema shape using pseudo-rust syntax: // editors( // item_id: usize, @@ -121,8 +113,7 @@ impl Domain for EditorDb { // start: usize, // end: usize, // ) - - const MIGRATIONS: &[&str] = &[ + pub static ref DB: EditorDb = &[ sql! ( CREATE TABLE editors( item_id INTEGER NOT NULL, @@ -198,9 +189,7 @@ impl Domain for EditorDb { ) STRICT; ), ]; -} - -db::static_connection!(DB, EditorDb, [WorkspaceDb]); +); // https://www.sqlite.org/limits.html // > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER, diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index 2d4710a8d4..c79feccb4b 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -431,7 +431,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider { buffer: &Entity, position: text::Anchor, cx: &mut App, - ) -> Option>>> { + ) -> Option>> { let buffer = self.to_base(buffer, &[position], cx)?; self.0.hover(&buffer, position, cx) } @@ -490,7 +490,7 @@ impl SemanticsProvider for BranchBufferSemanticsProvider { position: text::Anchor, kind: crate::GotoDefinitionKind, cx: &mut App, - ) -> Option>>>> { + ) -> Option>>> { let buffer = self.to_base(buffer, &[position], cx)?; self.0.definitions(&buffer, position, kind, cx) } diff --git a/crates/editor/src/rust_analyzer_ext.rs b/crates/editor/src/rust_analyzer_ext.rs index cf74ee0a9e..e3d83ab160 100644 --- a/crates/editor/src/rust_analyzer_ext.rs +++ b/crates/editor/src/rust_analyzer_ext.rs @@ -26,17 +26,6 @@ fn is_rust_language(language: &Language) -> bool { } pub fn apply_related_actions(editor: &Entity, window: &mut Window, cx: &mut App) { - if editor.read(cx).project().is_some_and(|project| { - project - .read(cx) - .language_server_statuses(cx) - .any(|(_, status)| status.name == RUST_ANALYZER_NAME) - }) { - register_action(editor, window, cancel_flycheck_action); - register_action(editor, window, run_flycheck_action); - register_action(editor, window, clear_flycheck_action); - } - if editor .read(cx) .buffer() @@ -49,6 +38,9 @@ pub fn apply_related_actions(editor: &Entity, window: &mut Window, cx: & register_action(editor, window, go_to_parent_module); register_action(editor, window, expand_macro_recursively); register_action(editor, window, open_docs); + register_action(editor, window, cancel_flycheck_action); + register_action(editor, window, run_flycheck_action); + register_action(editor, window, clear_flycheck_action); } } @@ -317,7 +309,7 @@ fn cancel_flycheck_action( let Some(project) = &editor.project else { return; }; - let buffer_id = editor + let Some(buffer_id) = editor .selections .disjoint_anchors() .iter() @@ -329,7 +321,10 @@ fn cancel_flycheck_action( .read(cx) .entry_id(cx)?; project.path_for_entry(entry_id, cx) - }); + }) + else { + return; + }; cancel_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx); } @@ -342,7 +337,7 @@ fn run_flycheck_action( let Some(project) = &editor.project else { return; }; - let buffer_id = editor + let Some(buffer_id) = editor .selections .disjoint_anchors() .iter() @@ -354,7 +349,10 @@ fn run_flycheck_action( .read(cx) .entry_id(cx)?; project.path_for_entry(entry_id, cx) - }); + }) + else { + return; + }; run_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx); } @@ -367,7 +365,7 @@ fn clear_flycheck_action( let Some(project) = &editor.project else { return; }; - let buffer_id = editor + let Some(buffer_id) = editor .selections .disjoint_anchors() .iter() @@ -379,6 +377,9 @@ fn clear_flycheck_action( .read(cx) .entry_id(cx)?; project.path_for_entry(entry_id, cx) - }); + }) + else { + return; + }; clear_flycheck(project.clone(), buffer_id, cx).detach_and_log_err(cx); } diff --git a/crates/editor/src/signature_help.rs b/crates/editor/src/signature_help.rs index cb21f35d7e..5c9800ab55 100644 --- a/crates/editor/src/signature_help.rs +++ b/crates/editor/src/signature_help.rs @@ -182,9 +182,7 @@ impl Editor { let signature_help = task.await; editor .update(cx, |editor, cx| { - let Some(mut signature_help) = - signature_help.unwrap_or_default().into_iter().next() - else { + let Some(mut signature_help) = signature_help.into_iter().next() else { editor .signature_help_state .hide(SignatureHelpHiddenBy::AutoClose); diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs index f5f7fc42b3..422979c429 100644 --- a/crates/feature_flags/src/feature_flags.rs +++ b/crates/feature_flags/src/feature_flags.rs @@ -98,10 +98,6 @@ impl FeatureFlag for GeminiAndNativeFeatureFlag { // integration too, and we'd like to turn Gemini/Native on in new builds // without enabling Claude Code in old builds. const NAME: &'static str = "gemini-and-native"; - - fn enabled_for_all() -> bool { - true - } } pub struct ClaudeCodeFeatureFlag; @@ -205,7 +201,7 @@ impl FeatureFlagAppExt for App { fn has_flag(&self) -> bool { self.try_global::() .map(|flags| flags.has_flag::()) - .unwrap_or(T::enabled_for_all()) + .unwrap_or(false) } fn is_staff(&self) -> bool { diff --git a/crates/feedback/Cargo.toml b/crates/feedback/Cargo.toml index db872f7a15..3a2c1fd713 100644 --- a/crates/feedback/Cargo.toml +++ b/crates/feedback/Cargo.toml @@ -15,9 +15,13 @@ path = "src/feedback.rs" test-support = [] [dependencies] +client.workspace = true gpui.workspace = true +human_bytes = "0.4.1" menu.workspace = true -system_specs.workspace = true +release_channel.workspace = true +serde.workspace = true +sysinfo.workspace = true ui.workspace = true urlencoding.workspace = true util.workspace = true diff --git a/crates/feedback/src/feedback.rs b/crates/feedback/src/feedback.rs index 3822dd7ba3..40c2707d34 100644 --- a/crates/feedback/src/feedback.rs +++ b/crates/feedback/src/feedback.rs @@ -1,14 +1,18 @@ use gpui::{App, ClipboardItem, PromptLevel, actions}; -use system_specs::{CopySystemSpecsIntoClipboard, SystemSpecs}; +use system_specs::SystemSpecs; use util::ResultExt; use workspace::Workspace; use zed_actions::feedback::FileBugReport; pub mod feedback_modal; +pub mod system_specs; + actions!( zed, [ + /// Copies system specifications to the clipboard for bug reports. + CopySystemSpecsIntoClipboard, /// Opens email client to send feedback to Zed support. EmailZed, /// Opens the Zed repository on GitHub. diff --git a/crates/system_specs/src/system_specs.rs b/crates/feedback/src/system_specs.rs similarity index 59% rename from crates/system_specs/src/system_specs.rs rename to crates/feedback/src/system_specs.rs index 731d335232..87642ab929 100644 --- a/crates/system_specs/src/system_specs.rs +++ b/crates/feedback/src/system_specs.rs @@ -1,22 +1,11 @@ -//! # system_specs - use client::telemetry; -pub use gpui::GpuSpecs; -use gpui::{App, AppContext as _, SemanticVersion, Task, Window, actions}; +use gpui::{App, AppContext as _, SemanticVersion, Task, Window}; use human_bytes::human_bytes; use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; use serde::Serialize; use std::{env, fmt::Display}; use sysinfo::{MemoryRefreshKind, RefreshKind, System}; -actions!( - zed, - [ - /// Copies system specifications to the clipboard for bug reports. - CopySystemSpecsIntoClipboard, - ] -); - #[derive(Clone, Debug, Serialize)] pub struct SystemSpecs { app_version: String, @@ -169,115 +158,6 @@ fn try_determine_available_gpus() -> Option { } } -#[derive(Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize, Clone)] -pub struct GpuInfo { - pub device_name: Option, - pub device_pci_id: u16, - pub vendor_name: Option, - pub vendor_pci_id: u16, - pub driver_version: Option, - pub driver_name: Option, -} - -#[cfg(any(target_os = "linux", target_os = "freebsd"))] -pub fn read_gpu_info_from_sys_class_drm() -> anyhow::Result> { - use anyhow::Context as _; - use pciid_parser; - let dir_iter = std::fs::read_dir("/sys/class/drm").context("Failed to read /sys/class/drm")?; - let mut pci_addresses = vec![]; - let mut gpus = Vec::::new(); - let pci_db = pciid_parser::Database::read().ok(); - for entry in dir_iter { - let Ok(entry) = entry else { - continue; - }; - - let device_path = entry.path().join("device"); - let Some(pci_address) = device_path.read_link().ok().and_then(|pci_address| { - pci_address - .file_name() - .and_then(std::ffi::OsStr::to_str) - .map(str::trim) - .map(str::to_string) - }) else { - continue; - }; - let Ok(device_pci_id) = read_pci_id_from_path(device_path.join("device")) else { - continue; - }; - let Ok(vendor_pci_id) = read_pci_id_from_path(device_path.join("vendor")) else { - continue; - }; - let driver_name = std::fs::read_link(device_path.join("driver")) - .ok() - .and_then(|driver_link| { - driver_link - .file_name() - .and_then(std::ffi::OsStr::to_str) - .map(str::trim) - .map(str::to_string) - }); - let driver_version = driver_name - .as_ref() - .and_then(|driver_name| { - std::fs::read_to_string(format!("/sys/module/{driver_name}/version")).ok() - }) - .as_deref() - .map(str::trim) - .map(str::to_string); - - let already_found = gpus - .iter() - .zip(&pci_addresses) - .any(|(gpu, gpu_pci_address)| { - gpu_pci_address == &pci_address - && gpu.driver_version == driver_version - && gpu.driver_name == driver_name - }); - - if already_found { - continue; - } - - let vendor = pci_db - .as_ref() - .and_then(|db| db.vendors.get(&vendor_pci_id)); - let vendor_name = vendor.map(|vendor| vendor.name.clone()); - let device_name = vendor - .and_then(|vendor| vendor.devices.get(&device_pci_id)) - .map(|device| device.name.clone()); - - gpus.push(GpuInfo { - device_name, - device_pci_id, - vendor_name, - vendor_pci_id, - driver_version, - driver_name, - }); - pci_addresses.push(pci_address); - } - - Ok(gpus) -} - -#[cfg(any(target_os = "linux", target_os = "freebsd"))] -fn read_pci_id_from_path(path: impl AsRef) -> anyhow::Result { - use anyhow::Context as _; - let id = std::fs::read_to_string(path)?; - let id = id - .trim() - .strip_prefix("0x") - .context("Not a device ID") - .context(id.clone())?; - anyhow::ensure!( - id.len() == 4, - "Not a device id, expected 4 digits, found {}", - id.len() - ); - u16::from_str_radix(id, 16).context("Failed to parse device ID") -} - /// Returns value of `ZED_BUNDLE_TYPE` set at compiletime or else at runtime. /// /// The compiletime value is used by flatpak since it doesn't seem to have a way to provide a diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 7512152324..8aaaa04729 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1401,16 +1401,13 @@ impl PickerDelegate for FileFinderDelegate { #[cfg(windows)] let raw_query = raw_query.trim().to_owned().replace("/", "\\"); #[cfg(not(windows))] - let raw_query = raw_query.trim(); + let raw_query = raw_query.trim().to_owned(); - let raw_query = raw_query.trim_end_matches(':').to_owned(); - let path = path_position.path.to_str(); - let path_trimmed = path.unwrap_or(&raw_query).trim_end_matches(':'); - let file_query_end = if path_trimmed == raw_query { + let file_query_end = if path_position.path.to_str().unwrap_or(&raw_query) == raw_query { None } else { // Safe to unwrap as we won't get here when the unwrap in if fails - Some(path.unwrap().len()) + Some(path_position.path.to_str().unwrap().len()) }; let query = FileSearchQuery { diff --git a/crates/file_finder/src/file_finder_tests.rs b/crates/file_finder/src/file_finder_tests.rs index cd0f203d6a..8203d1b1fd 100644 --- a/crates/file_finder/src/file_finder_tests.rs +++ b/crates/file_finder/src/file_finder_tests.rs @@ -218,7 +218,6 @@ async fn test_matching_paths(cx: &mut TestAppContext) { " ndan ", " band ", "a bandana", - "bandana:", ] { picker .update_in(cx, |picker, window, cx| { @@ -253,53 +252,6 @@ async fn test_matching_paths(cx: &mut TestAppContext) { } } -#[gpui::test] -async fn test_matching_paths_with_colon(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - path!("/root"), - json!({ - "a": { - "foo:bar.rs": "", - "foo.rs": "", - } - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - - let (picker, _, cx) = build_find_picker(project, cx); - - // 'foo:' matches both files - cx.simulate_input("foo:"); - picker.update(cx, |picker, _| { - assert_eq!(picker.delegate.matches.len(), 3); - assert_match_at_position(picker, 0, "foo.rs"); - assert_match_at_position(picker, 1, "foo:bar.rs"); - }); - - // 'foo:b' matches one of the files - cx.simulate_input("b"); - picker.update(cx, |picker, _| { - assert_eq!(picker.delegate.matches.len(), 2); - assert_match_at_position(picker, 0, "foo:bar.rs"); - }); - - cx.dispatch_action(editor::actions::Backspace); - - // 'foo:1' matches both files, specifying which row to jump to - cx.simulate_input("1"); - picker.update(cx, |picker, _| { - assert_eq!(picker.delegate.matches.len(), 3); - assert_match_at_position(picker, 0, "foo.rs"); - assert_match_at_position(picker, 1, "foo:bar.rs"); - }); -} - #[gpui::test] async fn test_unicode_paths(cx: &mut TestAppContext) { let app_state = init_test(cx); diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index b59d7e717a..bbd59fa7bc 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -37,10 +37,10 @@ use crate::{ AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId, EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext, Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform, - PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point, PromptBuilder, - PromptButton, PromptHandle, PromptLevel, Render, RenderImage, RenderablePromptHandle, - Reservation, ScreenCaptureSource, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, - Window, WindowAppearance, WindowHandle, WindowId, WindowInvalidator, + PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptButton, PromptHandle, + PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource, + SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance, + WindowHandle, WindowId, WindowInvalidator, colors::{Colors, GlobalColors}, current_platform, hash, init_app_menus, }; @@ -263,7 +263,6 @@ pub struct App { pub(crate) focus_handles: Arc, pub(crate) keymap: Rc>, pub(crate) keyboard_layout: Box, - pub(crate) keyboard_mapper: Rc, pub(crate) global_action_listeners: FxHashMap>>, pending_effects: VecDeque, @@ -313,7 +312,6 @@ impl App { let text_system = Arc::new(TextSystem::new(platform.text_system())); let entities = EntityMap::new(); let keyboard_layout = platform.keyboard_layout(); - let keyboard_mapper = platform.keyboard_mapper(); let app = Rc::new_cyclic(|this| AppCell { app: RefCell::new(App { @@ -339,7 +337,6 @@ impl App { focus_handles: Arc::new(RwLock::new(SlotMap::with_key())), keymap: Rc::new(RefCell::new(Keymap::default())), keyboard_layout, - keyboard_mapper, global_action_listeners: FxHashMap::default(), pending_effects: VecDeque::new(), pending_notifications: FxHashSet::default(), @@ -379,7 +376,6 @@ impl App { if let Some(app) = app.upgrade() { let cx = &mut app.borrow_mut(); cx.keyboard_layout = cx.platform.keyboard_layout(); - cx.keyboard_mapper = cx.platform.keyboard_mapper(); cx.keyboard_layout_observers .clone() .retain(&(), move |callback| (callback)(cx)); @@ -428,11 +424,6 @@ impl App { self.keyboard_layout.as_ref() } - /// Get the current keyboard mapper. - pub fn keyboard_mapper(&self) -> &Rc { - &self.keyboard_mapper - } - /// Invokes a handler when the current keyboard layout changes pub fn on_keyboard_layout_change(&self, mut callback: F) -> Subscription where diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 0f5b98df39..5e4b5fe6e9 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -352,7 +352,7 @@ impl Flatten for Result { } /// Information about the GPU GPUI is running on. -#[derive(Default, Debug, serde::Serialize, serde::Deserialize, Clone)] +#[derive(Default, Debug)] pub struct GpuSpecs { /// Whether the GPU is really a fake (like `llvmpipe`) running on the CPU. pub is_software_emulated: bool, diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index b3db09d821..757205fcc3 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -4,7 +4,7 @@ mod context; pub use binding::*; pub use context::*; -use crate::{Action, AsKeystroke, Keystroke, is_no_action}; +use crate::{Action, Keystroke, is_no_action}; use collections::{HashMap, HashSet}; use smallvec::SmallVec; use std::any::TypeId; @@ -141,7 +141,7 @@ impl Keymap { /// only. pub fn bindings_for_input( &self, - input: &[impl AsKeystroke], + input: &[Keystroke], context_stack: &[KeyContext], ) -> (SmallVec<[KeyBinding; 1]>, bool) { let mut matched_bindings = SmallVec::<[(usize, BindingIndex, &KeyBinding); 1]>::new(); @@ -192,6 +192,7 @@ impl Keymap { (bindings, !pending.is_empty()) } + /// Check if the given binding is enabled, given a certain key context. /// Returns the deepest depth at which the binding matches, or None if it doesn't match. fn binding_enabled(&self, binding: &KeyBinding, contexts: &[KeyContext]) -> Option { @@ -638,7 +639,7 @@ mod tests { fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) { let actual = keymap .bindings_for_action(action) - .map(|binding| binding.keystrokes[0].inner.unparse()) + .map(|binding| binding.keystrokes[0].unparse()) .collect::>(); assert_eq!(actual, expected, "{:?}", action); } diff --git a/crates/gpui/src/keymap/binding.rs b/crates/gpui/src/keymap/binding.rs index a7cf9d5c54..729498d153 100644 --- a/crates/gpui/src/keymap/binding.rs +++ b/crates/gpui/src/keymap/binding.rs @@ -1,15 +1,14 @@ use std::rc::Rc; -use crate::{ - Action, AsKeystroke, DummyKeyboardMapper, InvalidKeystrokeError, KeyBindingContextPredicate, - KeybindingKeystroke, Keystroke, PlatformKeyboardMapper, SharedString, -}; +use collections::HashMap; + +use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke, SharedString}; use smallvec::SmallVec; /// A keybinding and its associated metadata, from the keymap. pub struct KeyBinding { pub(crate) action: Box, - pub(crate) keystrokes: SmallVec<[KeybindingKeystroke; 2]>, + pub(crate) keystrokes: SmallVec<[Keystroke; 2]>, pub(crate) context_predicate: Option>, pub(crate) meta: Option, /// The json input string used when building the keybinding, if any @@ -33,15 +32,7 @@ impl KeyBinding { pub fn new(keystrokes: &str, action: A, context: Option<&str>) -> Self { let context_predicate = context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into()); - Self::load( - keystrokes, - Box::new(action), - context_predicate, - false, - None, - &DummyKeyboardMapper, - ) - .unwrap() + Self::load(keystrokes, Box::new(action), context_predicate, None, None).unwrap() } /// Load a keybinding from the given raw data. @@ -49,22 +40,24 @@ impl KeyBinding { keystrokes: &str, action: Box, context_predicate: Option>, - use_key_equivalents: bool, + key_equivalents: Option<&HashMap>, action_input: Option, - keyboard_mapper: &dyn PlatformKeyboardMapper, ) -> std::result::Result { - let keystrokes: SmallVec<[KeybindingKeystroke; 2]> = keystrokes + let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes .split_whitespace() - .map(|source| { - let keystroke = Keystroke::parse(source)?; - Ok(KeybindingKeystroke::new( - keystroke, - use_key_equivalents, - keyboard_mapper, - )) - }) + .map(Keystroke::parse) .collect::>()?; + if let Some(equivalents) = key_equivalents { + for keystroke in keystrokes.iter_mut() { + if keystroke.key.chars().count() == 1 + && let Some(key) = equivalents.get(&keystroke.key.chars().next().unwrap()) + { + keystroke.key = key.to_string(); + } + } + } + Ok(Self { keystrokes, action, @@ -86,13 +79,13 @@ impl KeyBinding { } /// Check if the given keystrokes match this binding. - pub fn match_keystrokes(&self, typed: &[impl AsKeystroke]) -> Option { + pub fn match_keystrokes(&self, typed: &[Keystroke]) -> Option { if self.keystrokes.len() < typed.len() { return None; } for (target, typed) in self.keystrokes.iter().zip(typed.iter()) { - if !typed.as_keystroke().should_match(target) { + if !typed.should_match(target) { return None; } } @@ -101,7 +94,7 @@ impl KeyBinding { } /// Get the keystrokes associated with this binding - pub fn keystrokes(&self) -> &[KeybindingKeystroke] { + pub fn keystrokes(&self) -> &[Keystroke] { self.keystrokes.as_slice() } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index f64710bc56..4d2feeaf1d 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -231,6 +231,7 @@ pub(crate) trait Platform: 'static { fn on_quit(&self, callback: Box); fn on_reopen(&self, callback: Box); + fn on_keyboard_layout_change(&self, callback: Box); fn set_menus(&self, menus: Vec, keymap: &Keymap); fn get_menus(&self) -> Option> { @@ -250,6 +251,7 @@ pub(crate) trait Platform: 'static { fn on_app_menu_action(&self, callback: Box); fn on_will_open_app_menu(&self, callback: Box); fn on_validate_app_menu_command(&self, callback: Box bool>); + fn keyboard_layout(&self) -> Box; fn compositor_name(&self) -> &'static str { "" @@ -270,10 +272,6 @@ pub(crate) trait Platform: 'static { fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task>; fn read_credentials(&self, url: &str) -> Task)>>>; fn delete_credentials(&self, url: &str) -> Task>; - - fn keyboard_layout(&self) -> Box; - fn keyboard_mapper(&self) -> Rc; - fn on_keyboard_layout_change(&self, callback: Box); } /// A handle to a platform's display, e.g. a monitor or laptop screen. diff --git a/crates/gpui/src/platform/keyboard.rs b/crates/gpui/src/platform/keyboard.rs index 10b8620258..e28d781520 100644 --- a/crates/gpui/src/platform/keyboard.rs +++ b/crates/gpui/src/platform/keyboard.rs @@ -1,7 +1,3 @@ -use collections::HashMap; - -use crate::{KeybindingKeystroke, Keystroke}; - /// A trait for platform-specific keyboard layouts pub trait PlatformKeyboardLayout { /// Get the keyboard layout ID, which should be unique to the layout @@ -9,33 +5,3 @@ pub trait PlatformKeyboardLayout { /// Get the keyboard layout display name fn name(&self) -> &str; } - -/// A trait for platform-specific keyboard mappings -pub trait PlatformKeyboardMapper { - /// Map a key equivalent to its platform-specific representation - fn map_key_equivalent( - &self, - keystroke: Keystroke, - use_key_equivalents: bool, - ) -> KeybindingKeystroke; - /// Get the key equivalents for the current keyboard layout, - /// only used on macOS - fn get_key_equivalents(&self) -> Option<&HashMap>; -} - -/// A dummy implementation of the platform keyboard mapper -pub struct DummyKeyboardMapper; - -impl PlatformKeyboardMapper for DummyKeyboardMapper { - fn map_key_equivalent( - &self, - keystroke: Keystroke, - _use_key_equivalents: bool, - ) -> KeybindingKeystroke { - KeybindingKeystroke::from_keystroke(keystroke) - } - - fn get_key_equivalents(&self) -> Option<&HashMap> { - None - } -} diff --git a/crates/gpui/src/platform/keystroke.rs b/crates/gpui/src/platform/keystroke.rs index 6ce17c3a01..24601eefd6 100644 --- a/crates/gpui/src/platform/keystroke.rs +++ b/crates/gpui/src/platform/keystroke.rs @@ -5,14 +5,6 @@ use std::{ fmt::{Display, Write}, }; -use crate::PlatformKeyboardMapper; - -/// This is a helper trait so that we can simplify the implementation of some functions -pub trait AsKeystroke { - /// Returns the GPUI representation of the keystroke. - fn as_keystroke(&self) -> &Keystroke; -} - /// A keystroke and associated metadata generated by the platform #[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)] pub struct Keystroke { @@ -32,17 +24,6 @@ pub struct Keystroke { pub key_char: Option, } -/// Represents a keystroke that can be used in keybindings and displayed to the user. -#[derive(Debug, Clone, Eq, PartialEq, Hash)] -pub struct KeybindingKeystroke { - /// The GPUI representation of the keystroke. - pub inner: Keystroke, - /// The modifiers to display. - pub display_modifiers: Modifiers, - /// The key to display. - pub display_key: String, -} - /// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use /// markdown to display it. #[derive(Debug)] @@ -77,7 +58,7 @@ impl Keystroke { /// /// This method assumes that `self` was typed and `target' is in the keymap, and checks /// both possibilities for self against the target. - pub fn should_match(&self, target: &KeybindingKeystroke) -> bool { + pub fn should_match(&self, target: &Keystroke) -> bool { #[cfg(not(target_os = "windows"))] if let Some(key_char) = self .key_char @@ -90,7 +71,7 @@ impl Keystroke { ..Default::default() }; - if &target.inner.key == key_char && target.inner.modifiers == ime_modifiers { + if &target.key == key_char && target.modifiers == ime_modifiers { return true; } } @@ -102,12 +83,12 @@ impl Keystroke { .filter(|key_char| key_char != &&self.key) { // On Windows, if key_char is set, then the typed keystroke produced the key_char - if &target.inner.key == key_char && target.inner.modifiers == Modifiers::none() { + if &target.key == key_char && target.modifiers == Modifiers::none() { return true; } } - target.inner.modifiers == self.modifiers && target.inner.key == self.key + target.modifiers == self.modifiers && target.key == self.key } /// key syntax is: @@ -219,7 +200,31 @@ impl Keystroke { /// Produces a representation of this key that Parse can understand. pub fn unparse(&self) -> String { - unparse(&self.modifiers, &self.key) + let mut str = String::new(); + if self.modifiers.function { + str.push_str("fn-"); + } + if self.modifiers.control { + str.push_str("ctrl-"); + } + if self.modifiers.alt { + str.push_str("alt-"); + } + if self.modifiers.platform { + #[cfg(target_os = "macos")] + str.push_str("cmd-"); + + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + str.push_str("super-"); + + #[cfg(target_os = "windows")] + str.push_str("win-"); + } + if self.modifiers.shift { + str.push_str("shift-"); + } + str.push_str(&self.key); + str } /// Returns true if this keystroke left @@ -261,32 +266,6 @@ impl Keystroke { } } -impl KeybindingKeystroke { - /// Create a new keybinding keystroke from the given keystroke - pub fn new( - inner: Keystroke, - use_key_equivalents: bool, - keyboard_mapper: &dyn PlatformKeyboardMapper, - ) -> Self { - keyboard_mapper.map_key_equivalent(inner, use_key_equivalents) - } - - pub(crate) fn from_keystroke(keystroke: Keystroke) -> Self { - let key = keystroke.key.clone(); - let modifiers = keystroke.modifiers; - KeybindingKeystroke { - inner: keystroke, - display_modifiers: modifiers, - display_key: key, - } - } - - /// Produces a representation of this key that Parse can understand. - pub fn unparse(&self) -> String { - unparse(&self.display_modifiers, &self.display_key) - } -} - fn is_printable_key(key: &str) -> bool { !matches!( key, @@ -343,15 +322,65 @@ fn is_printable_key(key: &str) -> bool { impl std::fmt::Display for Keystroke { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - display_modifiers(&self.modifiers, f)?; - display_key(&self.key, f) - } -} + if self.modifiers.control { + #[cfg(target_os = "macos")] + f.write_char('^')?; -impl std::fmt::Display for KeybindingKeystroke { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - display_modifiers(&self.display_modifiers, f)?; - display_key(&self.display_key, f) + #[cfg(not(target_os = "macos"))] + write!(f, "ctrl-")?; + } + if self.modifiers.alt { + #[cfg(target_os = "macos")] + f.write_char('⌥')?; + + #[cfg(not(target_os = "macos"))] + write!(f, "alt-")?; + } + if self.modifiers.platform { + #[cfg(target_os = "macos")] + f.write_char('⌘')?; + + #[cfg(any(target_os = "linux", target_os = "freebsd"))] + f.write_char('❖')?; + + #[cfg(target_os = "windows")] + f.write_char('⊞')?; + } + if self.modifiers.shift { + #[cfg(target_os = "macos")] + f.write_char('⇧')?; + + #[cfg(not(target_os = "macos"))] + write!(f, "shift-")?; + } + let key = match self.key.as_str() { + #[cfg(target_os = "macos")] + "backspace" => '⌫', + #[cfg(target_os = "macos")] + "up" => '↑', + #[cfg(target_os = "macos")] + "down" => '↓', + #[cfg(target_os = "macos")] + "left" => '←', + #[cfg(target_os = "macos")] + "right" => '→', + #[cfg(target_os = "macos")] + "tab" => '⇥', + #[cfg(target_os = "macos")] + "escape" => '⎋', + #[cfg(target_os = "macos")] + "shift" => '⇧', + #[cfg(target_os = "macos")] + "control" => '⌃', + #[cfg(target_os = "macos")] + "alt" => '⌥', + #[cfg(target_os = "macos")] + "platform" => '⌘', + + key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(), + key => return f.write_str(key), + }; + f.write_char(key) } } @@ -571,110 +600,3 @@ pub struct Capslock { #[serde(default)] pub on: bool, } - -impl AsKeystroke for Keystroke { - fn as_keystroke(&self) -> &Keystroke { - self - } -} - -impl AsKeystroke for KeybindingKeystroke { - fn as_keystroke(&self) -> &Keystroke { - &self.inner - } -} - -fn display_modifiers(modifiers: &Modifiers, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if modifiers.control { - #[cfg(target_os = "macos")] - f.write_char('^')?; - - #[cfg(not(target_os = "macos"))] - write!(f, "ctrl-")?; - } - if modifiers.alt { - #[cfg(target_os = "macos")] - f.write_char('⌥')?; - - #[cfg(not(target_os = "macos"))] - write!(f, "alt-")?; - } - if modifiers.platform { - #[cfg(target_os = "macos")] - f.write_char('⌘')?; - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - f.write_char('❖')?; - - #[cfg(target_os = "windows")] - f.write_char('⊞')?; - } - if modifiers.shift { - #[cfg(target_os = "macos")] - f.write_char('⇧')?; - - #[cfg(not(target_os = "macos"))] - write!(f, "shift-")?; - } - Ok(()) -} - -fn display_key(key: &str, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let key = match key { - #[cfg(target_os = "macos")] - "backspace" => '⌫', - #[cfg(target_os = "macos")] - "up" => '↑', - #[cfg(target_os = "macos")] - "down" => '↓', - #[cfg(target_os = "macos")] - "left" => '←', - #[cfg(target_os = "macos")] - "right" => '→', - #[cfg(target_os = "macos")] - "tab" => '⇥', - #[cfg(target_os = "macos")] - "escape" => '⎋', - #[cfg(target_os = "macos")] - "shift" => '⇧', - #[cfg(target_os = "macos")] - "control" => '⌃', - #[cfg(target_os = "macos")] - "alt" => '⌥', - #[cfg(target_os = "macos")] - "platform" => '⌘', - - key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(), - key => return f.write_str(key), - }; - f.write_char(key) -} - -#[inline] -fn unparse(modifiers: &Modifiers, key: &str) -> String { - let mut result = String::new(); - if modifiers.function { - result.push_str("fn-"); - } - if modifiers.control { - result.push_str("ctrl-"); - } - if modifiers.alt { - result.push_str("alt-"); - } - if modifiers.platform { - #[cfg(target_os = "macos")] - result.push_str("cmd-"); - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - result.push_str("super-"); - - #[cfg(target_os = "windows")] - result.push_str("win-"); - } - if modifiers.shift { - result.push_str("shift-"); - } - result.push_str(&key); - result -} diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 8bd89fc399..3fb1ef4572 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -25,8 +25,8 @@ use xkbcommon::xkb::{self, Keycode, Keysym, State}; use crate::{ Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor, Keymap, LinuxDispatcher, Menu, MenuItem, OwnedMenu, PathPromptOptions, - Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, - PlatformTextSystem, PlatformWindow, Point, Result, Task, WindowAppearance, WindowParams, px, + Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, + Point, Result, Task, WindowAppearance, WindowParams, px, }; #[cfg(any(feature = "wayland", feature = "x11"))] @@ -144,10 +144,6 @@ impl Platform for P { self.keyboard_layout() } - fn keyboard_mapper(&self) -> Rc { - Rc::new(crate::DummyKeyboardMapper) - } - fn on_keyboard_layout_change(&self, callback: Box) { self.with_common(|common| common.callbacks.keyboard_layout_change = Some(callback)); } diff --git a/crates/gpui/src/platform/mac/keyboard.rs b/crates/gpui/src/platform/mac/keyboard.rs index 1409731246..a9f6af3edb 100644 --- a/crates/gpui/src/platform/mac/keyboard.rs +++ b/crates/gpui/src/platform/mac/keyboard.rs @@ -1,9 +1,8 @@ -use collections::HashMap; use std::ffi::{CStr, c_void}; use objc::{msg_send, runtime::Object, sel, sel_impl}; -use crate::{KeybindingKeystroke, Keystroke, PlatformKeyboardLayout, PlatformKeyboardMapper}; +use crate::PlatformKeyboardLayout; use super::{ TISCopyCurrentKeyboardLayoutInputSource, TISGetInputSourceProperty, kTISPropertyInputSourceID, @@ -15,10 +14,6 @@ pub(crate) struct MacKeyboardLayout { name: String, } -pub(crate) struct MacKeyboardMapper { - key_equivalents: Option>, -} - impl PlatformKeyboardLayout for MacKeyboardLayout { fn id(&self) -> &str { &self.id @@ -29,27 +24,6 @@ impl PlatformKeyboardLayout for MacKeyboardLayout { } } -impl PlatformKeyboardMapper for MacKeyboardMapper { - fn map_key_equivalent( - &self, - mut keystroke: Keystroke, - use_key_equivalents: bool, - ) -> KeybindingKeystroke { - if use_key_equivalents && let Some(key_equivalents) = &self.key_equivalents { - if keystroke.key.chars().count() == 1 - && let Some(key) = key_equivalents.get(&keystroke.key.chars().next().unwrap()) - { - keystroke.key = key.to_string(); - } - } - KeybindingKeystroke::from_keystroke(keystroke) - } - - fn get_key_equivalents(&self) -> Option<&HashMap> { - self.key_equivalents.as_ref() - } -} - impl MacKeyboardLayout { pub(crate) fn new() -> Self { unsafe { @@ -73,1428 +47,3 @@ impl MacKeyboardLayout { } } } - -impl MacKeyboardMapper { - pub(crate) fn new(layout_id: &str) -> Self { - let key_equivalents = get_key_equivalents(layout_id); - - Self { key_equivalents } - } -} - -// On some keyboards (e.g. German QWERTZ) it is not possible to type the full ASCII range -// without using option. This means that some of our built in keyboard shortcuts do not work -// for those users. -// -// The way macOS solves this problem is to move shortcuts around so that they are all reachable, -// even if the mnemonic changes. https://developer.apple.com/documentation/swiftui/keyboardshortcut/localization-swift.struct -// -// For example, cmd-> is the "switch window" shortcut because the > key is right above tab. -// To ensure this doesn't cause problems for shortcuts defined for a QWERTY layout, apple moves -// any shortcuts defined as cmd-> to cmd-:. Coincidentally this s also the same keyboard position -// as cmd-> on a QWERTY layout. -// -// Another example is cmd-[ and cmd-], as they cannot be typed without option, those keys are remapped to cmd-ö -// and cmd-ä. These shortcuts are not in the same position as a QWERTY keyboard, because on a QWERTZ keyboard -// the + key is in the way; and shortcuts bound to cmd-+ are still typed as cmd-+ on either keyboard (though the -// specific key moves) -// -// As far as I can tell, there's no way to query the mappings Apple uses except by rendering a menu with every -// possible key combination, and inspecting the UI to see what it rendered. So that's what we did... -// -// These mappings were generated by running https://github.com/ConradIrwin/keyboard-inspector, tidying up the -// output to remove languages with no mappings and other oddities, and converting it to a less verbose representation with: -// jq -s 'map(to_entries | map({key: .key, value: [(.value | to_entries | map(.key) | join("")), (.value | to_entries | map(.value) | join(""))]}) | from_entries) | add' -// From there I used multi-cursor to produce this match statement. -fn get_key_equivalents(layout_id: &str) -> Option> { - let mappings: &[(char, char)] = match layout_id { - "com.apple.keylayout.ABC-AZERTY" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('.', ';'), - ('/', ':'), - ('0', 'à'), - ('1', '&'), - ('2', 'é'), - ('3', '"'), - ('4', '\''), - ('5', '('), - ('6', '§'), - ('7', 'è'), - ('8', '!'), - ('9', 'ç'), - (':', '°'), - (';', ')'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', '^'), - ('\'', 'ù'), - ('\\', '`'), - (']', '$'), - ('^', '6'), - ('`', '<'), - ('{', '¨'), - ('|', '£'), - ('}', '*'), - ('~', '>'), - ], - "com.apple.keylayout.ABC-QWERTZ" => &[ - ('"', '`'), - ('#', '§'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', 'ß'), - (':', 'Ü'), - (';', 'ü'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '´'), - ('\\', '#'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '\''), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.Albanian" => &[ - ('"', '\''), - (':', 'Ç'), - (';', 'ç'), - ('<', ';'), - ('>', ':'), - ('@', '"'), - ('\'', '@'), - ('\\', 'ë'), - ('`', '<'), - ('|', 'Ë'), - ('~', '>'), - ], - "com.apple.keylayout.Austrian" => &[ - ('"', '`'), - ('#', '§'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', 'ß'), - (':', 'Ü'), - (';', 'ü'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '´'), - ('\\', '#'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '\''), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.Azeri" => &[ - ('"', 'Ə'), - (',', 'ç'), - ('.', 'ş'), - ('/', '.'), - (':', 'I'), - (';', 'ı'), - ('<', 'Ç'), - ('>', 'Ş'), - ('?', ','), - ('W', 'Ü'), - ('[', 'ö'), - ('\'', 'ə'), - (']', 'ğ'), - ('w', 'ü'), - ('{', 'Ö'), - ('|', '/'), - ('}', 'Ğ'), - ], - "com.apple.keylayout.Belgian" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('.', ';'), - ('/', ':'), - ('0', 'à'), - ('1', '&'), - ('2', 'é'), - ('3', '"'), - ('4', '\''), - ('5', '('), - ('6', '§'), - ('7', 'è'), - ('8', '!'), - ('9', 'ç'), - (':', '°'), - (';', ')'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', '^'), - ('\'', 'ù'), - ('\\', '`'), - (']', '$'), - ('^', '6'), - ('`', '<'), - ('{', '¨'), - ('|', '£'), - ('}', '*'), - ('~', '>'), - ], - "com.apple.keylayout.Brazilian-ABNT2" => &[ - ('"', '`'), - ('/', 'ç'), - ('?', 'Ç'), - ('\'', '´'), - ('\\', '~'), - ('^', '¨'), - ('`', '\''), - ('|', '^'), - ('~', '"'), - ], - "com.apple.keylayout.Brazilian-Pro" => &[('^', 'ˆ'), ('~', '˜')], - "com.apple.keylayout.British" => &[('#', '£')], - "com.apple.keylayout.Canadian-CSA" => &[ - ('"', 'È'), - ('/', 'é'), - ('<', '\''), - ('>', '"'), - ('?', 'É'), - ('[', '^'), - ('\'', 'è'), - ('\\', 'à'), - (']', 'ç'), - ('`', 'ù'), - ('{', '¨'), - ('|', 'À'), - ('}', 'Ç'), - ('~', 'Ù'), - ], - "com.apple.keylayout.Croatian" => &[ - ('"', 'Ć'), - ('&', '\''), - ('(', ')'), - (')', '='), - ('*', '('), - (':', 'Č'), - (';', 'č'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'š'), - ('\'', 'ć'), - ('\\', 'ž'), - (']', 'đ'), - ('^', '&'), - ('`', '<'), - ('{', 'Š'), - ('|', 'Ž'), - ('}', 'Đ'), - ('~', '>'), - ], - "com.apple.keylayout.Croatian-PC" => &[ - ('"', 'Ć'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '\''), - (':', 'Č'), - (';', 'č'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'š'), - ('\'', 'ć'), - ('\\', 'ž'), - (']', 'đ'), - ('^', '&'), - ('`', '<'), - ('{', 'Š'), - ('|', 'Ž'), - ('}', 'Đ'), - ('~', '>'), - ], - "com.apple.keylayout.Czech" => &[ - ('!', '1'), - ('"', '!'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('+', '%'), - ('/', '\''), - ('0', 'é'), - ('1', '+'), - ('2', 'ě'), - ('3', 'š'), - ('4', 'č'), - ('5', 'ř'), - ('6', 'ž'), - ('7', 'ý'), - ('8', 'á'), - ('9', 'í'), - (':', '"'), - (';', 'ů'), - ('<', '?'), - ('>', ':'), - ('?', 'ˇ'), - ('@', '2'), - ('[', 'ú'), - ('\'', '§'), - (']', ')'), - ('^', '6'), - ('`', '¨'), - ('{', 'Ú'), - ('}', '('), - ('~', '`'), - ], - "com.apple.keylayout.Czech-QWERTY" => &[ - ('!', '1'), - ('"', '!'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('+', '%'), - ('/', '\''), - ('0', 'é'), - ('1', '+'), - ('2', 'ě'), - ('3', 'š'), - ('4', 'č'), - ('5', 'ř'), - ('6', 'ž'), - ('7', 'ý'), - ('8', 'á'), - ('9', 'í'), - (':', '"'), - (';', 'ů'), - ('<', '?'), - ('>', ':'), - ('?', 'ˇ'), - ('@', '2'), - ('[', 'ú'), - ('\'', '§'), - (']', ')'), - ('^', '6'), - ('`', '¨'), - ('{', 'Ú'), - ('}', '('), - ('~', '`'), - ], - "com.apple.keylayout.Danish" => &[ - ('"', '^'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'æ'), - ('\'', '¨'), - ('\\', '\''), - (']', 'ø'), - ('^', '&'), - ('`', '<'), - ('{', 'Æ'), - ('|', '*'), - ('}', 'Ø'), - ('~', '>'), - ], - "com.apple.keylayout.Faroese" => &[ - ('"', 'Ø'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Æ'), - (';', 'æ'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'å'), - ('\'', 'ø'), - ('\\', '\''), - (']', 'ð'), - ('^', '&'), - ('`', '<'), - ('{', 'Å'), - ('|', '*'), - ('}', 'Ð'), - ('~', '>'), - ], - "com.apple.keylayout.Finnish" => &[ - ('"', '^'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '\''), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.FinnishExtended" => &[ - ('"', 'ˆ'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '\''), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.FinnishSami-PC" => &[ - ('"', 'ˆ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '@'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.French" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('.', ';'), - ('/', ':'), - ('0', 'à'), - ('1', '&'), - ('2', 'é'), - ('3', '"'), - ('4', '\''), - ('5', '('), - ('6', '§'), - ('7', 'è'), - ('8', '!'), - ('9', 'ç'), - (':', '°'), - (';', ')'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', '^'), - ('\'', 'ù'), - ('\\', '`'), - (']', '$'), - ('^', '6'), - ('`', '<'), - ('{', '¨'), - ('|', '£'), - ('}', '*'), - ('~', '>'), - ], - "com.apple.keylayout.French-PC" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('-', ')'), - ('.', ';'), - ('/', ':'), - ('0', 'à'), - ('1', '&'), - ('2', 'é'), - ('3', '"'), - ('4', '\''), - ('5', '('), - ('6', '-'), - ('7', 'è'), - ('8', '_'), - ('9', 'ç'), - (':', '§'), - (';', '!'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', '^'), - ('\'', 'ù'), - ('\\', '*'), - (']', '$'), - ('^', '6'), - ('_', '°'), - ('`', '<'), - ('{', '¨'), - ('|', 'μ'), - ('}', '£'), - ('~', '>'), - ], - "com.apple.keylayout.French-numerical" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('.', ';'), - ('/', ':'), - ('0', 'à'), - ('1', '&'), - ('2', 'é'), - ('3', '"'), - ('4', '\''), - ('5', '('), - ('6', '§'), - ('7', 'è'), - ('8', '!'), - ('9', 'ç'), - (':', '°'), - (';', ')'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', '^'), - ('\'', 'ù'), - ('\\', '`'), - (']', '$'), - ('^', '6'), - ('`', '<'), - ('{', '¨'), - ('|', '£'), - ('}', '*'), - ('~', '>'), - ], - "com.apple.keylayout.German" => &[ - ('"', '`'), - ('#', '§'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', 'ß'), - (':', 'Ü'), - (';', 'ü'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '´'), - ('\\', '#'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '\''), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.German-DIN-2137" => &[ - ('"', '`'), - ('#', '§'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', 'ß'), - (':', 'Ü'), - (';', 'ü'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '´'), - ('\\', '#'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '\''), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.Hawaiian" => &[('\'', 'ʻ')], - "com.apple.keylayout.Hungarian" => &[ - ('!', '\''), - ('"', 'Á'), - ('#', '+'), - ('$', '!'), - ('&', '='), - ('(', ')'), - (')', 'Ö'), - ('*', '('), - ('+', 'Ó'), - ('/', 'ü'), - ('0', 'ö'), - (':', 'É'), - (';', 'é'), - ('<', 'Ü'), - ('=', 'ó'), - ('>', ':'), - ('@', '"'), - ('[', 'ő'), - ('\'', 'á'), - ('\\', 'ű'), - (']', 'ú'), - ('^', '/'), - ('`', 'í'), - ('{', 'Ő'), - ('|', 'Ű'), - ('}', 'Ú'), - ('~', 'Í'), - ], - "com.apple.keylayout.Hungarian-QWERTY" => &[ - ('!', '\''), - ('"', 'Á'), - ('#', '+'), - ('$', '!'), - ('&', '='), - ('(', ')'), - (')', 'Ö'), - ('*', '('), - ('+', 'Ó'), - ('/', 'ü'), - ('0', 'ö'), - (':', 'É'), - (';', 'é'), - ('<', 'Ü'), - ('=', 'ó'), - ('>', ':'), - ('@', '"'), - ('[', 'ő'), - ('\'', 'á'), - ('\\', 'ű'), - (']', 'ú'), - ('^', '/'), - ('`', 'í'), - ('{', 'Ő'), - ('|', 'Ű'), - ('}', 'Ú'), - ('~', 'Í'), - ], - "com.apple.keylayout.Icelandic" => &[ - ('"', 'Ö'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '\''), - (':', 'Ð'), - (';', 'ð'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'æ'), - ('\'', 'ö'), - ('\\', 'þ'), - (']', '´'), - ('^', '&'), - ('`', '<'), - ('{', 'Æ'), - ('|', 'Þ'), - ('}', '´'), - ('~', '>'), - ], - "com.apple.keylayout.Irish" => &[('#', '£')], - "com.apple.keylayout.IrishExtended" => &[('#', '£')], - "com.apple.keylayout.Italian" => &[ - ('!', '1'), - ('"', '%'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - (',', ';'), - ('.', ':'), - ('/', ','), - ('0', 'é'), - ('1', '&'), - ('2', '"'), - ('3', '\''), - ('4', '('), - ('5', 'ç'), - ('6', 'è'), - ('7', ')'), - ('8', '£'), - ('9', 'à'), - (':', '!'), - (';', 'ò'), - ('<', '.'), - ('>', '/'), - ('@', '2'), - ('[', 'ì'), - ('\'', 'ù'), - ('\\', '§'), - (']', '$'), - ('^', '6'), - ('`', '<'), - ('{', '^'), - ('|', '°'), - ('}', '*'), - ('~', '>'), - ], - "com.apple.keylayout.Italian-Pro" => &[ - ('"', '^'), - ('#', '£'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '\''), - (':', 'é'), - (';', 'è'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ò'), - ('\'', 'ì'), - ('\\', 'ù'), - (']', 'à'), - ('^', '&'), - ('`', '<'), - ('{', 'ç'), - ('|', '§'), - ('}', '°'), - ('~', '>'), - ], - "com.apple.keylayout.LatinAmerican" => &[ - ('"', '¨'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '\''), - (':', 'Ñ'), - (';', 'ñ'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', '{'), - ('\'', '´'), - ('\\', '¿'), - (']', '}'), - ('^', '&'), - ('`', '<'), - ('{', '['), - ('|', '¡'), - ('}', ']'), - ('~', '>'), - ], - "com.apple.keylayout.Lithuanian" => &[ - ('!', 'Ą'), - ('#', 'Ę'), - ('$', 'Ė'), - ('%', 'Į'), - ('&', 'Ų'), - ('*', 'Ū'), - ('+', 'Ž'), - ('1', 'ą'), - ('2', 'č'), - ('3', 'ę'), - ('4', 'ė'), - ('5', 'į'), - ('6', 'š'), - ('7', 'ų'), - ('8', 'ū'), - ('=', 'ž'), - ('@', 'Č'), - ('^', 'Š'), - ], - "com.apple.keylayout.Maltese" => &[ - ('#', '£'), - ('[', 'ġ'), - (']', 'ħ'), - ('`', 'ż'), - ('{', 'Ġ'), - ('}', 'Ħ'), - ('~', 'Ż'), - ], - "com.apple.keylayout.NorthernSami" => &[ - ('"', 'Ŋ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('Q', 'Á'), - ('W', 'Š'), - ('X', 'Č'), - ('[', 'ø'), - ('\'', 'ŋ'), - ('\\', 'đ'), - (']', 'æ'), - ('^', '&'), - ('`', 'ž'), - ('q', 'á'), - ('w', 'š'), - ('x', 'č'), - ('{', 'Ø'), - ('|', 'Đ'), - ('}', 'Æ'), - ('~', 'Ž'), - ], - "com.apple.keylayout.Norwegian" => &[ - ('"', '^'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ø'), - ('\'', '¨'), - ('\\', '@'), - (']', 'æ'), - ('^', '&'), - ('`', '<'), - ('{', 'Ø'), - ('|', '*'), - ('}', 'Æ'), - ('~', '>'), - ], - "com.apple.keylayout.NorwegianExtended" => &[ - ('"', 'ˆ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ø'), - ('\\', '@'), - (']', 'æ'), - ('`', '<'), - ('}', 'Æ'), - ('~', '>'), - ], - "com.apple.keylayout.NorwegianSami-PC" => &[ - ('"', 'ˆ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ø'), - ('\'', '¨'), - ('\\', '@'), - (']', 'æ'), - ('^', '&'), - ('`', '<'), - ('{', 'Ø'), - ('|', '*'), - ('}', 'Æ'), - ('~', '>'), - ], - "com.apple.keylayout.Polish" => &[ - ('!', '§'), - ('"', 'ę'), - ('#', '!'), - ('$', '?'), - ('%', '+'), - ('&', ':'), - ('(', '/'), - (')', '"'), - ('*', '_'), - ('+', ']'), - (',', '.'), - ('.', ','), - ('/', 'ż'), - (':', 'Ł'), - (';', 'ł'), - ('<', 'ś'), - ('=', '['), - ('>', 'ń'), - ('?', 'Ż'), - ('@', '%'), - ('[', 'ó'), - ('\'', 'ą'), - ('\\', ';'), - (']', '('), - ('^', '='), - ('_', 'ć'), - ('`', '<'), - ('{', 'ź'), - ('|', '$'), - ('}', ')'), - ('~', '>'), - ], - "com.apple.keylayout.Portuguese" => &[ - ('"', '`'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '\''), - (':', 'ª'), - (';', 'º'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'ç'), - ('\'', '´'), - (']', '~'), - ('^', '&'), - ('`', '<'), - ('{', 'Ç'), - ('}', '^'), - ('~', '>'), - ], - "com.apple.keylayout.Sami-PC" => &[ - ('"', 'Ŋ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('Q', 'Á'), - ('W', 'Š'), - ('X', 'Č'), - ('[', 'ø'), - ('\'', 'ŋ'), - ('\\', 'đ'), - (']', 'æ'), - ('^', '&'), - ('`', 'ž'), - ('q', 'á'), - ('w', 'š'), - ('x', 'č'), - ('{', 'Ø'), - ('|', 'Đ'), - ('}', 'Æ'), - ('~', 'Ž'), - ], - "com.apple.keylayout.Serbian-Latin" => &[ - ('"', 'Ć'), - ('&', '\''), - ('(', ')'), - (')', '='), - ('*', '('), - (':', 'Č'), - (';', 'č'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'š'), - ('\'', 'ć'), - ('\\', 'ž'), - (']', 'đ'), - ('^', '&'), - ('`', '<'), - ('{', 'Š'), - ('|', 'Ž'), - ('}', 'Đ'), - ('~', '>'), - ], - "com.apple.keylayout.Slovak" => &[ - ('!', '1'), - ('"', '!'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('+', '%'), - ('/', '\''), - ('0', 'é'), - ('1', '+'), - ('2', 'ľ'), - ('3', 'š'), - ('4', 'č'), - ('5', 'ť'), - ('6', 'ž'), - ('7', 'ý'), - ('8', 'á'), - ('9', 'í'), - (':', '"'), - (';', 'ô'), - ('<', '?'), - ('>', ':'), - ('?', 'ˇ'), - ('@', '2'), - ('[', 'ú'), - ('\'', '§'), - (']', 'ä'), - ('^', '6'), - ('`', 'ň'), - ('{', 'Ú'), - ('}', 'Ä'), - ('~', 'Ň'), - ], - "com.apple.keylayout.Slovak-QWERTY" => &[ - ('!', '1'), - ('"', '!'), - ('#', '3'), - ('$', '4'), - ('%', '5'), - ('&', '7'), - ('(', '9'), - (')', '0'), - ('*', '8'), - ('+', '%'), - ('/', '\''), - ('0', 'é'), - ('1', '+'), - ('2', 'ľ'), - ('3', 'š'), - ('4', 'č'), - ('5', 'ť'), - ('6', 'ž'), - ('7', 'ý'), - ('8', 'á'), - ('9', 'í'), - (':', '"'), - (';', 'ô'), - ('<', '?'), - ('>', ':'), - ('?', 'ˇ'), - ('@', '2'), - ('[', 'ú'), - ('\'', '§'), - (']', 'ä'), - ('^', '6'), - ('`', 'ň'), - ('{', 'Ú'), - ('}', 'Ä'), - ('~', 'Ň'), - ], - "com.apple.keylayout.Slovenian" => &[ - ('"', 'Ć'), - ('&', '\''), - ('(', ')'), - (')', '='), - ('*', '('), - (':', 'Č'), - (';', 'č'), - ('<', ';'), - ('=', '*'), - ('>', ':'), - ('@', '"'), - ('[', 'š'), - ('\'', 'ć'), - ('\\', 'ž'), - (']', 'đ'), - ('^', '&'), - ('`', '<'), - ('{', 'Š'), - ('|', 'Ž'), - ('}', 'Đ'), - ('~', '>'), - ], - "com.apple.keylayout.Spanish" => &[ - ('!', '¡'), - ('"', '¨'), - ('.', 'ç'), - ('/', '.'), - (':', 'º'), - (';', '´'), - ('<', '¿'), - ('>', 'Ç'), - ('@', '!'), - ('[', 'ñ'), - ('\'', '`'), - ('\\', '\''), - (']', ';'), - ('^', '/'), - ('`', '<'), - ('{', 'Ñ'), - ('|', '"'), - ('}', ':'), - ('~', '>'), - ], - "com.apple.keylayout.Spanish-ISO" => &[ - ('"', '¨'), - ('#', '·'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('.', 'ç'), - ('/', '.'), - (':', 'º'), - (';', '´'), - ('<', '¿'), - ('>', 'Ç'), - ('@', '"'), - ('[', 'ñ'), - ('\'', '`'), - ('\\', '\''), - (']', ';'), - ('^', '&'), - ('`', '<'), - ('{', 'Ñ'), - ('|', '"'), - ('}', '`'), - ('~', '>'), - ], - "com.apple.keylayout.Swedish" => &[ - ('"', '^'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '\''), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.Swedish-Pro" => &[ - ('"', '^'), - ('$', '€'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '\''), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.SwedishSami-PC" => &[ - ('"', 'ˆ'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('/', '´'), - (':', 'Å'), - (';', 'å'), - ('<', ';'), - ('=', '`'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '¨'), - ('\\', '@'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'Ö'), - ('|', '*'), - ('}', 'Ä'), - ('~', '>'), - ], - "com.apple.keylayout.SwissFrench" => &[ - ('!', '+'), - ('"', '`'), - ('#', '*'), - ('$', 'ç'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('+', '!'), - ('/', '\''), - (':', 'ü'), - (';', 'è'), - ('<', ';'), - ('=', '¨'), - ('>', ':'), - ('@', '"'), - ('[', 'é'), - ('\'', '^'), - ('\\', '$'), - (']', 'à'), - ('^', '&'), - ('`', '<'), - ('{', 'ö'), - ('|', '£'), - ('}', 'ä'), - ('~', '>'), - ], - "com.apple.keylayout.SwissGerman" => &[ - ('!', '+'), - ('"', '`'), - ('#', '*'), - ('$', 'ç'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('+', '!'), - ('/', '\''), - (':', 'è'), - (';', 'ü'), - ('<', ';'), - ('=', '¨'), - ('>', ':'), - ('@', '"'), - ('[', 'ö'), - ('\'', '^'), - ('\\', '$'), - (']', 'ä'), - ('^', '&'), - ('`', '<'), - ('{', 'é'), - ('|', '£'), - ('}', 'à'), - ('~', '>'), - ], - "com.apple.keylayout.Turkish" => &[ - ('"', '-'), - ('#', '"'), - ('$', '\''), - ('%', '('), - ('&', ')'), - ('(', '%'), - (')', ':'), - ('*', '_'), - (',', 'ö'), - ('-', 'ş'), - ('.', 'ç'), - ('/', '.'), - (':', '$'), - ('<', 'Ö'), - ('>', 'Ç'), - ('@', '*'), - ('[', 'ğ'), - ('\'', ','), - ('\\', 'ü'), - (']', 'ı'), - ('^', '/'), - ('_', 'Ş'), - ('`', '<'), - ('{', 'Ğ'), - ('|', 'Ü'), - ('}', 'I'), - ('~', '>'), - ], - "com.apple.keylayout.Turkish-QWERTY-PC" => &[ - ('"', 'I'), - ('#', '^'), - ('$', '+'), - ('&', '/'), - ('(', ')'), - (')', '='), - ('*', '('), - ('+', ':'), - (',', 'ö'), - ('.', 'ç'), - ('/', '*'), - (':', 'Ş'), - (';', 'ş'), - ('<', 'Ö'), - ('=', '.'), - ('>', 'Ç'), - ('@', '\''), - ('[', 'ğ'), - ('\'', 'ı'), - ('\\', ','), - (']', 'ü'), - ('^', '&'), - ('`', '<'), - ('{', 'Ğ'), - ('|', ';'), - ('}', 'Ü'), - ('~', '>'), - ], - "com.apple.keylayout.Turkish-Standard" => &[ - ('"', 'Ş'), - ('#', '^'), - ('&', '\''), - ('(', ')'), - (')', '='), - ('*', '('), - (',', '.'), - ('.', ','), - (':', 'Ç'), - (';', 'ç'), - ('<', ':'), - ('=', '*'), - ('>', ';'), - ('@', '"'), - ('[', 'ğ'), - ('\'', 'ş'), - ('\\', 'ü'), - (']', 'ı'), - ('^', '&'), - ('`', 'ö'), - ('{', 'Ğ'), - ('|', 'Ü'), - ('}', 'I'), - ('~', 'Ö'), - ], - "com.apple.keylayout.Turkmen" => &[ - ('C', 'Ç'), - ('Q', 'Ä'), - ('V', 'Ý'), - ('X', 'Ü'), - ('[', 'ň'), - ('\\', 'ş'), - (']', 'ö'), - ('^', '№'), - ('`', 'ž'), - ('c', 'ç'), - ('q', 'ä'), - ('v', 'ý'), - ('x', 'ü'), - ('{', 'Ň'), - ('|', 'Ş'), - ('}', 'Ö'), - ('~', 'Ž'), - ], - "com.apple.keylayout.USInternational-PC" => &[('^', 'ˆ'), ('~', '˜')], - "com.apple.keylayout.Welsh" => &[('#', '£')], - - _ => return None, - }; - - Some(HashMap::from_iter(mappings.iter().cloned())) -} diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 30453def00..832550dc46 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -1,5 +1,5 @@ use super::{ - BoolExt, MacKeyboardLayout, MacKeyboardMapper, + BoolExt, MacKeyboardLayout, attributed_string::{NSAttributedString, NSMutableAttributedString}, events::key_to_native, renderer, @@ -8,9 +8,8 @@ use crate::{ Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString, CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher, MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions, Platform, - PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, - PlatformWindow, Result, SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams, - hash, + PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result, + SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams, hash, }; use anyhow::{Context as _, anyhow}; use block::ConcreteBlock; @@ -172,7 +171,6 @@ pub(crate) struct MacPlatformState { finish_launching: Option>, dock_menu: Option, menus: Option>, - keyboard_mapper: Rc, } impl Default for MacPlatform { @@ -191,9 +189,6 @@ impl MacPlatform { #[cfg(not(feature = "font-kit"))] let text_system = Arc::new(crate::NoopTextSystem::new()); - let keyboard_layout = MacKeyboardLayout::new(); - let keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id())); - Self(Mutex::new(MacPlatformState { headless, text_system, @@ -214,7 +209,6 @@ impl MacPlatform { dock_menu: None, on_keyboard_layout_change: None, menus: None, - keyboard_mapper, })) } @@ -354,19 +348,19 @@ impl MacPlatform { let mut mask = NSEventModifierFlags::empty(); for (modifier, flag) in &[ ( - keystroke.display_modifiers.platform, + keystroke.modifiers.platform, NSEventModifierFlags::NSCommandKeyMask, ), ( - keystroke.display_modifiers.control, + keystroke.modifiers.control, NSEventModifierFlags::NSControlKeyMask, ), ( - keystroke.display_modifiers.alt, + keystroke.modifiers.alt, NSEventModifierFlags::NSAlternateKeyMask, ), ( - keystroke.display_modifiers.shift, + keystroke.modifiers.shift, NSEventModifierFlags::NSShiftKeyMask, ), ] { @@ -379,7 +373,7 @@ impl MacPlatform { .initWithTitle_action_keyEquivalent_( ns_string(name), selector, - ns_string(key_to_native(&keystroke.display_key).as_ref()), + ns_string(key_to_native(&keystroke.key).as_ref()), ) .autorelease(); if Self::os_version() >= SemanticVersion::new(12, 0, 0) { @@ -888,10 +882,6 @@ impl Platform for MacPlatform { Box::new(MacKeyboardLayout::new()) } - fn keyboard_mapper(&self) -> Rc { - self.0.lock().keyboard_mapper.clone() - } - fn app_path(&self) -> Result { unsafe { let bundle: id = NSBundle::mainBundle(); @@ -1403,8 +1393,6 @@ extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) { extern "C" fn on_keyboard_layout_change(this: &mut Object, _: Sel, _: id) { let platform = unsafe { get_mac_platform(this) }; let mut lock = platform.0.lock(); - let keyboard_layout = MacKeyboardLayout::new(); - lock.keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id())); if let Some(mut callback) = lock.on_keyboard_layout_change.take() { drop(lock); callback(); diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index 15b909199f..00afcd81b5 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -1,9 +1,8 @@ use crate::{ AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels, - DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, - PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PromptButton, - ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata, Task, - TestDisplay, TestWindow, WindowAppearance, WindowParams, size, + ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout, + PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, + SourceMetadata, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size, }; use anyhow::Result; use collections::VecDeque; @@ -238,10 +237,6 @@ impl Platform for TestPlatform { Box::new(TestKeyboardLayout) } - fn keyboard_mapper(&self) -> Rc { - Rc::new(DummyKeyboardMapper) - } - fn on_keyboard_layout_change(&self, _: Box) {} fn run(&self, _on_finish_launching: Box) { diff --git a/crates/gpui/src/platform/windows/dispatcher.rs b/crates/gpui/src/platform/windows/dispatcher.rs index f554dea128..e5b9c020d5 100644 --- a/crates/gpui/src/platform/windows/dispatcher.rs +++ b/crates/gpui/src/platform/windows/dispatcher.rs @@ -9,8 +9,10 @@ use parking::Parker; use parking_lot::Mutex; use util::ResultExt; use windows::{ + Foundation::TimeSpan, System::Threading::{ - ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemPriority, + ThreadPool, ThreadPoolTimer, TimerElapsedHandler, WorkItemHandler, WorkItemOptions, + WorkItemPriority, }, Win32::{ Foundation::{LPARAM, WPARAM}, @@ -54,7 +56,12 @@ impl WindowsDispatcher { Ok(()) }) }; - ThreadPool::RunWithPriorityAsync(&handler, WorkItemPriority::High).log_err(); + ThreadPool::RunWithPriorityAndOptionsAsync( + &handler, + WorkItemPriority::High, + WorkItemOptions::TimeSliced, + ) + .log_err(); } fn dispatch_on_threadpool_after(&self, runnable: Runnable, duration: Duration) { @@ -65,7 +72,12 @@ impl WindowsDispatcher { Ok(()) }) }; - ThreadPoolTimer::CreateTimer(&handler, duration.into()).log_err(); + let delay = TimeSpan { + // A time period expressed in 100-nanosecond units. + // 10,000,000 ticks per second + Duration: (duration.as_nanos() / 100) as i64, + }; + ThreadPoolTimer::CreateTimer(&handler, delay).log_err(); } } diff --git a/crates/gpui/src/platform/windows/keyboard.rs b/crates/gpui/src/platform/windows/keyboard.rs index 0eb97fbb0c..371feb70c2 100644 --- a/crates/gpui/src/platform/windows/keyboard.rs +++ b/crates/gpui/src/platform/windows/keyboard.rs @@ -1,31 +1,22 @@ use anyhow::Result; -use collections::HashMap; use windows::Win32::UI::{ Input::KeyboardAndMouse::{ - GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MAPVK_VK_TO_VSC, MapVirtualKeyW, ToUnicode, - VIRTUAL_KEY, VK_0, VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1, - VK_CONTROL, VK_MENU, VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7, - VK_OEM_8, VK_OEM_102, VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT, + GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MapVirtualKeyW, ToUnicode, VIRTUAL_KEY, VK_0, + VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1, VK_CONTROL, VK_MENU, + VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7, VK_OEM_8, VK_OEM_102, + VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT, }, WindowsAndMessaging::KL_NAMELENGTH, }; use windows_core::HSTRING; -use crate::{ - KeybindingKeystroke, Keystroke, Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper, -}; +use crate::{Modifiers, PlatformKeyboardLayout}; pub(crate) struct WindowsKeyboardLayout { id: String, name: String, } -pub(crate) struct WindowsKeyboardMapper { - key_to_vkey: HashMap, - vkey_to_key: HashMap, - vkey_to_shifted: HashMap, -} - impl PlatformKeyboardLayout for WindowsKeyboardLayout { fn id(&self) -> &str { &self.id @@ -36,65 +27,6 @@ impl PlatformKeyboardLayout for WindowsKeyboardLayout { } } -impl PlatformKeyboardMapper for WindowsKeyboardMapper { - fn map_key_equivalent( - &self, - mut keystroke: Keystroke, - use_key_equivalents: bool, - ) -> KeybindingKeystroke { - let Some((vkey, shifted_key)) = self.get_vkey_from_key(&keystroke.key, use_key_equivalents) - else { - return KeybindingKeystroke::from_keystroke(keystroke); - }; - if shifted_key && keystroke.modifiers.shift { - log::warn!( - "Keystroke '{}' has both shift and a shifted key, this is likely a bug", - keystroke.key - ); - } - - let shift = shifted_key || keystroke.modifiers.shift; - keystroke.modifiers.shift = false; - - let Some(key) = self.vkey_to_key.get(&vkey).cloned() else { - log::error!( - "Failed to map key equivalent '{:?}' to a valid key", - keystroke - ); - return KeybindingKeystroke::from_keystroke(keystroke); - }; - - keystroke.key = if shift { - let Some(shifted_key) = self.vkey_to_shifted.get(&vkey).cloned() else { - log::error!( - "Failed to map keystroke {:?} with virtual key '{:?}' to a shifted key", - keystroke, - vkey - ); - return KeybindingKeystroke::from_keystroke(keystroke); - }; - shifted_key - } else { - key.clone() - }; - - let modifiers = Modifiers { - shift, - ..keystroke.modifiers - }; - - KeybindingKeystroke { - inner: keystroke, - display_modifiers: modifiers, - display_key: key, - } - } - - fn get_key_equivalents(&self) -> Option<&HashMap> { - None - } -} - impl WindowsKeyboardLayout { pub(crate) fn new() -> Result { let mut buffer = [0u16; KL_NAMELENGTH as usize]; @@ -116,41 +48,6 @@ impl WindowsKeyboardLayout { } } -impl WindowsKeyboardMapper { - pub(crate) fn new() -> Self { - let mut key_to_vkey = HashMap::default(); - let mut vkey_to_key = HashMap::default(); - let mut vkey_to_shifted = HashMap::default(); - for vkey in CANDIDATE_VKEYS { - if let Some(key) = get_key_from_vkey(*vkey) { - key_to_vkey.insert(key.clone(), (vkey.0, false)); - vkey_to_key.insert(vkey.0, key); - } - let scan_code = unsafe { MapVirtualKeyW(vkey.0 as u32, MAPVK_VK_TO_VSC) }; - if scan_code == 0 { - continue; - } - if let Some(shifted_key) = get_shifted_key(*vkey, scan_code) { - key_to_vkey.insert(shifted_key.clone(), (vkey.0, true)); - vkey_to_shifted.insert(vkey.0, shifted_key); - } - } - Self { - key_to_vkey, - vkey_to_key, - vkey_to_shifted, - } - } - - fn get_vkey_from_key(&self, key: &str, use_key_equivalents: bool) -> Option<(u16, bool)> { - if use_key_equivalents { - get_vkey_from_key_with_us_layout(key) - } else { - self.key_to_vkey.get(key).cloned() - } - } -} - pub(crate) fn get_keystroke_key( vkey: VIRTUAL_KEY, scan_code: u32, @@ -243,134 +140,3 @@ pub(crate) fn generate_key_char( _ => None, } } - -fn get_vkey_from_key_with_us_layout(key: &str) -> Option<(u16, bool)> { - match key { - // ` => VK_OEM_3 - "`" => Some((VK_OEM_3.0, false)), - "~" => Some((VK_OEM_3.0, true)), - "1" => Some((VK_1.0, false)), - "!" => Some((VK_1.0, true)), - "2" => Some((VK_2.0, false)), - "@" => Some((VK_2.0, true)), - "3" => Some((VK_3.0, false)), - "#" => Some((VK_3.0, true)), - "4" => Some((VK_4.0, false)), - "$" => Some((VK_4.0, true)), - "5" => Some((VK_5.0, false)), - "%" => Some((VK_5.0, true)), - "6" => Some((VK_6.0, false)), - "^" => Some((VK_6.0, true)), - "7" => Some((VK_7.0, false)), - "&" => Some((VK_7.0, true)), - "8" => Some((VK_8.0, false)), - "*" => Some((VK_8.0, true)), - "9" => Some((VK_9.0, false)), - "(" => Some((VK_9.0, true)), - "0" => Some((VK_0.0, false)), - ")" => Some((VK_0.0, true)), - "-" => Some((VK_OEM_MINUS.0, false)), - "_" => Some((VK_OEM_MINUS.0, true)), - "=" => Some((VK_OEM_PLUS.0, false)), - "+" => Some((VK_OEM_PLUS.0, true)), - "[" => Some((VK_OEM_4.0, false)), - "{" => Some((VK_OEM_4.0, true)), - "]" => Some((VK_OEM_6.0, false)), - "}" => Some((VK_OEM_6.0, true)), - "\\" => Some((VK_OEM_5.0, false)), - "|" => Some((VK_OEM_5.0, true)), - ";" => Some((VK_OEM_1.0, false)), - ":" => Some((VK_OEM_1.0, true)), - "'" => Some((VK_OEM_7.0, false)), - "\"" => Some((VK_OEM_7.0, true)), - "," => Some((VK_OEM_COMMA.0, false)), - "<" => Some((VK_OEM_COMMA.0, true)), - "." => Some((VK_OEM_PERIOD.0, false)), - ">" => Some((VK_OEM_PERIOD.0, true)), - "/" => Some((VK_OEM_2.0, false)), - "?" => Some((VK_OEM_2.0, true)), - _ => None, - } -} - -const CANDIDATE_VKEYS: &[VIRTUAL_KEY] = &[ - VK_OEM_3, - VK_OEM_MINUS, - VK_OEM_PLUS, - VK_OEM_4, - VK_OEM_5, - VK_OEM_6, - VK_OEM_1, - VK_OEM_7, - VK_OEM_COMMA, - VK_OEM_PERIOD, - VK_OEM_2, - VK_OEM_102, - VK_OEM_8, - VK_ABNT_C1, - VK_0, - VK_1, - VK_2, - VK_3, - VK_4, - VK_5, - VK_6, - VK_7, - VK_8, - VK_9, -]; - -#[cfg(test)] -mod tests { - use crate::{Keystroke, Modifiers, PlatformKeyboardMapper, WindowsKeyboardMapper}; - - #[test] - fn test_keyboard_mapper() { - let mapper = WindowsKeyboardMapper::new(); - - // Normal case - let keystroke = Keystroke { - modifiers: Modifiers::control(), - key: "a".to_string(), - key_char: None, - }; - let mapped = mapper.map_key_equivalent(keystroke.clone(), true); - assert_eq!(mapped.inner, keystroke); - assert_eq!(mapped.display_key, "a"); - assert_eq!(mapped.display_modifiers, Modifiers::control()); - - // Shifted case, ctrl-$ - let keystroke = Keystroke { - modifiers: Modifiers::control(), - key: "$".to_string(), - key_char: None, - }; - let mapped = mapper.map_key_equivalent(keystroke.clone(), true); - assert_eq!(mapped.inner, keystroke); - assert_eq!(mapped.display_key, "4"); - assert_eq!(mapped.display_modifiers, Modifiers::control_shift()); - - // Shifted case, but shift is true - let keystroke = Keystroke { - modifiers: Modifiers::control_shift(), - key: "$".to_string(), - key_char: None, - }; - let mapped = mapper.map_key_equivalent(keystroke, true); - assert_eq!(mapped.inner.modifiers, Modifiers::control()); - assert_eq!(mapped.display_key, "4"); - assert_eq!(mapped.display_modifiers, Modifiers::control_shift()); - - // Windows style - let keystroke = Keystroke { - modifiers: Modifiers::control_shift(), - key: "4".to_string(), - key_char: None, - }; - let mapped = mapper.map_key_equivalent(keystroke, true); - assert_eq!(mapped.inner.modifiers, Modifiers::control()); - assert_eq!(mapped.inner.key, "$"); - assert_eq!(mapped.display_key, "4"); - assert_eq!(mapped.display_modifiers, Modifiers::control_shift()); - } -} diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index 5ac2be2f23..b13b9915f1 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -1,6 +1,5 @@ use std::{ cell::RefCell, - ffi::OsStr, mem::ManuallyDrop, path::{Path, PathBuf}, rc::Rc, @@ -351,10 +350,6 @@ impl Platform for WindowsPlatform { ) } - fn keyboard_mapper(&self) -> Rc { - Rc::new(WindowsKeyboardMapper::new()) - } - fn on_keyboard_layout_change(&self, callback: Box) { self.state.borrow_mut().callbacks.keyboard_layout_change = Some(callback); } @@ -465,15 +460,13 @@ impl Platform for WindowsPlatform { } fn open_url(&self, url: &str) { - if url.is_empty() { - return; - } let url_string = url.to_string(); self.background_executor() .spawn(async move { - open_target(&url_string) - .with_context(|| format!("Opening url: {}", url_string)) - .log_err(); + if url_string.is_empty() { + return; + } + open_target(url_string.as_str()); }) .detach(); } @@ -521,29 +514,37 @@ impl Platform for WindowsPlatform { } fn reveal_path(&self, path: &Path) { - if path.as_os_str().is_empty() { + let Ok(file_full_path) = path.canonicalize() else { + log::error!("unable to parse file path"); return; - } - let path = path.to_path_buf(); + }; self.background_executor() .spawn(async move { - open_target_in_explorer(&path) - .with_context(|| format!("Revealing path {} in explorer", path.display())) - .log_err(); + let Some(path) = file_full_path.to_str() else { + return; + }; + if path.is_empty() { + return; + } + open_target_in_explorer(path); }) .detach(); } fn open_with_system(&self, path: &Path) { - if path.as_os_str().is_empty() { + let Ok(full_path) = path.canonicalize() else { + log::error!("unable to parse file full path: {}", path.display()); return; - } - let path = path.to_path_buf(); + }; self.background_executor() .spawn(async move { - open_target(&path) - .with_context(|| format!("Opening {} with system", path.display())) - .log_err(); + let Some(full_path_str) = full_path.to_str() else { + return; + }; + if full_path_str.is_empty() { + return; + }; + open_target(full_path_str); }) .detach(); } @@ -734,67 +735,39 @@ pub(crate) struct WindowCreationInfo { pub(crate) disable_direct_composition: bool, } -fn open_target(target: impl AsRef) -> Result<()> { - let target = target.as_ref(); - let ret = unsafe { - ShellExecuteW( +fn open_target(target: &str) { + unsafe { + let ret = ShellExecuteW( None, windows::core::w!("open"), &HSTRING::from(target), None, None, SW_SHOWDEFAULT, - ) - }; - if ret.0 as isize <= 32 { - Err(anyhow::anyhow!( - "Unable to open target: {}", - std::io::Error::last_os_error() - )) - } else { - Ok(()) + ); + if ret.0 as isize <= 32 { + log::error!("Unable to open target: {}", std::io::Error::last_os_error()); + } } } -fn open_target_in_explorer(target: &Path) -> Result<()> { - let dir = target.parent().context("No parent folder found")?; - let desktop = unsafe { SHGetDesktopFolder()? }; - - let mut dir_item = std::ptr::null_mut(); +fn open_target_in_explorer(target: &str) { unsafe { - desktop.ParseDisplayName( - HWND::default(), + let ret = ShellExecuteW( None, - &HSTRING::from(dir), + windows::core::w!("open"), + windows::core::w!("explorer.exe"), + &HSTRING::from(format!("/select,{}", target).as_str()), None, - &mut dir_item, - std::ptr::null_mut(), - )?; - } - - let mut file_item = std::ptr::null_mut(); - unsafe { - desktop.ParseDisplayName( - HWND::default(), - None, - &HSTRING::from(target), - None, - &mut file_item, - std::ptr::null_mut(), - )?; - } - - let highlight = [file_item as *const _]; - unsafe { SHOpenFolderAndSelectItems(dir_item as _, Some(&highlight), 0) }.or_else(|err| { - if err.code().0 == ERROR_FILE_NOT_FOUND.0 as i32 { - // On some systems, the above call mysteriously fails with "file not - // found" even though the file is there. In these cases, ShellExecute() - // seems to work as a fallback (although it won't select the file). - open_target(dir).context("Opening target parent folder") - } else { - Err(anyhow::anyhow!("Can not open target path: {}", err)) + SW_SHOWDEFAULT, + ); + if ret.0 as isize <= 32 { + log::error!( + "Unable to open target in explorer: {}", + std::io::Error::last_os_error() + ); } - }) + } } fn file_open_dialog( diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index 2dca57424b..b96557b391 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -401,19 +401,12 @@ pub fn init(cx: &mut App) { mod persistence { use std::path::PathBuf; - use db::{ - query, - sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, - }; + use db::{define_connection, query, sqlez_macros::sql}; use workspace::{ItemId, WorkspaceDb, WorkspaceId}; - pub struct ImageViewerDb(ThreadSafeConnection); - - impl Domain for ImageViewerDb { - const NAME: &str = stringify!(ImageViewerDb); - - const MIGRATIONS: &[&str] = &[sql!( + define_connection! { + pub static ref IMAGE_VIEWER: ImageViewerDb = + &[sql!( CREATE TABLE image_viewers ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -424,11 +417,9 @@ mod persistence { FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; - )]; + )]; } - db::static_connection!(IMAGE_VIEWER, ImageViewerDb, [WorkspaceDb]); - impl ImageViewerDb { query! { pub async fn save_image_path( diff --git a/crates/inspector_ui/Cargo.toml b/crates/inspector_ui/Cargo.toml index cefe888974..8e55a8a477 100644 --- a/crates/inspector_ui/Cargo.toml +++ b/crates/inspector_ui/Cargo.toml @@ -24,7 +24,6 @@ serde_json_lenient.workspace = true theme.workspace = true ui.workspace = true util.workspace = true -util_macros.workspace = true workspace-hack.workspace = true workspace.workspace = true zed_actions.workspace = true diff --git a/crates/inspector_ui/src/div_inspector.rs b/crates/inspector_ui/src/div_inspector.rs index c3d687e57a..0c2b16b9f4 100644 --- a/crates/inspector_ui/src/div_inspector.rs +++ b/crates/inspector_ui/src/div_inspector.rs @@ -25,7 +25,7 @@ use util::split_str_with_ranges; /// Path used for unsaved buffer that contains style json. To support the json language server, this /// matches the name used in the generated schemas. -const ZED_INSPECTOR_STYLE_JSON: &str = util_macros::path!("/zed-inspector-style.json"); +const ZED_INSPECTOR_STYLE_JSON: &str = "/zed-inspector-style.json"; pub(crate) struct DivInspector { state: State, diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 4ddc2b3018..b106110c33 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1569,21 +1569,11 @@ impl Buffer { self.send_operation(op, true, cx); } - pub fn buffer_diagnostics( - &self, - for_server: Option, - ) -> Vec<&DiagnosticEntry> { - match for_server { - Some(server_id) => match self.diagnostics.binary_search_by_key(&server_id, |v| v.0) { - Ok(idx) => self.diagnostics[idx].1.iter().collect(), - Err(_) => Vec::new(), - }, - None => self - .diagnostics - .iter() - .flat_map(|(_, diagnostic_set)| diagnostic_set.iter()) - .collect(), - } + pub fn get_diagnostics(&self, server_id: LanguageServerId) -> Option<&DiagnosticSet> { + let Ok(idx) = self.diagnostics.binary_search_by_key(&server_id, |v| v.0) else { + return None; + }; + Some(&self.diagnostics[idx].1) } fn request_autoindent(&mut self, cx: &mut Context) { diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 0f82d3997f..90a59ce066 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -5,7 +5,7 @@ use anyhow::Result; use collections::{FxHashMap, HashMap, HashSet}; use ec4rs::{ Properties as EditorconfigProperties, - property::{FinalNewline, IndentSize, IndentStyle, MaxLineLen, TabWidth, TrimTrailingWs}, + property::{FinalNewline, IndentSize, IndentStyle, TabWidth, TrimTrailingWs}, }; use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder}; use gpui::{App, Modifiers}; @@ -350,12 +350,6 @@ pub struct CompletionSettings { /// Default: `fallback` #[serde(default = "default_words_completion_mode")] pub words: WordsCompletionMode, - /// How many characters has to be in the completions query to automatically show the words-based completions. - /// Before that value, it's still possible to trigger the words-based completion manually with the corresponding editor command. - /// - /// Default: 3 - #[serde(default = "default_3")] - pub words_min_length: usize, /// Whether to fetch LSP completions or not. /// /// Default: true @@ -365,7 +359,7 @@ pub struct CompletionSettings { /// When set to 0, waits indefinitely. /// /// Default: 0 - #[serde(default)] + #[serde(default = "default_lsp_fetch_timeout_ms")] pub lsp_fetch_timeout_ms: u64, /// Controls how LSP completions are inserted. /// @@ -411,8 +405,8 @@ fn default_lsp_insert_mode() -> LspInsertMode { LspInsertMode::ReplaceSuffix } -fn default_3() -> usize { - 3 +fn default_lsp_fetch_timeout_ms() -> u64 { + 0 } /// The settings for a particular language. @@ -1137,10 +1131,6 @@ impl AllLanguageSettings { } fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigProperties) { - let preferred_line_length = cfg.get::().ok().and_then(|v| match v { - MaxLineLen::Value(u) => Some(u as u32), - MaxLineLen::Off => None, - }); let tab_size = cfg.get::().ok().and_then(|v| match v { IndentSize::Value(u) => NonZeroU32::new(u as u32), IndentSize::UseTabWidth => cfg.get::().ok().and_then(|w| match w { @@ -1168,7 +1158,6 @@ fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigPr *target = value; } } - merge(&mut settings.preferred_line_length, preferred_line_length); merge(&mut settings.tab_size, tab_size); merge(&mut settings.hard_tabs, hard_tabs); merge( @@ -1474,7 +1463,6 @@ impl settings::Settings for AllLanguageSettings { } else { d.completions = Some(CompletionSettings { words: mode, - words_min_length: 3, lsp: true, lsp_fetch_timeout_ms: 0, lsp_insert_mode: LspInsertMode::ReplaceSuffix, diff --git a/crates/language/src/toolchain.rs b/crates/language/src/toolchain.rs index 73c142c8ca..979513bc96 100644 --- a/crates/language/src/toolchain.rs +++ b/crates/language/src/toolchain.rs @@ -96,7 +96,7 @@ impl LanguageToolchainStore for T { } type DefaultIndex = usize; -#[derive(Default, Clone, Debug)] +#[derive(Default, Clone)] pub struct ToolchainList { pub toolchains: Vec, pub default: Option, diff --git a/crates/language_tools/src/key_context_view.rs b/crates/language_tools/src/key_context_view.rs index 4140713544..057259d114 100644 --- a/crates/language_tools/src/key_context_view.rs +++ b/crates/language_tools/src/key_context_view.rs @@ -4,6 +4,7 @@ use gpui::{ }; use itertools::Itertools; use serde_json::json; +use settings::get_key_equivalents; use ui::{Button, ButtonStyle}; use ui::{ ButtonCommon, Clickable, Context, FluentBuilder, InteractiveElement, Label, LabelCommon, @@ -168,8 +169,7 @@ impl Item for KeyContextView { impl Render for KeyContextView { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl ui::IntoElement { use itertools::Itertools; - - let key_equivalents = cx.keyboard_mapper().get_key_equivalents(); + let key_equivalents = get_key_equivalents(cx.keyboard_layout().id()); v_flex() .id("key-context-view") .overflow_scroll() diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index d5206c1f26..43c0365291 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -1743,5 +1743,6 @@ pub enum Event { } impl EventEmitter for LogStore {} +impl EventEmitter for LspLogView {} impl EventEmitter for LspLogView {} impl EventEmitter for LspLogView {} diff --git a/crates/languages/src/javascript/highlights.scm b/crates/languages/src/javascript/highlights.scm index ebeac7efff..9d5ebbaf71 100644 --- a/crates/languages/src/javascript/highlights.scm +++ b/crates/languages/src/javascript/highlights.scm @@ -231,7 +231,6 @@ "implements" "interface" "keyof" - "module" "namespace" "private" "protected" @@ -251,4 +250,4 @@ (jsx_closing_element ([""]) @punctuation.bracket.jsx) (jsx_self_closing_element (["<" "/>"]) @punctuation.bracket.jsx) (jsx_attribute "=" @punctuation.delimiter.jsx) -(jsx_text) @text.jsx +(jsx_text) @text.jsx \ No newline at end of file diff --git a/crates/languages/src/javascript/injections.scm b/crates/languages/src/javascript/injections.scm index dbec1937b1..7baba5f227 100644 --- a/crates/languages/src/javascript/injections.scm +++ b/crates/languages/src/javascript/injections.scm @@ -11,21 +11,6 @@ (#set! injection.language "css")) ) -(call_expression - function: (member_expression - object: (identifier) @_obj (#eq? @_obj "styled") - property: (property_identifier)) - arguments: (template_string (string_fragment) @injection.content - (#set! injection.language "css")) -) - -(call_expression - function: (call_expression - function: (identifier) @_name (#eq? @_name "styled")) - arguments: (template_string (string_fragment) @injection.content - (#set! injection.language "css")) -) - (call_expression function: (identifier) @_name (#eq? @_name "html") arguments: (template_string) @injection.content diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index 3e8dce756b..c6c7357148 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -510,6 +510,20 @@ impl LspAdapter for RustLspAdapter { } } + let cargo_diagnostics_fetched_separately = ProjectSettings::get_global(cx) + .diagnostics + .fetch_cargo_diagnostics(); + if cargo_diagnostics_fetched_separately { + let disable_check_on_save = json!({ + "checkOnSave": false, + }); + if let Some(initialization_options) = &mut original.initialization_options { + merge_json_value_into(disable_check_on_save, initialization_options); + } else { + original.initialization_options = Some(disable_check_on_save); + } + } + Ok(original) } } diff --git a/crates/languages/src/tsx/highlights.scm b/crates/languages/src/tsx/highlights.scm index f7cb987831..5e2fbbf63a 100644 --- a/crates/languages/src/tsx/highlights.scm +++ b/crates/languages/src/tsx/highlights.scm @@ -237,7 +237,6 @@ "implements" "interface" "keyof" - "module" "namespace" "private" "protected" @@ -257,4 +256,4 @@ (jsx_closing_element ([""]) @punctuation.bracket.jsx) (jsx_self_closing_element (["<" "/>"]) @punctuation.bracket.jsx) (jsx_attribute "=" @punctuation.delimiter.jsx) -(jsx_text) @text.jsx +(jsx_text) @text.jsx \ No newline at end of file diff --git a/crates/languages/src/tsx/injections.scm b/crates/languages/src/tsx/injections.scm index 9eec01cc89..48da80995b 100644 --- a/crates/languages/src/tsx/injections.scm +++ b/crates/languages/src/tsx/injections.scm @@ -11,21 +11,6 @@ (#set! injection.language "css")) ) -(call_expression - function: (member_expression - object: (identifier) @_obj (#eq? @_obj "styled") - property: (property_identifier)) - arguments: (template_string (string_fragment) @injection.content - (#set! injection.language "css")) -) - -(call_expression - function: (call_expression - function: (identifier) @_name (#eq? @_name "styled")) - arguments: (template_string (string_fragment) @injection.content - (#set! injection.language "css")) -) - (call_expression function: (identifier) @_name (#eq? @_name "html") arguments: (template_string (string_fragment) @injection.content diff --git a/crates/languages/src/typescript/highlights.scm b/crates/languages/src/typescript/highlights.scm index 84cbbae77d..af37ef6415 100644 --- a/crates/languages/src/typescript/highlights.scm +++ b/crates/languages/src/typescript/highlights.scm @@ -248,7 +248,6 @@ "is" "keyof" "let" - "module" "namespace" "new" "of" @@ -273,4 +272,4 @@ "while" "with" "yield" -] @keyword +] @keyword \ No newline at end of file diff --git a/crates/languages/src/typescript/injections.scm b/crates/languages/src/typescript/injections.scm index 1ca1e9ad59..7affdc5b75 100644 --- a/crates/languages/src/typescript/injections.scm +++ b/crates/languages/src/typescript/injections.scm @@ -15,21 +15,6 @@ (#set! injection.language "css")) ) -(call_expression - function: (member_expression - object: (identifier) @_obj (#eq? @_obj "styled") - property: (property_identifier)) - arguments: (template_string (string_fragment) @injection.content - (#set! injection.language "css")) -) - -(call_expression - function: (call_expression - function: (identifier) @_name (#eq? @_name "styled")) - arguments: (template_string (string_fragment) @injection.content - (#set! injection.language "css")) -) - (call_expression function: (identifier) @_name (#eq? @_name "html") arguments: (template_string) @injection.content diff --git a/crates/livekit_client/Cargo.toml b/crates/livekit_client/Cargo.toml index 3575325ac0..58059967b7 100644 --- a/crates/livekit_client/Cargo.toml +++ b/crates/livekit_client/Cargo.toml @@ -25,7 +25,6 @@ async-trait.workspace = true collections.workspace = true cpal.workspace = true futures.workspace = true -audio.workspace = true gpui = { workspace = true, features = ["screen-capture", "x11", "wayland", "windows-manifest"] } gpui_tokio.workspace = true http_client_tls.workspace = true @@ -36,7 +35,6 @@ nanoid.workspace = true parking_lot.workspace = true postage.workspace = true smallvec.workspace = true -settings.workspace = true tokio-tungstenite.workspace = true util.workspace = true workspace-hack.workspace = true diff --git a/crates/livekit_client/src/lib.rs b/crates/livekit_client/src/lib.rs index 055aa3704e..e3934410e1 100644 --- a/crates/livekit_client/src/lib.rs +++ b/crates/livekit_client/src/lib.rs @@ -24,11 +24,8 @@ mod livekit_client; )))] pub use livekit_client::*; -// If you need proper LSP in livekit_client you've got to comment -// - the cfg blocks above -// - the mods: mock_client & test and their conditional blocks -// - the pub use mock_client::* and their conditional blocks - +// If you need proper LSP in livekit_client you've got to comment out +// the mocks and test #[cfg(any( test, feature = "test-support", diff --git a/crates/livekit_client/src/livekit_client.rs b/crates/livekit_client/src/livekit_client.rs index 0751b014f4..adeea4f512 100644 --- a/crates/livekit_client/src/livekit_client.rs +++ b/crates/livekit_client/src/livekit_client.rs @@ -1,16 +1,15 @@ use std::sync::Arc; use anyhow::{Context as _, Result}; -use audio::AudioSettings; use collections::HashMap; use futures::{SinkExt, channel::mpsc}; use gpui::{App, AsyncApp, ScreenCaptureSource, ScreenCaptureStream, Task}; use gpui_tokio::Tokio; -use log::info; use playback::capture_local_video_track; -use settings::Settings; mod playback; +#[cfg(feature = "record-microphone")] +mod record; use crate::{LocalTrack, Participant, RemoteTrack, RoomEvent, TrackPublication}; pub use playback::AudioStream; @@ -126,14 +125,9 @@ impl Room { pub fn play_remote_audio_track( &self, track: &RemoteAudioTrack, - cx: &mut App, + _cx: &App, ) -> Result { - if AudioSettings::get_global(cx).rodio_audio { - info!("Using experimental.rodio_audio audio pipeline"); - playback::play_remote_audio_track(&track.0, cx) - } else { - Ok(self.playback.play_remote_audio_track(&track.0)) - } + Ok(self.playback.play_remote_audio_track(&track.0)) } } diff --git a/crates/livekit_client/src/livekit_client/playback.rs b/crates/livekit_client/src/livekit_client/playback.rs index d6b64dbaca..e13fb7bd81 100644 --- a/crates/livekit_client/src/livekit_client/playback.rs +++ b/crates/livekit_client/src/livekit_client/playback.rs @@ -18,16 +18,13 @@ use livekit::webrtc::{ video_stream::native::NativeVideoStream, }; use parking_lot::Mutex; -use rodio::Source; use std::cell::RefCell; use std::sync::Weak; -use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; +use std::sync::atomic::{self, AtomicI32}; use std::time::Duration; use std::{borrow::Cow, collections::VecDeque, sync::Arc, thread}; use util::{ResultExt as _, maybe}; -mod source; - pub(crate) struct AudioStack { executor: BackgroundExecutor, apm: Arc>, @@ -43,29 +40,6 @@ pub(crate) struct AudioStack { const SAMPLE_RATE: u32 = 48000; const NUM_CHANNELS: u32 = 2; -pub(crate) fn play_remote_audio_track( - track: &livekit::track::RemoteAudioTrack, - cx: &mut gpui::App, -) -> Result { - let stop_handle = Arc::new(AtomicBool::new(false)); - let stop_handle_clone = stop_handle.clone(); - let stream = source::LiveKitStream::new(cx.background_executor(), track) - .stoppable() - .periodic_access(Duration::from_millis(50), move |s| { - if stop_handle.load(Ordering::Relaxed) { - s.stop(); - } - }); - audio::Audio::play_source(stream, cx).context("Could not play audio")?; - - let on_drop = util::defer(move || { - stop_handle_clone.store(true, Ordering::Relaxed); - }); - Ok(AudioStream::Output { - _drop: Box::new(on_drop), - }) -} - impl AudioStack { pub(crate) fn new(executor: BackgroundExecutor) -> Self { let apm = Arc::new(Mutex::new(apm::AudioProcessingModule::new( @@ -87,7 +61,7 @@ impl AudioStack { ) -> AudioStream { let output_task = self.start_output(); - let next_ssrc = self.next_ssrc.fetch_add(1, Ordering::Relaxed); + let next_ssrc = self.next_ssrc.fetch_add(1, atomic::Ordering::Relaxed); let source = AudioMixerSource { ssrc: next_ssrc, sample_rate: SAMPLE_RATE, @@ -123,23 +97,6 @@ impl AudioStack { } } - fn start_output(&self) -> Arc> { - if let Some(task) = self._output_task.borrow().upgrade() { - return task; - } - let task = Arc::new(self.executor.spawn({ - let apm = self.apm.clone(); - let mixer = self.mixer.clone(); - async move { - Self::play_output(apm, mixer, SAMPLE_RATE, NUM_CHANNELS) - .await - .log_err(); - } - })); - *self._output_task.borrow_mut() = Arc::downgrade(&task); - task - } - pub(crate) fn capture_local_microphone_track( &self, ) -> Result<(crate::LocalAudioTrack, AudioStream)> { @@ -182,6 +139,23 @@ impl AudioStack { )) } + fn start_output(&self) -> Arc> { + if let Some(task) = self._output_task.borrow().upgrade() { + return task; + } + let task = Arc::new(self.executor.spawn({ + let apm = self.apm.clone(); + let mixer = self.mixer.clone(); + async move { + Self::play_output(apm, mixer, SAMPLE_RATE, NUM_CHANNELS) + .await + .log_err(); + } + })); + *self._output_task.borrow_mut() = Arc::downgrade(&task); + task + } + async fn play_output( apm: Arc>, mixer: Arc>, diff --git a/crates/livekit_client/src/livekit_client/playback/source.rs b/crates/livekit_client/src/livekit_client/playback/source.rs deleted file mode 100644 index 021640247d..0000000000 --- a/crates/livekit_client/src/livekit_client/playback/source.rs +++ /dev/null @@ -1,67 +0,0 @@ -use futures::StreamExt; -use libwebrtc::{audio_stream::native::NativeAudioStream, prelude::AudioFrame}; -use livekit::track::RemoteAudioTrack; -use rodio::{Source, buffer::SamplesBuffer, conversions::SampleTypeConverter}; - -use crate::livekit_client::playback::{NUM_CHANNELS, SAMPLE_RATE}; - -fn frame_to_samplesbuffer(frame: AudioFrame) -> SamplesBuffer { - let samples = frame.data.iter().copied(); - let samples = SampleTypeConverter::<_, _>::new(samples); - let samples: Vec = samples.collect(); - SamplesBuffer::new(frame.num_channels as u16, frame.sample_rate, samples) -} - -pub struct LiveKitStream { - // shared_buffer: SharedBuffer, - inner: rodio::queue::SourcesQueueOutput, - _receiver_task: gpui::Task<()>, -} - -impl LiveKitStream { - pub fn new(executor: &gpui::BackgroundExecutor, track: &RemoteAudioTrack) -> Self { - let mut stream = - NativeAudioStream::new(track.rtc_track(), SAMPLE_RATE as i32, NUM_CHANNELS as i32); - let (queue_input, queue_output) = rodio::queue::queue(true); - // spawn rtc stream - let receiver_task = executor.spawn({ - async move { - while let Some(frame) = stream.next().await { - let samples = frame_to_samplesbuffer(frame); - queue_input.append(samples); - } - } - }); - - LiveKitStream { - _receiver_task: receiver_task, - inner: queue_output, - } - } -} - -impl Iterator for LiveKitStream { - type Item = rodio::Sample; - - fn next(&mut self) -> Option { - self.inner.next() - } -} - -impl Source for LiveKitStream { - fn current_span_len(&self) -> Option { - self.inner.current_span_len() - } - - fn channels(&self) -> rodio::ChannelCount { - self.inner.channels() - } - - fn sample_rate(&self) -> rodio::SampleRate { - self.inner.sample_rate() - } - - fn total_duration(&self) -> Option { - self.inner.total_duration() - } -} diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 942225d098..ce9e2fe229 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -45,7 +45,7 @@ use util::{ConnectionResult, ResultExt, TryFutureExt, redact}; const JSON_RPC_VERSION: &str = "2.0"; const CONTENT_LEN_HEADER: &str = "Content-Length: "; -pub const LSP_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 2); +const LSP_REQUEST_TIMEOUT: Duration = Duration::from_secs(60 * 2); const SERVER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5); type NotificationHandler = Box, Value, &mut AsyncApp)>; diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index e27cbf868a..a54d38163d 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -835,7 +835,7 @@ impl MultiBuffer { this.convert_edits_to_buffer_edits(edits, &snapshot, &original_indent_columns); drop(snapshot); - let mut buffer_ids = Vec::with_capacity(buffer_edits.len()); + let mut buffer_ids = Vec::new(); for (buffer_id, mut edits) in buffer_edits { buffer_ids.push(buffer_id); edits.sort_by_key(|edit| edit.range.start); diff --git a/crates/onboarding/src/ai_setup_page.rs b/crates/onboarding/src/ai_setup_page.rs index 54c49bc72a..672bcf1cd9 100644 --- a/crates/onboarding/src/ai_setup_page.rs +++ b/crates/onboarding/src/ai_setup_page.rs @@ -283,13 +283,17 @@ pub(crate) fn render_ai_setup_page( v_flex() .mt_2() .gap_6() - .child( - AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx) - .tab_index(Some({ - tab_index += 1; - tab_index - 1 - })), - ) + .child({ + let mut ai_upsell_card = + AiUpsellCard::new(client, &user_store, user_store.read(cx).plan(), cx); + + ai_upsell_card.tab_index = Some({ + tab_index += 1; + tab_index - 1 + }); + + ai_upsell_card + }) .child(render_llm_provider_section( &mut tab_index, workspace, diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index 47dfd84894..8fae695854 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -606,7 +606,7 @@ fn render_popular_settings_section( cx: &mut App, ) -> impl IntoElement { const LIGATURE_TOOLTIP: &str = - "Font ligatures combine two characters into one. For example, turning != into ≠."; + "Font ligatures combine two characters into one. For example, turning =/= into ≠."; v_flex() .pt_6() diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index 873dd63201..884374a72f 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -850,19 +850,13 @@ impl workspace::SerializableItem for Onboarding { } mod persistence { - use db::{ - query, - sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, - }; + use db::{define_connection, query, sqlez_macros::sql}; use workspace::WorkspaceDb; - pub struct OnboardingPagesDb(ThreadSafeConnection); - - impl Domain for OnboardingPagesDb { - const NAME: &str = stringify!(OnboardingPagesDb); - - const MIGRATIONS: &[&str] = &[sql!( + define_connection! { + pub static ref ONBOARDING_PAGES: OnboardingPagesDb = + &[ + sql!( CREATE TABLE onboarding_pages ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -872,11 +866,10 @@ mod persistence { FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; - )]; + ), + ]; } - db::static_connection!(ONBOARDING_PAGES, OnboardingPagesDb, [WorkspaceDb]); - impl OnboardingPagesDb { query! { pub async fn save_onboarding_page( diff --git a/crates/onboarding/src/welcome.rs b/crates/onboarding/src/welcome.rs index 8ff55d812b..3fe9c32a48 100644 --- a/crates/onboarding/src/welcome.rs +++ b/crates/onboarding/src/welcome.rs @@ -414,19 +414,13 @@ impl workspace::SerializableItem for WelcomePage { } mod persistence { - use db::{ - query, - sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, - }; + use db::{define_connection, query, sqlez_macros::sql}; use workspace::WorkspaceDb; - pub struct WelcomePagesDb(ThreadSafeConnection); - - impl Domain for WelcomePagesDb { - const NAME: &str = stringify!(WelcomePagesDb); - - const MIGRATIONS: &[&str] = (&[sql!( + define_connection! { + pub static ref WELCOME_PAGES: WelcomePagesDb = + &[ + sql!( CREATE TABLE welcome_pages ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -436,11 +430,10 @@ mod persistence { FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; - )]); + ), + ]; } - db::static_connection!(WELCOME_PAGES, WelcomePagesDb, [WorkspaceDb]); - impl WelcomePagesDb { query! { pub async fn save_welcome_page( diff --git a/crates/open_ai/src/open_ai.rs b/crates/open_ai/src/open_ai.rs index 08be82b830..acf6ec434a 100644 --- a/crates/open_ai/src/open_ai.rs +++ b/crates/open_ai/src/open_ai.rs @@ -446,6 +446,7 @@ pub enum ResponseStreamResult { #[derive(Serialize, Deserialize, Debug)] pub struct ResponseStreamEvent { + pub model: String, pub choices: Vec, pub usage: Option, } diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index 295bad6e59..a171b193d0 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -88,18 +88,9 @@ pub enum BufferStoreEvent { }, } -#[derive(Default, Debug, Clone)] +#[derive(Default, Debug)] pub struct ProjectTransaction(pub HashMap, language::Transaction>); -impl PartialEq for ProjectTransaction { - fn eq(&self, other: &Self) -> bool { - self.0.len() == other.0.len() - && self.0.iter().all(|(buffer, transaction)| { - other.0.get(buffer).is_some_and(|t| t.id == transaction.id) - }) - } -} - impl EventEmitter for BufferStore {} impl RemoteBufferStore { diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 2906c32ff4..834bf2c2d2 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -34,7 +34,7 @@ use http_client::HttpClient; use language::{Buffer, LanguageToolchainStore, language_settings::InlayHintKind}; use node_runtime::NodeRuntime; -use remote::{SshInfo, SshRemoteClient, ssh_session::SshArgs}; +use remote::{SshRemoteClient, ssh_session::SshArgs}; use rpc::{ AnyProtoClient, TypedEnvelope, proto::{self}, @@ -254,18 +254,14 @@ impl DapStore { cx.spawn(async move |_, cx| { let response = request.await?; let binary = DebugAdapterBinary::from_proto(response)?; - let (mut ssh_command, envs, path_style, ssh_shell) = + let (mut ssh_command, envs, path_style) = ssh_client.read_with(cx, |ssh, _| { - let SshInfo { - args: SshArgs { arguments, envs }, - path_style, - shell, - } = ssh.ssh_info().context("SSH arguments not found")?; + let (SshArgs { arguments, envs }, path_style) = + ssh.ssh_info().context("SSH arguments not found")?; anyhow::Ok(( SshCommand { arguments }, envs.unwrap_or_default(), path_style, - shell, )) })??; @@ -284,7 +280,6 @@ impl DapStore { } let (program, args) = wrap_for_ssh( - &ssh_shell, &ssh_command, binary .command diff --git a/crates/project/src/debugger/locators/cargo.rs b/crates/project/src/debugger/locators/cargo.rs index b2f9580f9c..3e28fac8af 100644 --- a/crates/project/src/debugger/locators/cargo.rs +++ b/crates/project/src/debugger/locators/cargo.rs @@ -117,7 +117,7 @@ impl DapLocator for CargoLocator { .cwd .clone() .context("Couldn't get cwd from debug config which is needed for locators")?; - let builder = ShellBuilder::new(None, &build_config.shell).non_interactive(); + let builder = ShellBuilder::new(true, &build_config.shell).non_interactive(); let (program, args) = builder.build( Some("cargo".into()), &build_config @@ -126,7 +126,7 @@ impl DapLocator for CargoLocator { .cloned() .take_while(|arg| arg != "--") .chain(Some("--message-format=json".to_owned())) - .collect::>(), + .collect(), ); let mut child = util::command::new_smol_command(program) .args(args) diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index ce7a871d1a..c90d85358a 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -3444,7 +3444,8 @@ impl LspCommand for GetCodeLens { capabilities .server_capabilities .code_lens_provider - .is_some() + .as_ref() + .is_some_and(|code_lens_options| code_lens_options.resolve_provider.unwrap_or(false)) } fn to_lsp( diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index deebaedd74..0b58009f37 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -72,11 +72,10 @@ use lsp::{ AdapterServerCapabilities, CodeActionKind, CompletionContext, DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesRegistrationOptions, Edit, FileOperationFilter, FileOperationPatternKind, FileOperationRegistrationOptions, FileRename, FileSystemWatcher, - LSP_REQUEST_TIMEOUT, LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions, - LanguageServerId, LanguageServerName, LanguageServerSelector, LspRequestFuture, - MessageActionItem, MessageType, OneOf, RenameFilesParams, SymbolKind, - TextDocumentSyncSaveOptions, TextEdit, WillRenameFiles, WorkDoneProgressCancelParams, - WorkspaceFolder, notification::DidRenameFiles, + LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerId, + LanguageServerName, LanguageServerSelector, LspRequestFuture, MessageActionItem, MessageType, + OneOf, RenameFilesParams, SymbolKind, TextDocumentSyncSaveOptions, TextEdit, WillRenameFiles, + WorkDoneProgressCancelParams, WorkspaceFolder, notification::DidRenameFiles, }; use node_runtime::read_package_installed_version; use parking_lot::Mutex; @@ -85,7 +84,7 @@ use rand::prelude::*; use rpc::{ AnyProtoClient, - proto::{FromProto, LspRequestId, LspRequestMessage as _, ToProto}, + proto::{FromProto, ToProto}, }; use serde::Serialize; use settings::{Settings, SettingsLocation, SettingsStore}; @@ -93,7 +92,7 @@ use sha2::{Digest, Sha256}; use smol::channel::Sender; use snippet::Snippet; use std::{ - any::{Any, TypeId}, + any::Any, borrow::Cow, cell::RefCell, cmp::{Ordering, Reverse}, @@ -3491,7 +3490,6 @@ pub struct LspStore { pub(super) lsp_server_capabilities: HashMap, lsp_document_colors: HashMap, lsp_code_lens: HashMap, - running_lsp_requests: HashMap>)>, } #[derive(Debug, Default, Clone)] @@ -3501,7 +3499,7 @@ pub struct DocumentColors { } type DocumentColorTask = Shared>>>; -type CodeLensTask = Shared>, Arc>>>; +type CodeLensTask = Shared, Arc>>>; #[derive(Debug, Default)] struct DocumentColorData { @@ -3581,8 +3579,6 @@ struct CoreSymbol { impl LspStore { pub fn init(client: &AnyProtoClient) { - client.add_entity_request_handler(Self::handle_lsp_query); - client.add_entity_message_handler(Self::handle_lsp_query_response); client.add_entity_request_handler(Self::handle_multi_lsp_query); client.add_entity_request_handler(Self::handle_restart_language_servers); client.add_entity_request_handler(Self::handle_stop_language_servers); @@ -3762,7 +3758,6 @@ impl LspStore { lsp_server_capabilities: HashMap::default(), lsp_document_colors: HashMap::default(), lsp_code_lens: HashMap::default(), - running_lsp_requests: HashMap::default(), active_entry: None, _maintain_workspace_config, _maintain_buffer_languages: Self::maintain_buffer_languages(languages, cx), @@ -3824,7 +3819,6 @@ impl LspStore { lsp_server_capabilities: HashMap::default(), lsp_document_colors: HashMap::default(), lsp_code_lens: HashMap::default(), - running_lsp_requests: HashMap::default(), active_entry: None, _maintain_workspace_config, @@ -4387,6 +4381,8 @@ impl LspStore { } } + // TODO: remove MultiLspQuery: instead, the proto handler should pick appropriate server(s) + // Then, use `send_lsp_proto_request` or analogue for most of the LSP proto requests and inline this check inside fn is_capable_for_proto_request( &self, buffer: &Entity, @@ -4643,6 +4639,7 @@ impl LspStore { Some((file, language, raw_buffer.remote_id())) }) .sorted_by_key(|(file, _, _)| Reverse(file.worktree.read(cx).is_visible())); + for (file, language, buffer_id) in buffers { let worktree_id = file.worktree_id(cx); let Some(worktree) = local @@ -4684,6 +4681,7 @@ impl LspStore { cx, ) .collect::>(); + for node in nodes { let server_id = node.server_id_or_init(|disposition| { let path = &disposition.path; @@ -5235,130 +5233,154 @@ impl LspStore { pub fn definitions( &mut self, - buffer: &Entity, + buffer_handle: &Entity, position: PointUtf16, cx: &mut Context, - ) -> Task>>> { + ) -> Task>> { if let Some((upstream_client, project_id)) = self.upstream_client() { let request = GetDefinitions { position }; - if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(None)); + if !self.is_capable_for_proto_request(buffer_handle, &request, cx) { + return Task::ready(Ok(Vec::new())); } - let request_task = upstream_client.request_lsp( + let request_task = upstream_client.request(proto::MultiLspQuery { + buffer_id: buffer_handle.read(cx).remote_id().into(), + version: serialize_version(&buffer_handle.read(cx).version()), project_id, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(project_id, buffer.read(cx)), - ); - let buffer = buffer.clone(); + strategy: Some(proto::multi_lsp_query::Strategy::All( + proto::AllLanguageServers {}, + )), + request: Some(proto::multi_lsp_query::Request::GetDefinition( + request.to_proto(project_id, buffer_handle.read(cx)), + )), + }); + let buffer = buffer_handle.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { - return Ok(None); + return Ok(Vec::new()); }; - let Some(responses) = request_task.await? else { - return Ok(None); - }; - let actions = join_all(responses.payload.into_iter().map(|response| { - GetDefinitions { position }.response_from_proto( - response.response, - project.clone(), - buffer.clone(), - cx.clone(), - ) - })) + let responses = request_task.await?.responses; + let actions = join_all( + responses + .into_iter() + .filter_map(|lsp_response| match lsp_response.response? { + proto::lsp_response::Response::GetDefinitionResponse(response) => { + Some(response) + } + unexpected => { + debug_panic!("Unexpected response: {unexpected:?}"); + None + } + }) + .map(|definitions_response| { + GetDefinitions { position }.response_from_proto( + definitions_response, + project.clone(), + buffer.clone(), + cx.clone(), + ) + }), + ) .await; - Ok(Some( - actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect(), - )) + Ok(actions + .into_iter() + .collect::>>>()? + .into_iter() + .flatten() + .dedup() + .collect()) }) } else { let definitions_task = self.request_multiple_lsp_locally( - buffer, + buffer_handle, Some(position), GetDefinitions { position }, cx, ); cx.background_spawn(async move { - Ok(Some( - definitions_task - .await - .into_iter() - .flat_map(|(_, definitions)| definitions) - .dedup() - .collect(), - )) + Ok(definitions_task + .await + .into_iter() + .flat_map(|(_, definitions)| definitions) + .dedup() + .collect()) }) } } pub fn declarations( &mut self, - buffer: &Entity, + buffer_handle: &Entity, position: PointUtf16, cx: &mut Context, - ) -> Task>>> { + ) -> Task>> { if let Some((upstream_client, project_id)) = self.upstream_client() { let request = GetDeclarations { position }; - if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(None)); + if !self.is_capable_for_proto_request(buffer_handle, &request, cx) { + return Task::ready(Ok(Vec::new())); } - let request_task = upstream_client.request_lsp( + let request_task = upstream_client.request(proto::MultiLspQuery { + buffer_id: buffer_handle.read(cx).remote_id().into(), + version: serialize_version(&buffer_handle.read(cx).version()), project_id, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(project_id, buffer.read(cx)), - ); - let buffer = buffer.clone(); + strategy: Some(proto::multi_lsp_query::Strategy::All( + proto::AllLanguageServers {}, + )), + request: Some(proto::multi_lsp_query::Request::GetDeclaration( + request.to_proto(project_id, buffer_handle.read(cx)), + )), + }); + let buffer = buffer_handle.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { - return Ok(None); + return Ok(Vec::new()); }; - let Some(responses) = request_task.await? else { - return Ok(None); - }; - let actions = join_all(responses.payload.into_iter().map(|response| { - GetDeclarations { position }.response_from_proto( - response.response, - project.clone(), - buffer.clone(), - cx.clone(), - ) - })) + let responses = request_task.await?.responses; + let actions = join_all( + responses + .into_iter() + .filter_map(|lsp_response| match lsp_response.response? { + proto::lsp_response::Response::GetDeclarationResponse(response) => { + Some(response) + } + unexpected => { + debug_panic!("Unexpected response: {unexpected:?}"); + None + } + }) + .map(|declarations_response| { + GetDeclarations { position }.response_from_proto( + declarations_response, + project.clone(), + buffer.clone(), + cx.clone(), + ) + }), + ) .await; - Ok(Some( - actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect(), - )) + Ok(actions + .into_iter() + .collect::>>>()? + .into_iter() + .flatten() + .dedup() + .collect()) }) } else { let declarations_task = self.request_multiple_lsp_locally( - buffer, + buffer_handle, Some(position), GetDeclarations { position }, cx, ); cx.background_spawn(async move { - Ok(Some( - declarations_task - .await - .into_iter() - .flat_map(|(_, declarations)| declarations) - .dedup() - .collect(), - )) + Ok(declarations_task + .await + .into_iter() + .flat_map(|(_, declarations)| declarations) + .dedup() + .collect()) }) } } @@ -5368,45 +5390,59 @@ impl LspStore { buffer: &Entity, position: PointUtf16, cx: &mut Context, - ) -> Task>>> { + ) -> Task>> { if let Some((upstream_client, project_id)) = self.upstream_client() { let request = GetTypeDefinitions { position }; if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(None)); + return Task::ready(Ok(Vec::new())); } - let request_task = upstream_client.request_lsp( + let request_task = upstream_client.request(proto::MultiLspQuery { + buffer_id: buffer.read(cx).remote_id().into(), + version: serialize_version(&buffer.read(cx).version()), project_id, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(project_id, buffer.read(cx)), - ); + strategy: Some(proto::multi_lsp_query::Strategy::All( + proto::AllLanguageServers {}, + )), + request: Some(proto::multi_lsp_query::Request::GetTypeDefinition( + request.to_proto(project_id, buffer.read(cx)), + )), + }); let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { - return Ok(None); + return Ok(Vec::new()); }; - let Some(responses) = request_task.await? else { - return Ok(None); - }; - let actions = join_all(responses.payload.into_iter().map(|response| { - GetTypeDefinitions { position }.response_from_proto( - response.response, - project.clone(), - buffer.clone(), - cx.clone(), - ) - })) + let responses = request_task.await?.responses; + let actions = join_all( + responses + .into_iter() + .filter_map(|lsp_response| match lsp_response.response? { + proto::lsp_response::Response::GetTypeDefinitionResponse(response) => { + Some(response) + } + unexpected => { + debug_panic!("Unexpected response: {unexpected:?}"); + None + } + }) + .map(|type_definitions_response| { + GetTypeDefinitions { position }.response_from_proto( + type_definitions_response, + project.clone(), + buffer.clone(), + cx.clone(), + ) + }), + ) .await; - Ok(Some( - actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect(), - )) + Ok(actions + .into_iter() + .collect::>>>()? + .into_iter() + .flatten() + .dedup() + .collect()) }) } else { let type_definitions_task = self.request_multiple_lsp_locally( @@ -5416,14 +5452,12 @@ impl LspStore { cx, ); cx.background_spawn(async move { - Ok(Some( - type_definitions_task - .await - .into_iter() - .flat_map(|(_, type_definitions)| type_definitions) - .dedup() - .collect(), - )) + Ok(type_definitions_task + .await + .into_iter() + .flat_map(|(_, type_definitions)| type_definitions) + .dedup() + .collect()) }) } } @@ -5433,45 +5467,59 @@ impl LspStore { buffer: &Entity, position: PointUtf16, cx: &mut Context, - ) -> Task>>> { + ) -> Task>> { if let Some((upstream_client, project_id)) = self.upstream_client() { let request = GetImplementations { position }; if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(None)); + return Task::ready(Ok(Vec::new())); } - let request_task = upstream_client.request_lsp( + let request_task = upstream_client.request(proto::MultiLspQuery { + buffer_id: buffer.read(cx).remote_id().into(), + version: serialize_version(&buffer.read(cx).version()), project_id, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(project_id, buffer.read(cx)), - ); + strategy: Some(proto::multi_lsp_query::Strategy::All( + proto::AllLanguageServers {}, + )), + request: Some(proto::multi_lsp_query::Request::GetImplementation( + request.to_proto(project_id, buffer.read(cx)), + )), + }); let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { - return Ok(None); + return Ok(Vec::new()); }; - let Some(responses) = request_task.await? else { - return Ok(None); - }; - let actions = join_all(responses.payload.into_iter().map(|response| { - GetImplementations { position }.response_from_proto( - response.response, - project.clone(), - buffer.clone(), - cx.clone(), - ) - })) + let responses = request_task.await?.responses; + let actions = join_all( + responses + .into_iter() + .filter_map(|lsp_response| match lsp_response.response? { + proto::lsp_response::Response::GetImplementationResponse(response) => { + Some(response) + } + unexpected => { + debug_panic!("Unexpected response: {unexpected:?}"); + None + } + }) + .map(|implementations_response| { + GetImplementations { position }.response_from_proto( + implementations_response, + project.clone(), + buffer.clone(), + cx.clone(), + ) + }), + ) .await; - Ok(Some( - actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect(), - )) + Ok(actions + .into_iter() + .collect::>>>()? + .into_iter() + .flatten() + .dedup() + .collect()) }) } else { let implementations_task = self.request_multiple_lsp_locally( @@ -5481,14 +5529,12 @@ impl LspStore { cx, ); cx.background_spawn(async move { - Ok(Some( - implementations_task - .await - .into_iter() - .flat_map(|(_, implementations)| implementations) - .dedup() - .collect(), - )) + Ok(implementations_task + .await + .into_iter() + .flat_map(|(_, implementations)| implementations) + .dedup() + .collect()) }) } } @@ -5498,44 +5544,59 @@ impl LspStore { buffer: &Entity, position: PointUtf16, cx: &mut Context, - ) -> Task>>> { + ) -> Task>> { if let Some((upstream_client, project_id)) = self.upstream_client() { let request = GetReferences { position }; if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(None)); + return Task::ready(Ok(Vec::new())); } - - let request_task = upstream_client.request_lsp( + let request_task = upstream_client.request(proto::MultiLspQuery { + buffer_id: buffer.read(cx).remote_id().into(), + version: serialize_version(&buffer.read(cx).version()), project_id, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(project_id, buffer.read(cx)), - ); + strategy: Some(proto::multi_lsp_query::Strategy::All( + proto::AllLanguageServers {}, + )), + request: Some(proto::multi_lsp_query::Request::GetReferences( + request.to_proto(project_id, buffer.read(cx)), + )), + }); let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { - return Ok(None); - }; - let Some(responses) = request_task.await? else { - return Ok(None); + return Ok(Vec::new()); }; + let responses = request_task.await?.responses; + let actions = join_all( + responses + .into_iter() + .filter_map(|lsp_response| match lsp_response.response? { + proto::lsp_response::Response::GetReferencesResponse(response) => { + Some(response) + } + unexpected => { + debug_panic!("Unexpected response: {unexpected:?}"); + None + } + }) + .map(|references_response| { + GetReferences { position }.response_from_proto( + references_response, + project.clone(), + buffer.clone(), + cx.clone(), + ) + }), + ) + .await; - let locations = join_all(responses.payload.into_iter().map(|lsp_response| { - GetReferences { position }.response_from_proto( - lsp_response.response, - project.clone(), - buffer.clone(), - cx.clone(), - ) - })) - .await - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect(); - Ok(Some(locations)) + Ok(actions + .into_iter() + .collect::>>>()? + .into_iter() + .flatten() + .dedup() + .collect()) }) } else { let references_task = self.request_multiple_lsp_locally( @@ -5545,14 +5606,12 @@ impl LspStore { cx, ); cx.background_spawn(async move { - Ok(Some( - references_task - .await - .into_iter() - .flat_map(|(_, references)| references) - .dedup() - .collect(), - )) + Ok(references_task + .await + .into_iter() + .flat_map(|(_, references)| references) + .dedup() + .collect()) }) } } @@ -5563,51 +5622,65 @@ impl LspStore { range: Range, kinds: Option>, cx: &mut Context, - ) -> Task>>> { + ) -> Task>> { if let Some((upstream_client, project_id)) = self.upstream_client() { let request = GetCodeActions { range: range.clone(), kinds: kinds.clone(), }; if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(None)); + return Task::ready(Ok(Vec::new())); } - let request_task = upstream_client.request_lsp( + let request_task = upstream_client.request(proto::MultiLspQuery { + buffer_id: buffer.read(cx).remote_id().into(), + version: serialize_version(&buffer.read(cx).version()), project_id, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(project_id, buffer.read(cx)), - ); + strategy: Some(proto::multi_lsp_query::Strategy::All( + proto::AllLanguageServers {}, + )), + request: Some(proto::multi_lsp_query::Request::GetCodeActions( + request.to_proto(project_id, buffer.read(cx)), + )), + }); let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { let Some(project) = weak_project.upgrade() else { - return Ok(None); + return Ok(Vec::new()); }; - let Some(responses) = request_task.await? else { - return Ok(None); - }; - let actions = join_all(responses.payload.into_iter().map(|response| { - GetCodeActions { - range: range.clone(), - kinds: kinds.clone(), - } - .response_from_proto( - response.response, - project.clone(), - buffer.clone(), - cx.clone(), - ) - })) + let responses = request_task.await?.responses; + let actions = join_all( + responses + .into_iter() + .filter_map(|lsp_response| match lsp_response.response? { + proto::lsp_response::Response::GetCodeActionsResponse(response) => { + Some(response) + } + unexpected => { + debug_panic!("Unexpected response: {unexpected:?}"); + None + } + }) + .map(|code_actions_response| { + GetCodeActions { + range: range.clone(), + kinds: kinds.clone(), + } + .response_from_proto( + code_actions_response, + project.clone(), + buffer.clone(), + cx.clone(), + ) + }), + ) .await; - Ok(Some( - actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .collect(), - )) + Ok(actions + .into_iter() + .collect::>>>()? + .into_iter() + .flatten() + .collect()) }) } else { let all_actions_task = self.request_multiple_lsp_locally( @@ -5617,13 +5690,11 @@ impl LspStore { cx, ); cx.background_spawn(async move { - Ok(Some( - all_actions_task - .await - .into_iter() - .flat_map(|(_, actions)| actions) - .collect(), - )) + Ok(all_actions_task + .await + .into_iter() + .flat_map(|(_, actions)| actions) + .collect()) }) } } @@ -5648,10 +5719,8 @@ impl LspStore { != cached_data.lens.keys().copied().collect() }); if !has_different_servers { - return Task::ready(Ok(Some( - cached_data.lens.values().flatten().cloned().collect(), - ))) - .shared(); + return Task::ready(Ok(cached_data.lens.values().flatten().cloned().collect())) + .shared(); } } @@ -5689,19 +5758,17 @@ impl LspStore { lsp_store .update(cx, |lsp_store, _| { let lsp_data = lsp_store.lsp_code_lens.entry(buffer_id).or_default(); - if let Some(fetched_lens) = fetched_lens { - if lsp_data.lens_for_version == query_version_queried_for { - lsp_data.lens.extend(fetched_lens); - } else if !lsp_data - .lens_for_version - .changed_since(&query_version_queried_for) - { - lsp_data.lens_for_version = query_version_queried_for; - lsp_data.lens = fetched_lens; - } + if lsp_data.lens_for_version == query_version_queried_for { + lsp_data.lens.extend(fetched_lens.clone()); + } else if !lsp_data + .lens_for_version + .changed_since(&query_version_queried_for) + { + lsp_data.lens_for_version = query_version_queried_for; + lsp_data.lens = fetched_lens.clone(); } lsp_data.update = None; - Some(lsp_data.lens.values().flatten().cloned().collect()) + lsp_data.lens.values().flatten().cloned().collect() }) .map_err(Arc::new) }) @@ -5714,40 +5781,64 @@ impl LspStore { &mut self, buffer: &Entity, cx: &mut Context, - ) -> Task>>>> { + ) -> Task>>> { if let Some((upstream_client, project_id)) = self.upstream_client() { let request = GetCodeLens; if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(None)); + return Task::ready(Ok(HashMap::default())); } - let request_task = upstream_client.request_lsp( + let request_task = upstream_client.request(proto::MultiLspQuery { + buffer_id: buffer.read(cx).remote_id().into(), + version: serialize_version(&buffer.read(cx).version()), project_id, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(project_id, buffer.read(cx)), - ); + strategy: Some(proto::multi_lsp_query::Strategy::All( + proto::AllLanguageServers {}, + )), + request: Some(proto::multi_lsp_query::Request::GetCodeLens( + request.to_proto(project_id, buffer.read(cx)), + )), + }); let buffer = buffer.clone(); cx.spawn(async move |weak_lsp_store, cx| { let Some(lsp_store) = weak_lsp_store.upgrade() else { - return Ok(None); + return Ok(HashMap::default()); }; - let Some(responses) = request_task.await? else { - return Ok(None); - }; - - let code_lens_actions = join_all(responses.payload.into_iter().map(|response| { - let lsp_store = lsp_store.clone(); - let buffer = buffer.clone(); - let cx = cx.clone(); - async move { - ( - LanguageServerId::from_proto(response.server_id), - GetCodeLens - .response_from_proto(response.response, lsp_store, buffer, cx) - .await, - ) - } - })) + let responses = request_task.await?.responses; + let code_lens_actions = join_all( + responses + .into_iter() + .filter_map(|lsp_response| { + let response = match lsp_response.response? { + proto::lsp_response::Response::GetCodeLensResponse(response) => { + Some(response) + } + unexpected => { + debug_panic!("Unexpected response: {unexpected:?}"); + None + } + }?; + let server_id = LanguageServerId::from_proto(lsp_response.server_id); + Some((server_id, response)) + }) + .map(|(server_id, code_lens_response)| { + let lsp_store = lsp_store.clone(); + let buffer = buffer.clone(); + let cx = cx.clone(); + async move { + ( + server_id, + GetCodeLens + .response_from_proto( + code_lens_response, + lsp_store, + buffer, + cx, + ) + .await, + ) + } + }), + ) .await; let mut has_errors = false; @@ -5766,14 +5857,14 @@ impl LspStore { !has_errors || !code_lens_actions.is_empty(), "Failed to fetch code lens" ); - Ok(Some(code_lens_actions)) + Ok(code_lens_actions) }) } else { let code_lens_actions_task = self.request_multiple_lsp_locally(buffer, None::, GetCodeLens, cx); - cx.background_spawn(async move { - Ok(Some(code_lens_actions_task.await.into_iter().collect())) - }) + cx.background_spawn( + async move { Ok(code_lens_actions_task.await.into_iter().collect()) }, + ) } } @@ -6389,23 +6480,48 @@ impl LspStore { let buffer_id = buffer.read(cx).remote_id(); if let Some((client, upstream_project_id)) = self.upstream_client() { - let request = GetDocumentDiagnostics { - previous_result_id: None, - }; - if !self.is_capable_for_proto_request(&buffer, &request, cx) { + if !self.is_capable_for_proto_request( + &buffer, + &GetDocumentDiagnostics { + previous_result_id: None, + }, + cx, + ) { return Task::ready(Ok(None)); } - let request_task = client.request_lsp( - upstream_project_id, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(upstream_project_id, buffer.read(cx)), - ); + let request_task = client.request(proto::MultiLspQuery { + buffer_id: buffer_id.to_proto(), + version: serialize_version(&buffer.read(cx).version()), + project_id: upstream_project_id, + strategy: Some(proto::multi_lsp_query::Strategy::All( + proto::AllLanguageServers {}, + )), + request: Some(proto::multi_lsp_query::Request::GetDocumentDiagnostics( + proto::GetDocumentDiagnostics { + project_id: upstream_project_id, + buffer_id: buffer_id.to_proto(), + version: serialize_version(&buffer.read(cx).version()), + }, + )), + }); cx.background_spawn(async move { + let _proto_responses = request_task + .await? + .responses + .into_iter() + .filter_map(|lsp_response| match lsp_response.response? { + proto::lsp_response::Response::GetDocumentDiagnosticsResponse(response) => { + Some(response) + } + unexpected => { + debug_panic!("Unexpected response: {unexpected:?}"); + None + } + }) + .collect::>(); // Proto requests cause the diagnostics to be pulled from language server(s) on the local side // and then, buffer state updated with the diagnostics received, which will be later propagated to the client. // Do not attempt to further process the dummy responses here. - let _response = request_task.await?; Ok(None) }) } else { @@ -6690,18 +6806,16 @@ impl LspStore { .update(cx, |lsp_store, _| { let lsp_data = lsp_store.lsp_document_colors.entry(buffer_id).or_default(); - if let Some(fetched_colors) = fetched_colors { - if lsp_data.colors_for_version == query_version_queried_for { - lsp_data.colors.extend(fetched_colors); - lsp_data.cache_version += 1; - } else if !lsp_data - .colors_for_version - .changed_since(&query_version_queried_for) - { - lsp_data.colors_for_version = query_version_queried_for; - lsp_data.colors = fetched_colors; - lsp_data.cache_version += 1; - } + if lsp_data.colors_for_version == query_version_queried_for { + lsp_data.colors.extend(fetched_colors.clone()); + lsp_data.cache_version += 1; + } else if !lsp_data + .colors_for_version + .changed_since(&query_version_queried_for) + { + lsp_data.colors_for_version = query_version_queried_for; + lsp_data.colors = fetched_colors.clone(); + lsp_data.cache_version += 1; } lsp_data.colors_update = None; let colors = lsp_data @@ -6726,45 +6840,56 @@ impl LspStore { &mut self, buffer: &Entity, cx: &mut Context, - ) -> Task>>>> { + ) -> Task>>> { if let Some((client, project_id)) = self.upstream_client() { let request = GetDocumentColor {}; if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(None)); + return Task::ready(Ok(HashMap::default())); } - let request_task = client.request_lsp( + let request_task = client.request(proto::MultiLspQuery { project_id, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(project_id, buffer.read(cx)), - ); + buffer_id: buffer.read(cx).remote_id().to_proto(), + version: serialize_version(&buffer.read(cx).version()), + strategy: Some(proto::multi_lsp_query::Strategy::All( + proto::AllLanguageServers {}, + )), + request: Some(proto::multi_lsp_query::Request::GetDocumentColor( + request.to_proto(project_id, buffer.read(cx)), + )), + }); let buffer = buffer.clone(); - cx.spawn(async move |lsp_store, cx| { - let Some(project) = lsp_store.upgrade() else { - return Ok(None); + cx.spawn(async move |project, cx| { + let Some(project) = project.upgrade() else { + return Ok(HashMap::default()); }; let colors = join_all( request_task .await .log_err() - .flatten() - .map(|response| response.payload) + .map(|response| response.responses) .unwrap_or_default() .into_iter() - .map(|color_response| { + .filter_map(|lsp_response| match lsp_response.response? { + proto::lsp_response::Response::GetDocumentColorResponse(response) => { + Some(( + LanguageServerId::from_proto(lsp_response.server_id), + response, + )) + } + unexpected => { + debug_panic!("Unexpected response: {unexpected:?}"); + None + } + }) + .map(|(server_id, color_response)| { let response = request.response_from_proto( - color_response.response, + color_response, project.clone(), buffer.clone(), cx.clone(), ); - async move { - ( - LanguageServerId::from_proto(color_response.server_id), - response.await.log_err().unwrap_or_default(), - ) - } + async move { (server_id, response.await.log_err().unwrap_or_default()) } }), ) .await @@ -6775,25 +6900,23 @@ impl LspStore { .extend(colors); acc }); - Ok(Some(colors)) + Ok(colors) }) } else { let document_colors_task = self.request_multiple_lsp_locally(buffer, None::, GetDocumentColor, cx); cx.background_spawn(async move { - Ok(Some( - document_colors_task - .await - .into_iter() - .fold(HashMap::default(), |mut acc, (server_id, colors)| { - acc.entry(server_id) - .or_insert_with(HashSet::default) - .extend(colors); - acc - }) - .into_iter() - .collect(), - )) + Ok(document_colors_task + .await + .into_iter() + .fold(HashMap::default(), |mut acc, (server_id, colors)| { + acc.entry(server_id) + .or_insert_with(HashSet::default) + .extend(colors); + acc + }) + .into_iter() + .collect()) }) } } @@ -6803,34 +6926,49 @@ impl LspStore { buffer: &Entity, position: T, cx: &mut Context, - ) -> Task>> { + ) -> Task> { let position = position.to_point_utf16(buffer.read(cx)); if let Some((client, upstream_project_id)) = self.upstream_client() { let request = GetSignatureHelp { position }; if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(None); + return Task::ready(Vec::new()); } - let request_task = client.request_lsp( - upstream_project_id, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(upstream_project_id, buffer.read(cx)), - ); + let request_task = client.request(proto::MultiLspQuery { + buffer_id: buffer.read(cx).remote_id().into(), + version: serialize_version(&buffer.read(cx).version()), + project_id: upstream_project_id, + strategy: Some(proto::multi_lsp_query::Strategy::All( + proto::AllLanguageServers {}, + )), + request: Some(proto::multi_lsp_query::Request::GetSignatureHelp( + request.to_proto(upstream_project_id, buffer.read(cx)), + )), + }); let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { - let project = weak_project.upgrade()?; - let signatures = join_all( + let Some(project) = weak_project.upgrade() else { + return Vec::new(); + }; + join_all( request_task .await .log_err() - .flatten() - .map(|response| response.payload) + .map(|response| response.responses) .unwrap_or_default() .into_iter() - .map(|response| { + .filter_map(|lsp_response| match lsp_response.response? { + proto::lsp_response::Response::GetSignatureHelpResponse(response) => { + Some(response) + } + unexpected => { + debug_panic!("Unexpected response: {unexpected:?}"); + None + } + }) + .map(|signature_response| { let response = GetSignatureHelp { position }.response_from_proto( - response.response, + signature_response, project.clone(), buffer.clone(), cx.clone(), @@ -6841,8 +6979,7 @@ impl LspStore { .await .into_iter() .flatten() - .collect(); - Some(signatures) + .collect() }) } else { let all_actions_task = self.request_multiple_lsp_locally( @@ -6852,13 +6989,11 @@ impl LspStore { cx, ); cx.background_spawn(async move { - Some( - all_actions_task - .await - .into_iter() - .flat_map(|(_, actions)| actions) - .collect::>(), - ) + all_actions_task + .await + .into_iter() + .flat_map(|(_, actions)| actions) + .collect::>() }) } } @@ -6868,32 +7003,47 @@ impl LspStore { buffer: &Entity, position: PointUtf16, cx: &mut Context, - ) -> Task>> { + ) -> Task> { if let Some((client, upstream_project_id)) = self.upstream_client() { let request = GetHover { position }; if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(None); + return Task::ready(Vec::new()); } - let request_task = client.request_lsp( - upstream_project_id, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(upstream_project_id, buffer.read(cx)), - ); + let request_task = client.request(proto::MultiLspQuery { + buffer_id: buffer.read(cx).remote_id().into(), + version: serialize_version(&buffer.read(cx).version()), + project_id: upstream_project_id, + strategy: Some(proto::multi_lsp_query::Strategy::All( + proto::AllLanguageServers {}, + )), + request: Some(proto::multi_lsp_query::Request::GetHover( + request.to_proto(upstream_project_id, buffer.read(cx)), + )), + }); let buffer = buffer.clone(); cx.spawn(async move |weak_project, cx| { - let project = weak_project.upgrade()?; - let hovers = join_all( + let Some(project) = weak_project.upgrade() else { + return Vec::new(); + }; + join_all( request_task .await .log_err() - .flatten() - .map(|response| response.payload) + .map(|response| response.responses) .unwrap_or_default() .into_iter() - .map(|response| { + .filter_map(|lsp_response| match lsp_response.response? { + proto::lsp_response::Response::GetHoverResponse(response) => { + Some(response) + } + unexpected => { + debug_panic!("Unexpected response: {unexpected:?}"); + None + } + }) + .map(|hover_response| { let response = GetHover { position }.response_from_proto( - response.response, + hover_response, project.clone(), buffer.clone(), cx.clone(), @@ -6910,8 +7060,7 @@ impl LspStore { .await .into_iter() .flatten() - .collect(); - Some(hovers) + .collect() }) } else { let all_actions_task = self.request_multiple_lsp_locally( @@ -6921,13 +7070,11 @@ impl LspStore { cx, ); cx.background_spawn(async move { - Some( - all_actions_task - .await - .into_iter() - .filter_map(|(_, hover)| remove_empty_hover_blocks(hover?)) - .collect::>(), - ) + all_actions_task + .await + .into_iter() + .filter_map(|(_, hover)| remove_empty_hover_blocks(hover?)) + .collect::>() }) } } @@ -7588,16 +7735,19 @@ impl LspStore { let snapshot = buffer_handle.read(cx).snapshot(); let buffer = buffer_handle.read(cx); let reused_diagnostics = buffer - .buffer_diagnostics(Some(server_id)) - .iter() - .filter(|v| merge(buffer, &v.diagnostic, cx)) - .map(|v| { - let start = Unclipped(v.range.start.to_point_utf16(&snapshot)); - let end = Unclipped(v.range.end.to_point_utf16(&snapshot)); - DiagnosticEntry { - range: start..end, - diagnostic: v.diagnostic.clone(), - } + .get_diagnostics(server_id) + .into_iter() + .flat_map(|diag| { + diag.iter() + .filter(|v| merge(buffer, &v.diagnostic, cx)) + .map(|v| { + let start = Unclipped(v.range.start.to_point_utf16(&snapshot)); + let end = Unclipped(v.range.end.to_point_utf16(&snapshot)); + DiagnosticEntry { + range: start..end, + diagnostic: v.diagnostic.clone(), + } + }) }) .collect::>(); @@ -7987,203 +8137,6 @@ impl LspStore { })? } - async fn handle_lsp_query( - lsp_store: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - use proto::lsp_query::Request; - let sender_id = envelope.original_sender_id().unwrap_or_default(); - let lsp_query = envelope.payload; - let lsp_request_id = LspRequestId(lsp_query.lsp_request_id); - match lsp_query.request.context("invalid LSP query request")? { - Request::GetReferences(get_references) => { - let position = get_references.position.clone().and_then(deserialize_anchor); - Self::query_lsp_locally::( - lsp_store, - sender_id, - lsp_request_id, - get_references, - position, - cx.clone(), - ) - .await?; - } - Request::GetDocumentColor(get_document_color) => { - Self::query_lsp_locally::( - lsp_store, - sender_id, - lsp_request_id, - get_document_color, - None, - cx.clone(), - ) - .await?; - } - Request::GetHover(get_hover) => { - let position = get_hover.position.clone().and_then(deserialize_anchor); - Self::query_lsp_locally::( - lsp_store, - sender_id, - lsp_request_id, - get_hover, - position, - cx.clone(), - ) - .await?; - } - Request::GetCodeActions(get_code_actions) => { - Self::query_lsp_locally::( - lsp_store, - sender_id, - lsp_request_id, - get_code_actions, - None, - cx.clone(), - ) - .await?; - } - Request::GetSignatureHelp(get_signature_help) => { - let position = get_signature_help - .position - .clone() - .and_then(deserialize_anchor); - Self::query_lsp_locally::( - lsp_store, - sender_id, - lsp_request_id, - get_signature_help, - position, - cx.clone(), - ) - .await?; - } - Request::GetCodeLens(get_code_lens) => { - Self::query_lsp_locally::( - lsp_store, - sender_id, - lsp_request_id, - get_code_lens, - None, - cx.clone(), - ) - .await?; - } - Request::GetDefinition(get_definition) => { - let position = get_definition.position.clone().and_then(deserialize_anchor); - Self::query_lsp_locally::( - lsp_store, - sender_id, - lsp_request_id, - get_definition, - position, - cx.clone(), - ) - .await?; - } - Request::GetDeclaration(get_declaration) => { - let position = get_declaration - .position - .clone() - .and_then(deserialize_anchor); - Self::query_lsp_locally::( - lsp_store, - sender_id, - lsp_request_id, - get_declaration, - position, - cx.clone(), - ) - .await?; - } - Request::GetTypeDefinition(get_type_definition) => { - let position = get_type_definition - .position - .clone() - .and_then(deserialize_anchor); - Self::query_lsp_locally::( - lsp_store, - sender_id, - lsp_request_id, - get_type_definition, - position, - cx.clone(), - ) - .await?; - } - Request::GetImplementation(get_implementation) => { - let position = get_implementation - .position - .clone() - .and_then(deserialize_anchor); - Self::query_lsp_locally::( - lsp_store, - sender_id, - lsp_request_id, - get_implementation, - position, - cx.clone(), - ) - .await?; - } - // Diagnostics pull synchronizes internally via the buffer state, and cannot be handled generically as the other requests. - Request::GetDocumentDiagnostics(get_document_diagnostics) => { - let buffer_id = BufferId::new(get_document_diagnostics.buffer_id())?; - let version = deserialize_version(get_document_diagnostics.buffer_version()); - let buffer = lsp_store.update(&mut cx, |this, cx| { - this.buffer_store.read(cx).get_existing(buffer_id) - })??; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(version.clone()) - })? - .await?; - lsp_store.update(&mut cx, |lsp_store, cx| { - let existing_queries = lsp_store - .running_lsp_requests - .entry(TypeId::of::()) - .or_default(); - if ::ProtoRequest::stop_previous_requests( - ) || buffer.read(cx).version.changed_since(&existing_queries.0) - { - existing_queries.1.clear(); - } - existing_queries.1.insert( - lsp_request_id, - cx.spawn(async move |lsp_store, cx| { - let diagnostics_pull = lsp_store - .update(cx, |lsp_store, cx| { - lsp_store.pull_diagnostics_for_buffer(buffer, cx) - }) - .ok(); - if let Some(diagnostics_pull) = diagnostics_pull { - match diagnostics_pull.await { - Ok(()) => {} - Err(e) => log::error!("Failed to pull diagnostics: {e:#}"), - }; - } - }), - ); - })?; - } - } - Ok(proto::Ack {}) - } - - async fn handle_lsp_query_response( - lsp_store: Entity, - envelope: TypedEnvelope, - cx: AsyncApp, - ) -> Result<()> { - lsp_store.read_with(&cx, |lsp_store, _| { - if let Some((upstream_client, _)) = lsp_store.upstream_client() { - upstream_client.handle_lsp_response(envelope.clone()); - } - })?; - Ok(()) - } - - // todo(lsp) remove after Zed Stable hits v0.204.x async fn handle_multi_lsp_query( lsp_store: Entity, envelope: TypedEnvelope, @@ -8757,7 +8710,7 @@ impl LspStore { (root_path.join(&old_path), root_path.join(&new_path)) }; - let _transaction = Self::will_rename_entry( + Self::will_rename_entry( this.downgrade(), worktree_id, &old_abs_path, @@ -9026,22 +8979,13 @@ impl LspStore { lsp_store.update(&mut cx, |lsp_store, cx| { if let Some(server) = lsp_store.language_server_for_id(server_id) { let text_document = if envelope.payload.current_file_only { - let buffer_id = envelope - .payload - .buffer_id - .map(|id| BufferId::new(id)) - .transpose()?; - buffer_id - .and_then(|buffer_id| { - lsp_store - .buffer_store() - .read(cx) - .get(buffer_id) - .and_then(|buffer| { - Some(buffer.read(cx).file()?.as_local()?.abs_path(cx)) - }) - .map(|path| make_text_document_identifier(&path)) - }) + let buffer_id = BufferId::new(envelope.payload.buffer_id)?; + lsp_store + .buffer_store() + .read(cx) + .get(buffer_id) + .and_then(|buffer| Some(buffer.read(cx).file()?.as_local()?.abs_path(cx))) + .map(|path| make_text_document_identifier(&path)) .transpose()? } else { None @@ -9228,7 +9172,7 @@ impl LspStore { new_path: &Path, is_dir: bool, cx: AsyncApp, - ) -> Task { + ) -> Task<()> { let old_uri = lsp::Url::from_file_path(old_path).ok().map(String::from); let new_uri = lsp::Url::from_file_path(new_path).ok().map(String::from); cx.spawn(async move |cx| { @@ -9261,7 +9205,7 @@ impl LspStore { .log_err() .flatten()?; - let transaction = LocalLspStore::deserialize_workspace_edit( + LocalLspStore::deserialize_workspace_edit( this.upgrade()?, edit, false, @@ -9269,8 +9213,8 @@ impl LspStore { cx, ) .await - .ok()?; - Some(transaction) + .ok(); + Some(()) } }); tasks.push(apply_edit); @@ -9280,17 +9224,11 @@ impl LspStore { }) .ok() .flatten(); - let mut merged_transaction = ProjectTransaction::default(); for task in tasks { // Await on tasks sequentially so that the order of application of edits is deterministic // (at least with regards to the order of registration of language servers) - if let Some(transaction) = task.await { - for (buffer, buffer_transaction) in transaction.0 { - merged_transaction.0.insert(buffer, buffer_transaction); - } - } + task.await; } - merged_transaction }) } @@ -11703,11 +11641,12 @@ impl LspStore { // Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings. } "workspace/symbol" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.workspace_symbol_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); + if let Some(options) = parse_register_capabilities(reg)? { + server.update_capabilities(|capabilities| { + capabilities.workspace_symbol_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } } "workspace/fileOperations" => { if let Some(options) = reg.register_options { @@ -11731,11 +11670,12 @@ impl LspStore { } } "textDocument/rangeFormatting" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.document_range_formatting_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); + if let Some(options) = parse_register_capabilities(reg)? { + server.update_capabilities(|capabilities| { + capabilities.document_range_formatting_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } } "textDocument/onTypeFormatting" => { if let Some(options) = reg @@ -11750,32 +11690,36 @@ impl LspStore { } } "textDocument/formatting" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.document_formatting_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); + if let Some(options) = parse_register_capabilities(reg)? { + server.update_capabilities(|capabilities| { + capabilities.document_formatting_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } } "textDocument/rename" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.rename_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); + if let Some(options) = parse_register_capabilities(reg)? { + server.update_capabilities(|capabilities| { + capabilities.rename_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } } "textDocument/inlayHint" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.inlay_hint_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); + if let Some(options) = parse_register_capabilities(reg)? { + server.update_capabilities(|capabilities| { + capabilities.inlay_hint_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } } "textDocument/documentSymbol" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.document_symbol_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); + if let Some(options) = parse_register_capabilities(reg)? { + server.update_capabilities(|capabilities| { + capabilities.document_symbol_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } } "textDocument/codeAction" => { if let Some(options) = reg @@ -11791,11 +11735,12 @@ impl LspStore { } } "textDocument/definition" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.definition_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); + if let Some(options) = parse_register_capabilities(reg)? { + server.update_capabilities(|capabilities| { + capabilities.definition_provider = Some(options); + }); + notify_server_capabilities_updated(&server, cx); + } } "textDocument/completion" => { if let Some(caps) = reg @@ -11903,7 +11848,7 @@ impl LspStore { notify_server_capabilities_updated(&server, cx); } } - "textDocument/documentColor" => { + "textDocument/colorProvider" => { if let Some(caps) = reg .register_options .map(serde_json::from_value) @@ -12054,7 +11999,7 @@ impl LspStore { }); notify_server_capabilities_updated(&server, cx); } - "textDocument/documentColor" => { + "textDocument/colorProvider" => { server.update_capabilities(|capabilities| { capabilities.color_provider = None; }); @@ -12067,88 +12012,6 @@ impl LspStore { Ok(()) } - async fn query_lsp_locally( - lsp_store: Entity, - sender_id: proto::PeerId, - lsp_request_id: LspRequestId, - proto_request: T::ProtoRequest, - position: Option, - mut cx: AsyncApp, - ) -> Result<()> - where - T: LspCommand + Clone, - T::ProtoRequest: proto::LspRequestMessage, - ::Response: - Into<::Response>, - { - let buffer_id = BufferId::new(proto_request.buffer_id())?; - let version = deserialize_version(proto_request.buffer_version()); - let buffer = lsp_store.update(&mut cx, |this, cx| { - this.buffer_store.read(cx).get_existing(buffer_id) - })??; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(version.clone()) - })? - .await?; - let buffer_version = buffer.read_with(&cx, |buffer, _| buffer.version())?; - let request = - T::from_proto(proto_request, lsp_store.clone(), buffer.clone(), cx.clone()).await?; - lsp_store.update(&mut cx, |lsp_store, cx| { - let request_task = - lsp_store.request_multiple_lsp_locally(&buffer, position, request, cx); - let existing_queries = lsp_store - .running_lsp_requests - .entry(TypeId::of::()) - .or_default(); - if T::ProtoRequest::stop_previous_requests() - || buffer_version.changed_since(&existing_queries.0) - { - existing_queries.1.clear(); - } - existing_queries.1.insert( - lsp_request_id, - cx.spawn(async move |lsp_store, cx| { - let response = request_task.await; - lsp_store - .update(cx, |lsp_store, cx| { - if let Some((client, project_id)) = lsp_store.downstream_client.clone() - { - let response = response - .into_iter() - .map(|(server_id, response)| { - ( - server_id.to_proto(), - T::response_to_proto( - response, - lsp_store, - sender_id, - &buffer_version, - cx, - ) - .into(), - ) - }) - .collect::>(); - match client.send_lsp_response::( - project_id, - lsp_request_id, - response, - ) { - Ok(()) => {} - Err(e) => { - log::error!("Failed to send LSP response: {e:#}",) - } - } - } - }) - .ok(); - }), - ); - })?; - Ok(()) - } - fn take_text_document_sync_options( capabilities: &mut lsp::ServerCapabilities, ) -> lsp::TextDocumentSyncOptions { @@ -12162,22 +12025,16 @@ impl LspStore { None => lsp::TextDocumentSyncOptions::default(), } } - - #[cfg(any(test, feature = "test-support"))] - pub fn forget_code_lens_task(&mut self, buffer_id: BufferId) -> Option { - let data = self.lsp_code_lens.get_mut(&buffer_id)?; - Some(data.update.take()?.1) - } } // Registration with registerOptions as null, should fallback to true. // https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/client.ts#L2133 fn parse_register_capabilities( reg: lsp::Registration, -) -> Result> { +) -> anyhow::Result>> { Ok(match reg.register_options { - Some(options) => OneOf::Right(serde_json::from_value::(options)?), - None => OneOf::Left(true), + Some(options) => Some(OneOf::Right(serde_json::from_value::(options)?)), + None => Some(OneOf::Left(true)), }) } diff --git a/crates/project/src/lsp_store/rust_analyzer_ext.rs b/crates/project/src/lsp_store/rust_analyzer_ext.rs index 54f63220b1..e5e6338d3c 100644 --- a/crates/project/src/lsp_store/rust_analyzer_ext.rs +++ b/crates/project/src/lsp_store/rust_analyzer_ext.rs @@ -1,8 +1,8 @@ use ::serde::{Deserialize, Serialize}; use anyhow::Context as _; -use gpui::{App, AsyncApp, Entity, Task, WeakEntity}; -use language::{Buffer, ServerHealth}; -use lsp::{LanguageServer, LanguageServerId, LanguageServerName}; +use gpui::{App, Entity, Task, WeakEntity}; +use language::ServerHealth; +use lsp::{LanguageServer, LanguageServerName}; use rpc::proto; use crate::{LspStore, LspStoreEvent, Project, ProjectPath, lsp_store}; @@ -83,32 +83,31 @@ pub fn register_notifications(lsp_store: WeakEntity, language_server: pub fn cancel_flycheck( project: Entity, - buffer_path: Option, + buffer_path: ProjectPath, cx: &mut App, ) -> Task> { let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); let lsp_store = project.read(cx).lsp_store(); - let buffer = buffer_path.map(|buffer_path| { - project.update(cx, |project, cx| { - project.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(buffer_path, cx) - }) + let buffer = project.update(cx, |project, cx| { + project.buffer_store().update(cx, |buffer_store, cx| { + buffer_store.open_buffer(buffer_path, cx) }) }); cx.spawn(async move |cx| { - let buffer = match buffer { - Some(buffer) => Some(buffer.await?), - None => None, - }; - let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), cx) + let buffer = buffer.await?; + let Some(rust_analyzer_server) = project.read_with(cx, |project, cx| { + project.language_server_id_for_name(buffer.read(cx), &RUST_ANALYZER_NAME, cx) + })? else { return Ok(()); }; + let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto())?; if let Some((client, project_id)) = upstream_client { let request = proto::LspExtCancelFlycheck { project_id, + buffer_id, language_server_id: rust_analyzer_server.to_proto(), }; client @@ -131,33 +130,28 @@ pub fn cancel_flycheck( pub fn run_flycheck( project: Entity, - buffer_path: Option, + buffer_path: ProjectPath, cx: &mut App, ) -> Task> { let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); let lsp_store = project.read(cx).lsp_store(); - let buffer = buffer_path.map(|buffer_path| { - project.update(cx, |project, cx| { - project.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(buffer_path, cx) - }) + let buffer = project.update(cx, |project, cx| { + project.buffer_store().update(cx, |buffer_store, cx| { + buffer_store.open_buffer(buffer_path, cx) }) }); cx.spawn(async move |cx| { - let buffer = match buffer { - Some(buffer) => Some(buffer.await?), - None => None, - }; - let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), cx) + let buffer = buffer.await?; + let Some(rust_analyzer_server) = project.read_with(cx, |project, cx| { + project.language_server_id_for_name(buffer.read(cx), &RUST_ANALYZER_NAME, cx) + })? else { return Ok(()); }; + let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto())?; if let Some((client, project_id)) = upstream_client { - let buffer_id = buffer - .map(|buffer| buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto())) - .transpose()?; let request = proto::LspExtRunFlycheck { project_id, buffer_id, @@ -188,32 +182,31 @@ pub fn run_flycheck( pub fn clear_flycheck( project: Entity, - buffer_path: Option, + buffer_path: ProjectPath, cx: &mut App, ) -> Task> { let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); let lsp_store = project.read(cx).lsp_store(); - let buffer = buffer_path.map(|buffer_path| { - project.update(cx, |project, cx| { - project.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(buffer_path, cx) - }) + let buffer = project.update(cx, |project, cx| { + project.buffer_store().update(cx, |buffer_store, cx| { + buffer_store.open_buffer(buffer_path, cx) }) }); cx.spawn(async move |cx| { - let buffer = match buffer { - Some(buffer) => Some(buffer.await?), - None => None, - }; - let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), cx) + let buffer = buffer.await?; + let Some(rust_analyzer_server) = project.read_with(cx, |project, cx| { + project.language_server_id_for_name(buffer.read(cx), &RUST_ANALYZER_NAME, cx) + })? else { return Ok(()); }; + let buffer_id = buffer.read_with(cx, |buffer, _| buffer.remote_id().to_proto())?; if let Some((client, project_id)) = upstream_client { let request = proto::LspExtClearFlycheck { project_id, + buffer_id, language_server_id: rust_analyzer_server.to_proto(), }; client @@ -233,40 +226,3 @@ pub fn clear_flycheck( anyhow::Ok(()) }) } - -fn find_rust_analyzer_server( - project: &Entity, - buffer: Option<&Entity>, - cx: &mut AsyncApp, -) -> Option { - project - .read_with(cx, |project, cx| { - buffer - .and_then(|buffer| { - project.language_server_id_for_name(buffer.read(cx), &RUST_ANALYZER_NAME, cx) - }) - // If no rust-analyzer found for the current buffer (e.g. `settings.json`), fall back to the project lookup - // and use project's rust-analyzer if it's the only one. - .or_else(|| { - let rust_analyzer_servers = project - .lsp_store() - .read(cx) - .language_server_statuses - .iter() - .filter_map(|(server_id, server_status)| { - if server_status.name == RUST_ANALYZER_NAME { - Some(*server_id) - } else { - None - } - }) - .collect::>(); - if rust_analyzer_servers.len() == 1 { - rust_analyzer_servers.first().copied() - } else { - None - } - }) - }) - .ok()? -} diff --git a/crates/project/src/manifest_tree/server_tree.rs b/crates/project/src/manifest_tree/server_tree.rs index 48e2007d47..5e5f4bab49 100644 --- a/crates/project/src/manifest_tree/server_tree.rs +++ b/crates/project/src/manifest_tree/server_tree.rs @@ -181,7 +181,6 @@ impl LanguageServerTree { &root_path.path, language_name.clone(), ); - ( Arc::new(InnerTreeNode::new( adapter.name(), @@ -409,7 +408,6 @@ impl ServerTreeRebase { if live_node.id.get().is_some() { return Some(node); } - let disposition = &live_node.disposition; let Some((existing_node, _)) = self .old_contents diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 9fd4eed641..e47c020a42 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -327,7 +327,6 @@ pub enum Event { RevealInProjectPanel(ProjectEntryId), SnippetEdit(BufferId, Vec<(lsp::Range, Snippet)>), ExpandedAllForEntry(WorktreeId, ProjectEntryId), - EntryRenamed(ProjectTransaction), AgentLocationChanged, } @@ -2120,7 +2119,7 @@ impl Project { let is_root_entry = self.entry_is_worktree_root(entry_id, cx); let lsp_store = self.lsp_store().downgrade(); - cx.spawn(async move |project, cx| { + cx.spawn(async move |_, cx| { let (old_abs_path, new_abs_path) = { let root_path = worktree.read_with(cx, |this, _| this.abs_path())?; let new_abs_path = if is_root_entry { @@ -2130,7 +2129,7 @@ impl Project { }; (root_path.join(&old_path), new_abs_path) }; - let transaction = LspStore::will_rename_entry( + LspStore::will_rename_entry( lsp_store.clone(), worktree_id, &old_abs_path, @@ -2146,12 +2145,6 @@ impl Project { })? .await?; - project - .update(cx, |_, cx| { - cx.emit(Event::EntryRenamed(transaction)); - }) - .ok(); - lsp_store .read_with(cx, |this, _| { this.did_rename_entry(worktree_id, &old_abs_path, &new_abs_path, is_dir); @@ -3422,7 +3415,7 @@ impl Project { buffer: &Entity, position: T, cx: &mut Context, - ) -> Task>>> { + ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); let guard = self.retain_remotely_created_models(cx); let task = self.lsp_store.update(cx, |lsp_store, cx| { @@ -3440,7 +3433,7 @@ impl Project { buffer: &Entity, position: T, cx: &mut Context, - ) -> Task>>> { + ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); let guard = self.retain_remotely_created_models(cx); let task = self.lsp_store.update(cx, |lsp_store, cx| { @@ -3458,7 +3451,7 @@ impl Project { buffer: &Entity, position: T, cx: &mut Context, - ) -> Task>>> { + ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); let guard = self.retain_remotely_created_models(cx); let task = self.lsp_store.update(cx, |lsp_store, cx| { @@ -3476,7 +3469,7 @@ impl Project { buffer: &Entity, position: T, cx: &mut Context, - ) -> Task>>> { + ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); let guard = self.retain_remotely_created_models(cx); let task = self.lsp_store.update(cx, |lsp_store, cx| { @@ -3494,7 +3487,7 @@ impl Project { buffer: &Entity, position: T, cx: &mut Context, - ) -> Task>>> { + ) -> Task>> { let position = position.to_point_utf16(buffer.read(cx)); let guard = self.retain_remotely_created_models(cx); let task = self.lsp_store.update(cx, |lsp_store, cx| { @@ -3592,12 +3585,23 @@ impl Project { }) } + pub fn signature_help( + &self, + buffer: &Entity, + position: T, + cx: &mut Context, + ) -> Task> { + self.lsp_store.update(cx, |lsp_store, cx| { + lsp_store.signature_help(buffer, position, cx) + }) + } + pub fn hover( &self, buffer: &Entity, position: T, cx: &mut Context, - ) -> Task>> { + ) -> Task> { let position = position.to_point_utf16(buffer.read(cx)); self.lsp_store .update(cx, |lsp_store, cx| lsp_store.hover(buffer, position, cx)) @@ -3633,7 +3637,7 @@ impl Project { range: Range, kinds: Option>, cx: &mut Context, - ) -> Task>>> { + ) -> Task>> { let buffer = buffer_handle.read(cx); let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end); self.lsp_store.update(cx, |lsp_store, cx| { @@ -3646,7 +3650,7 @@ impl Project { buffer: &Entity, range: Range, cx: &mut Context, - ) -> Task>>> { + ) -> Task>> { let snapshot = buffer.read(cx).snapshot(); let range = range.to_point(&snapshot); let range_start = snapshot.anchor_before(range.start); @@ -3664,18 +3668,16 @@ impl Project { let mut code_lens_actions = code_lens_actions .await .map_err(|e| anyhow!("code lens fetch failed: {e:#}"))?; - if let Some(code_lens_actions) = &mut code_lens_actions { - code_lens_actions.retain(|code_lens_action| { - range - .start - .cmp(&code_lens_action.range.start, &snapshot) - .is_ge() - && range - .end - .cmp(&code_lens_action.range.end, &snapshot) - .is_le() - }); - } + code_lens_actions.retain(|code_lens_action| { + range + .start + .cmp(&code_lens_action.range.start, &snapshot) + .is_ge() + && range + .end + .cmp(&code_lens_action.range.end, &snapshot) + .is_le() + }); Ok(code_lens_actions) }) } diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 4447c25129..a6fea4059c 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -181,6 +181,17 @@ pub struct DiagnosticsSettings { /// Settings for showing inline diagnostics. pub inline: InlineDiagnosticsSettings, + + /// Configuration, related to Rust language diagnostics. + pub cargo: Option, +} + +impl DiagnosticsSettings { + pub fn fetch_cargo_diagnostics(&self) -> bool { + self.cargo + .as_ref() + .is_some_and(|cargo_diagnostics| cargo_diagnostics.fetch_cargo_diagnostics) + } } #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] @@ -247,6 +258,7 @@ impl Default for DiagnosticsSettings { include_warnings: true, lsp_pull_diagnostics: LspPullDiagnosticsSettings::default(), inline: InlineDiagnosticsSettings::default(), + cargo: None, } } } @@ -280,6 +292,16 @@ impl Default for GlobalLspSettings { } } +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +pub struct CargoDiagnosticsSettings { + /// When enabled, Zed disables rust-analyzer's check on save and starts to query + /// Cargo diagnostics separately. + /// + /// Default: false + #[serde(default)] + pub fetch_cargo_diagnostics: bool, +} + #[derive( Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, JsonSchema, )] diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 6dcd07482e..8b0b21fcd6 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -4,7 +4,6 @@ use crate::{ Event, git_store::StatusEntry, task_inventory::TaskContexts, task_store::TaskSettingsLocation, *, }; -use async_trait::async_trait; use buffer_diff::{ BufferDiffEvent, CALCULATE_DIFF_TASK, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind, assert_hunks, @@ -22,8 +21,7 @@ use http_client::Url; use itertools::Itertools; use language::{ Diagnostic, DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, DiskState, FakeLspAdapter, - LanguageConfig, LanguageMatcher, LanguageName, LineEnding, ManifestName, ManifestProvider, - ManifestQuery, OffsetRangeExt, Point, ToPoint, ToolchainLister, + LanguageConfig, LanguageMatcher, LanguageName, LineEnding, OffsetRangeExt, Point, ToPoint, language_settings::{AllLanguageSettings, LanguageSettingsContent, language_settings}, tree_sitter_rust, tree_sitter_typescript, }; @@ -142,10 +140,8 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) { end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true - max_line_length = 120 [*.js] tab_width = 10 - max_line_length = off "#, ".zed": { "settings.json": r#"{ @@ -153,8 +149,7 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) { "hard_tabs": false, "ensure_final_newline_on_save": false, "remove_trailing_whitespace_on_save": false, - "preferred_line_length": 64, - "soft_wrap": "editor_width", + "soft_wrap": "editor_width" }"#, }, "a.rs": "fn a() {\n A\n}", @@ -162,7 +157,6 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) { ".editorconfig": r#" [*.rs] indent_size = 2 - max_line_length = off, "#, "b.rs": "fn b() {\n B\n}", }, @@ -211,7 +205,6 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) { assert_eq!(settings_a.hard_tabs, true); assert_eq!(settings_a.ensure_final_newline_on_save, true); assert_eq!(settings_a.remove_trailing_whitespace_on_save, true); - assert_eq!(settings_a.preferred_line_length, 120); // .editorconfig in b/ overrides .editorconfig in root assert_eq!(Some(settings_b.tab_size), NonZeroU32::new(2)); @@ -219,10 +212,6 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) { // "indent_size" is not set, so "tab_width" is used assert_eq!(Some(settings_c.tab_size), NonZeroU32::new(10)); - // When max_line_length is "off", default to .zed/settings.json - assert_eq!(settings_b.preferred_line_length, 64); - assert_eq!(settings_c.preferred_line_length, 64); - // README.md should not be affected by .editorconfig's globe "*.rs" assert_eq!(Some(settings_readme.tab_size), NonZeroU32::new(8)); }); @@ -598,203 +587,6 @@ async fn test_fallback_to_single_worktree_tasks(cx: &mut gpui::TestAppContext) { ); } -#[gpui::test] -async fn test_running_multiple_instances_of_a_single_server_in_one_worktree( - cx: &mut gpui::TestAppContext, -) { - pub(crate) struct PyprojectTomlManifestProvider; - - impl ManifestProvider for PyprojectTomlManifestProvider { - fn name(&self) -> ManifestName { - SharedString::new_static("pyproject.toml").into() - } - - fn search( - &self, - ManifestQuery { - path, - depth, - delegate, - }: ManifestQuery, - ) -> Option> { - for path in path.ancestors().take(depth) { - let p = path.join("pyproject.toml"); - if delegate.exists(&p, Some(false)) { - return Some(path.into()); - } - } - - None - } - } - - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - fs.insert_tree( - path!("/the-root"), - json!({ - ".zed": { - "settings.json": r#" - { - "languages": { - "Python": { - "language_servers": ["ty"] - } - } - }"# - }, - "project-a": { - ".venv": {}, - "file.py": "", - "pyproject.toml": "" - }, - "project-b": { - ".venv": {}, - "source_file.py":"", - "another_file.py": "", - "pyproject.toml": "" - } - }), - ) - .await; - cx.update(|cx| { - ManifestProvidersStore::global(cx).register(Arc::new(PyprojectTomlManifestProvider)) - }); - - let project = Project::test(fs.clone(), [path!("/the-root").as_ref()], cx).await; - let language_registry = project.read_with(cx, |project, _| project.languages().clone()); - let _fake_python_server = language_registry.register_fake_lsp( - "Python", - FakeLspAdapter { - name: "ty", - capabilities: lsp::ServerCapabilities { - ..Default::default() - }, - ..Default::default() - }, - ); - - language_registry.add(python_lang(fs.clone())); - let (first_buffer, _handle) = project - .update(cx, |project, cx| { - project.open_local_buffer_with_lsp(path!("/the-root/project-a/file.py"), cx) - }) - .await - .unwrap(); - cx.executor().run_until_parked(); - let servers = project.update(cx, |project, cx| { - project.lsp_store.update(cx, |this, cx| { - first_buffer.update(cx, |buffer, cx| { - this.language_servers_for_local_buffer(buffer, cx) - .map(|(adapter, server)| (adapter.clone(), server.clone())) - .collect::>() - }) - }) - }); - cx.executor().run_until_parked(); - assert_eq!(servers.len(), 1); - let (adapter, server) = servers.into_iter().next().unwrap(); - assert_eq!(adapter.name(), LanguageServerName::new_static("ty")); - assert_eq!(server.server_id(), LanguageServerId(0)); - // `workspace_folders` are set to the rooting point. - assert_eq!( - server.workspace_folders(), - BTreeSet::from_iter( - [Url::from_file_path(path!("/the-root/project-a")).unwrap()].into_iter() - ) - ); - - let (second_project_buffer, _other_handle) = project - .update(cx, |project, cx| { - project.open_local_buffer_with_lsp(path!("/the-root/project-b/source_file.py"), cx) - }) - .await - .unwrap(); - cx.executor().run_until_parked(); - let servers = project.update(cx, |project, cx| { - project.lsp_store.update(cx, |this, cx| { - second_project_buffer.update(cx, |buffer, cx| { - this.language_servers_for_local_buffer(buffer, cx) - .map(|(adapter, server)| (adapter.clone(), server.clone())) - .collect::>() - }) - }) - }); - cx.executor().run_until_parked(); - assert_eq!(servers.len(), 1); - let (adapter, server) = servers.into_iter().next().unwrap(); - assert_eq!(adapter.name(), LanguageServerName::new_static("ty")); - // We're not using venvs at all here, so both folders should fall under the same root. - assert_eq!(server.server_id(), LanguageServerId(0)); - // Now, let's select a different toolchain for one of subprojects. - let (available_toolchains_for_b, root_path) = project - .update(cx, |this, cx| { - let worktree_id = this.worktrees(cx).next().unwrap().read(cx).id(); - this.available_toolchains( - ProjectPath { - worktree_id, - path: Arc::from("project-b/source_file.py".as_ref()), - }, - LanguageName::new("Python"), - cx, - ) - }) - .await - .expect("A toolchain to be discovered"); - assert_eq!(root_path.as_ref(), Path::new("project-b")); - assert_eq!(available_toolchains_for_b.toolchains().len(), 1); - let currently_active_toolchain = project - .update(cx, |this, cx| { - let worktree_id = this.worktrees(cx).next().unwrap().read(cx).id(); - this.active_toolchain( - ProjectPath { - worktree_id, - path: Arc::from("project-b/source_file.py".as_ref()), - }, - LanguageName::new("Python"), - cx, - ) - }) - .await; - - assert!(currently_active_toolchain.is_none()); - let _ = project - .update(cx, |this, cx| { - let worktree_id = this.worktrees(cx).next().unwrap().read(cx).id(); - this.activate_toolchain( - ProjectPath { - worktree_id, - path: root_path, - }, - available_toolchains_for_b - .toolchains - .into_iter() - .next() - .unwrap(), - cx, - ) - }) - .await - .unwrap(); - cx.run_until_parked(); - let servers = project.update(cx, |project, cx| { - project.lsp_store.update(cx, |this, cx| { - second_project_buffer.update(cx, |buffer, cx| { - this.language_servers_for_local_buffer(buffer, cx) - .map(|(adapter, server)| (adapter.clone(), server.clone())) - .collect::>() - }) - }) - }); - cx.executor().run_until_parked(); - assert_eq!(servers.len(), 1); - let (adapter, server) = servers.into_iter().next().unwrap(); - assert_eq!(adapter.name(), LanguageServerName::new_static("ty")); - // There's a new language server in town. - assert_eq!(server.server_id(), LanguageServerId(1)); -} - #[gpui::test] async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { init_test(cx); @@ -3213,7 +3005,6 @@ async fn test_definition(cx: &mut gpui::TestAppContext) { let mut definitions = project .update(cx, |project, cx| project.definitions(&buffer, 22, cx)) .await - .unwrap() .unwrap(); // Assert no new language server started @@ -3728,7 +3519,7 @@ async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { .next() .await; - let action = actions.await.unwrap().unwrap()[0].clone(); + let action = actions.await.unwrap()[0].clone(); let apply = project.update(cx, |project, cx| { project.apply_code_action(buffer.clone(), action, true, cx) }); @@ -6319,7 +6110,6 @@ async fn test_multiple_language_server_hovers(cx: &mut gpui::TestAppContext) { hover_task .await .into_iter() - .flatten() .map(|hover| hover.contents.iter().map(|block| &block.text).join("|")) .sorted() .collect::>(), @@ -6393,7 +6183,6 @@ async fn test_hovers_with_empty_parts(cx: &mut gpui::TestAppContext) { hover_task .await .into_iter() - .flatten() .map(|hover| hover.contents.iter().map(|block| &block.text).join("|")) .sorted() .collect::>(), @@ -6472,7 +6261,7 @@ async fn test_code_actions_only_kinds(cx: &mut gpui::TestAppContext) { .await .expect("The code action request should have been triggered"); - let code_actions = code_actions_task.await.unwrap().unwrap(); + let code_actions = code_actions_task.await.unwrap(); assert_eq!(code_actions.len(), 1); assert_eq!( code_actions[0].lsp_action.action_kind(), @@ -6631,7 +6420,6 @@ async fn test_multiple_language_server_actions(cx: &mut gpui::TestAppContext) { code_actions_task .await .unwrap() - .unwrap() .into_iter() .map(|code_action| code_action.lsp_action.title().to_owned()) .sorted() @@ -9181,65 +8969,6 @@ fn rust_lang() -> Arc { )) } -fn python_lang(fs: Arc) -> Arc { - struct PythonMootToolchainLister(Arc); - #[async_trait] - impl ToolchainLister for PythonMootToolchainLister { - async fn list( - &self, - worktree_root: PathBuf, - subroot_relative_path: Option>, - _: Option>, - ) -> ToolchainList { - // This lister will always return a path .venv directories within ancestors - let ancestors = subroot_relative_path - .into_iter() - .flat_map(|path| path.ancestors().map(ToOwned::to_owned).collect::>()); - let mut toolchains = vec![]; - for ancestor in ancestors { - let venv_path = worktree_root.join(ancestor).join(".venv"); - if self.0.is_dir(&venv_path).await { - toolchains.push(Toolchain { - name: SharedString::new("Python Venv"), - path: venv_path.to_string_lossy().into_owned().into(), - language_name: LanguageName(SharedString::new_static("Python")), - as_json: serde_json::Value::Null, - }) - } - } - ToolchainList { - toolchains, - ..Default::default() - } - } - // Returns a term which we should use in UI to refer to a toolchain. - fn term(&self) -> SharedString { - SharedString::new_static("virtual environment") - } - /// Returns the name of the manifest file for this toolchain. - fn manifest_name(&self) -> ManifestName { - SharedString::new_static("pyproject.toml").into() - } - } - Arc::new( - Language::new( - LanguageConfig { - name: "Python".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["py".to_string()], - ..Default::default() - }, - ..Default::default() - }, - None, // We're not testing Python parsing with this language. - ) - .with_manifest(Some(ManifestName::from(SharedString::new_static( - "pyproject.toml", - )))) - .with_toolchain_lister(Some(Arc::new(PythonMootToolchainLister(fs)))), - ) -} - fn typescript_lang() -> Arc { Arc::new(Language::new( LanguageConfig { diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index b009b357fe..e9582e73fd 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -4,7 +4,7 @@ use collections::HashMap; use gpui::{App, AppContext as _, Context, Entity, Task, WeakEntity}; use itertools::Itertools; use language::LanguageName; -use remote::{SshInfo, ssh_session::SshArgs}; +use remote::ssh_session::SshArgs; use settings::{Settings, SettingsLocation}; use smol::channel::bounded; use std::{ @@ -13,7 +13,7 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; -use task::{Shell, ShellBuilder, SpawnInTerminal}; +use task::{DEFAULT_REMOTE_SHELL, Shell, ShellBuilder, SpawnInTerminal}; use terminal::{ TaskState, TaskStatus, Terminal, TerminalBuilder, terminal_settings::{self, ActivateScript, TerminalSettings, VenvSettings}, @@ -58,13 +58,11 @@ impl SshCommand { } } -#[derive(Debug)] pub struct SshDetails { pub host: String, pub ssh_command: SshCommand, pub envs: Option>, pub path_style: PathStyle, - pub shell: String, } impl Project { @@ -89,18 +87,12 @@ impl Project { pub fn ssh_details(&self, cx: &App) -> Option { if let Some(ssh_client) = &self.ssh_client { let ssh_client = ssh_client.read(cx); - if let Some(SshInfo { - args: SshArgs { arguments, envs }, - path_style, - shell, - }) = ssh_client.ssh_info() - { + if let Some((SshArgs { arguments, envs }, path_style)) = ssh_client.ssh_info() { return Some(SshDetails { host: ssh_client.connection_options().host, ssh_command: SshCommand { arguments }, envs, path_style, - shell, }); } } @@ -173,9 +165,7 @@ impl Project { let ssh_details = self.ssh_details(cx); let settings = self.terminal_settings(&path, cx).clone(); - let builder = - ShellBuilder::new(ssh_details.as_ref().map(|ssh| &*ssh.shell), &settings.shell) - .non_interactive(); + let builder = ShellBuilder::new(ssh_details.is_none(), &settings.shell).non_interactive(); let (command, args) = builder.build(Some(command), &Vec::new()); let mut env = self @@ -190,11 +180,9 @@ impl Project { ssh_command, envs, path_style, - shell, .. }) => { let (command, args) = wrap_for_ssh( - &shell, &ssh_command, Some((&command, &args)), path.as_deref(), @@ -292,7 +280,6 @@ impl Project { ssh_command, envs, path_style, - shell, }) => { log::debug!("Connecting to a remote server: {ssh_command:?}"); @@ -304,7 +291,6 @@ impl Project { .or_insert_with(|| "xterm-256color".to_string()); let (program, args) = wrap_for_ssh( - &shell, &ssh_command, None, path.as_deref(), @@ -357,13 +343,11 @@ impl Project { ssh_command, envs, path_style, - shell, }) => { log::debug!("Connecting to a remote server: {ssh_command:?}"); env.entry("TERM".to_string()) .or_insert_with(|| "xterm-256color".to_string()); let (program, args) = wrap_for_ssh( - &shell, &ssh_command, spawn_task .command @@ -653,7 +637,6 @@ impl Project { } pub fn wrap_for_ssh( - shell: &str, ssh_command: &SshCommand, command: Option<(&String, &Vec)>, path: Option<&Path>, @@ -662,11 +645,16 @@ pub fn wrap_for_ssh( path_style: PathStyle, ) -> (String, Vec) { let to_run = if let Some((command, args)) = command { - let command: Option> = shlex::try_quote(command).ok(); + // DEFAULT_REMOTE_SHELL is '"${SHELL:-sh}"' so must not be escaped + let command: Option> = if command == DEFAULT_REMOTE_SHELL { + Some(command.into()) + } else { + shlex::try_quote(command).ok() + }; let args = args.iter().filter_map(|arg| shlex::try_quote(arg).ok()); command.into_iter().chain(args).join(" ") } else { - format!("exec {shell} -l") + "exec ${SHELL:-sh} -l".to_string() }; let mut env_changes = String::new(); @@ -700,7 +688,7 @@ pub fn wrap_for_ssh( } else { format!("cd; {env_changes} {to_run}") }; - let shell_invocation = format!("{shell} -c {}", shlex::try_quote(&commands).unwrap()); + let shell_invocation = format!("sh -c {}", shlex::try_quote(&commands).unwrap()); let program = "ssh".to_string(); let mut args = ssh_command.arguments.clone(); diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 5a30a3e9bc..52ec7a9880 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -69,7 +69,6 @@ use workspace::{ notifications::{DetachAndPromptErr, NotifyTaskExt}, }; use worktree::CreatedEntry; -use zed_actions::workspace::OpenWithSystem; const PROJECT_PANEL_KEY: &str = "ProjectPanel"; const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; @@ -256,6 +255,8 @@ actions!( RevealInFileManager, /// Removes the selected folder from the project. RemoveFromProject, + /// Opens the selected file with the system's default application. + OpenWithSystem, /// Cuts the selected file or directory. Cut, /// Pastes the previously cut or copied item. @@ -4089,7 +4090,6 @@ impl ProjectPanel { .when(!is_sticky, |this| { this .when(is_highlighted && folded_directory_drag_target.is_none(), |this| this.border_color(transparent_white()).bg(item_colors.drag_over)) - .when(settings.drag_and_drop, |this| this .on_drag_move::(cx.listener( move |this, event: &DragMoveEvent, _, cx| { let is_current_target = this.drag_target_entry.as_ref() @@ -4223,7 +4223,7 @@ impl ProjectPanel { } this.drag_onto(selections, entry_id, kind.is_file(), window, cx); }), - )) + ) }) .on_mouse_down( MouseButton::Left, @@ -4434,7 +4434,6 @@ impl ProjectPanel { div() .when(!is_sticky, |div| { div - .when(settings.drag_and_drop, |div| div .on_drop(cx.listener(move |this, selections: &DraggedSelection, window, cx| { this.hover_scroll_task.take(); this.drag_target_entry = None; @@ -4466,7 +4465,7 @@ impl ProjectPanel { } }, - ))) + )) }) .child( Label::new(DELIMITER.clone()) @@ -4486,7 +4485,6 @@ impl ProjectPanel { .when(index != components_len - 1, |div|{ let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - index).cloned(); div - .when(settings.drag_and_drop, |div| div .on_drag_move(cx.listener( move |this, event: &DragMoveEvent, _, _| { if event.bounds.contains(&event.event.position) { @@ -4524,7 +4522,7 @@ impl ProjectPanel { target.index == index ), |this| { this.bg(item_colors.drag_over) - })) + }) }) }) .on_click(cx.listener(move |this, _, _, cx| { @@ -5032,8 +5030,7 @@ impl ProjectPanel { sticky_parents.reverse(); - let panel_settings = ProjectPanelSettings::get_global(cx); - let git_status_enabled = panel_settings.git_status; + let git_status_enabled = ProjectPanelSettings::get_global(cx).git_status; let root_name = OsStr::new(worktree.root_name()); let git_summaries_by_id = if git_status_enabled { @@ -5117,11 +5114,11 @@ impl Render for ProjectPanel { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let has_worktree = !self.visible_entries.is_empty(); let project = self.project.read(cx); - let panel_settings = ProjectPanelSettings::get_global(cx); - let indent_size = panel_settings.indent_size; - let show_indent_guides = panel_settings.indent_guides.show == ShowIndentGuides::Always; + let indent_size = ProjectPanelSettings::get_global(cx).indent_size; + let show_indent_guides = + ProjectPanelSettings::get_global(cx).indent_guides.show == ShowIndentGuides::Always; let show_sticky_entries = { - if panel_settings.sticky_scroll { + if ProjectPanelSettings::get_global(cx).sticky_scroll { let is_scrollable = self.scroll_handle.is_scrollable(); let is_scrolled = self.scroll_handle.offset().y < px(0.); is_scrollable && is_scrolled @@ -5209,10 +5206,8 @@ impl Render for ProjectPanel { h_flex() .id("project-panel") .group("project-panel") - .when(panel_settings.drag_and_drop, |this| { - this.on_drag_move(cx.listener(handle_drag_move::)) - .on_drag_move(cx.listener(handle_drag_move::)) - }) + .on_drag_move(cx.listener(handle_drag_move::)) + .on_drag_move(cx.listener(handle_drag_move::)) .size_full() .relative() .on_modifiers_changed(cx.listener( @@ -5550,32 +5545,30 @@ impl Render for ProjectPanel { })), ) .when(is_local, |div| { - div.when(panel_settings.drag_and_drop, |div| { - div.drag_over::(|style, _, _, cx| { - style.bg(cx.theme().colors().drop_target_background) - }) - .on_drop(cx.listener( - move |this, external_paths: &ExternalPaths, window, cx| { - this.drag_target_entry = None; - this.hover_scroll_task.take(); - if let Some(task) = this - .workspace - .update(cx, |workspace, cx| { - workspace.open_workspace_for_paths( - true, - external_paths.paths().to_owned(), - window, - cx, - ) - }) - .log_err() - { - task.detach_and_log_err(cx); - } - cx.stop_propagation(); - }, - )) + div.drag_over::(|style, _, _, cx| { + style.bg(cx.theme().colors().drop_target_background) }) + .on_drop(cx.listener( + move |this, external_paths: &ExternalPaths, window, cx| { + this.drag_target_entry = None; + this.hover_scroll_task.take(); + if let Some(task) = this + .workspace + .update(cx, |workspace, cx| { + workspace.open_workspace_for_paths( + true, + external_paths.paths().to_owned(), + window, + cx, + ) + }) + .log_err() + { + task.detach_and_log_err(cx); + } + cx.stop_propagation(); + }, + )) }) } } diff --git a/crates/project_panel/src/project_panel_settings.rs b/crates/project_panel/src/project_panel_settings.rs index fc399d66a7..8a243589ed 100644 --- a/crates/project_panel/src/project_panel_settings.rs +++ b/crates/project_panel/src/project_panel_settings.rs @@ -47,7 +47,6 @@ pub struct ProjectPanelSettings { pub scrollbar: ScrollbarSettings, pub show_diagnostics: ShowDiagnostics, pub hide_root: bool, - pub drag_and_drop: bool, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -161,10 +160,6 @@ pub struct ProjectPanelSettingsContent { /// /// Default: true pub sticky_scroll: Option, - /// Whether to enable drag-and-drop operations in the project panel. - /// - /// Default: true - pub drag_and_drop: Option, } impl Settings for ProjectPanelSettings { diff --git a/crates/proto/proto/lsp.proto b/crates/proto/proto/lsp.proto index 473ef5c38c..ea9647feff 100644 --- a/crates/proto/proto/lsp.proto +++ b/crates/proto/proto/lsp.proto @@ -753,47 +753,28 @@ message TextEdit { PointUtf16 lsp_range_end = 3; } -message LspQuery { +message MultiLspQuery { uint64 project_id = 1; - uint64 lsp_request_id = 2; + uint64 buffer_id = 2; + repeated VectorClockEntry version = 3; + oneof strategy { + AllLanguageServers all = 4; + } oneof request { - GetReferences get_references = 3; - GetDocumentColor get_document_color = 4; GetHover get_hover = 5; GetCodeActions get_code_actions = 6; GetSignatureHelp get_signature_help = 7; GetCodeLens get_code_lens = 8; GetDocumentDiagnostics get_document_diagnostics = 9; - GetDefinition get_definition = 10; - GetDeclaration get_declaration = 11; - GetTypeDefinition get_type_definition = 12; - GetImplementation get_implementation = 13; + GetDocumentColor get_document_color = 10; + GetDefinition get_definition = 11; + GetDeclaration get_declaration = 12; + GetTypeDefinition get_type_definition = 13; + GetImplementation get_implementation = 14; + GetReferences get_references = 15; } } -message LspQueryResponse { - uint64 project_id = 1; - uint64 lsp_request_id = 2; - repeated LspResponse responses = 3; -} - -message LspResponse { - oneof response { - GetHoverResponse get_hover_response = 1; - GetCodeActionsResponse get_code_actions_response = 2; - GetSignatureHelpResponse get_signature_help_response = 3; - GetCodeLensResponse get_code_lens_response = 4; - GetDocumentDiagnosticsResponse get_document_diagnostics_response = 5; - GetDocumentColorResponse get_document_color_response = 6; - GetDefinitionResponse get_definition_response = 8; - GetDeclarationResponse get_declaration_response = 9; - GetTypeDefinitionResponse get_type_definition_response = 10; - GetImplementationResponse get_implementation_response = 11; - GetReferencesResponse get_references_response = 12; - } - uint64 server_id = 7; -} - message AllLanguageServers {} message LanguageServerSelector { @@ -817,6 +798,27 @@ message StopLanguageServers { bool all = 4; } +message MultiLspQueryResponse { + repeated LspResponse responses = 1; +} + +message LspResponse { + oneof response { + GetHoverResponse get_hover_response = 1; + GetCodeActionsResponse get_code_actions_response = 2; + GetSignatureHelpResponse get_signature_help_response = 3; + GetCodeLensResponse get_code_lens_response = 4; + GetDocumentDiagnosticsResponse get_document_diagnostics_response = 5; + GetDocumentColorResponse get_document_color_response = 6; + GetDefinitionResponse get_definition_response = 8; + GetDeclarationResponse get_declaration_response = 9; + GetTypeDefinitionResponse get_type_definition_response = 10; + GetImplementationResponse get_implementation_response = 11; + GetReferencesResponse get_references_response = 12; + } + uint64 server_id = 7; +} + message LspExtRunnables { uint64 project_id = 1; uint64 buffer_id = 2; @@ -834,19 +836,21 @@ message LspRunnable { message LspExtCancelFlycheck { uint64 project_id = 1; - uint64 language_server_id = 2; + uint64 buffer_id = 2; + uint64 language_server_id = 3; } message LspExtRunFlycheck { uint64 project_id = 1; - optional uint64 buffer_id = 2; + uint64 buffer_id = 2; uint64 language_server_id = 3; bool current_file_only = 4; } message LspExtClearFlycheck { uint64 project_id = 1; - uint64 language_server_id = 2; + uint64 buffer_id = 2; + uint64 language_server_id = 3; } message LspDiagnosticRelatedInformation { @@ -905,30 +909,3 @@ message PullWorkspaceDiagnostics { uint64 project_id = 1; uint64 server_id = 2; } - -// todo(lsp) remove after Zed Stable hits v0.204.x -message MultiLspQuery { - uint64 project_id = 1; - uint64 buffer_id = 2; - repeated VectorClockEntry version = 3; - oneof strategy { - AllLanguageServers all = 4; - } - oneof request { - GetHover get_hover = 5; - GetCodeActions get_code_actions = 6; - GetSignatureHelp get_signature_help = 7; - GetCodeLens get_code_lens = 8; - GetDocumentDiagnostics get_document_diagnostics = 9; - GetDocumentColor get_document_color = 10; - GetDefinition get_definition = 11; - GetDeclaration get_declaration = 12; - GetTypeDefinition get_type_definition = 13; - GetImplementation get_implementation = 14; - GetReferences get_references = 15; - } -} - -message MultiLspQueryResponse { - repeated LspResponse responses = 1; -} diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 70689bcd63..310fcf584e 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -393,10 +393,7 @@ message Envelope { GetCrashFilesResponse get_crash_files_response = 362; GitClone git_clone = 363; - GitCloneResponse git_clone_response = 364; - - LspQuery lsp_query = 365; - LspQueryResponse lsp_query_response = 366; // current max + GitCloneResponse git_clone_response = 364; // current max } reserved 87 to 88; diff --git a/crates/proto/src/macros.rs b/crates/proto/src/macros.rs index 59e984d7db..2ce0c0df25 100644 --- a/crates/proto/src/macros.rs +++ b/crates/proto/src/macros.rs @@ -69,32 +69,3 @@ macro_rules! entity_messages { })* }; } - -#[macro_export] -macro_rules! lsp_messages { - ($(($request_name:ident, $response_name:ident, $stop_previous_requests:expr)),* $(,)?) => { - $(impl LspRequestMessage for $request_name { - type Response = $response_name; - - fn to_proto_query(self) -> $crate::lsp_query::Request { - $crate::lsp_query::Request::$request_name(self) - } - - fn response_to_proto_query(response: Self::Response) -> $crate::lsp_response::Response { - $crate::lsp_response::Response::$response_name(response) - } - - fn buffer_id(&self) -> u64 { - self.buffer_id - } - - fn buffer_version(&self) -> &[$crate::VectorClockEntry] { - &self.version - } - - fn stop_previous_requests() -> bool { - $stop_previous_requests - } - })* - }; -} diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index d38e54685f..802db09590 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -169,9 +169,6 @@ messages!( (MarkNotificationRead, Foreground), (MoveChannel, Foreground), (ReorderChannel, Foreground), - (LspQuery, Background), - (LspQueryResponse, Background), - // todo(lsp) remove after Zed Stable hits v0.204.x (MultiLspQuery, Background), (MultiLspQueryResponse, Background), (OnTypeFormatting, Background), @@ -429,10 +426,7 @@ request_messages!( (SetRoomParticipantRole, Ack), (BlameBuffer, BlameBufferResponse), (RejoinRemoteProjects, RejoinRemoteProjectsResponse), - // todo(lsp) remove after Zed Stable hits v0.204.x (MultiLspQuery, MultiLspQueryResponse), - (LspQuery, Ack), - (LspQueryResponse, Ack), (RestartLanguageServers, Ack), (StopLanguageServers, Ack), (OpenContext, OpenContextResponse), @@ -484,20 +478,6 @@ request_messages!( (GitClone, GitCloneResponse) ); -lsp_messages!( - (GetReferences, GetReferencesResponse, true), - (GetDocumentColor, GetDocumentColorResponse, true), - (GetHover, GetHoverResponse, true), - (GetCodeActions, GetCodeActionsResponse, true), - (GetSignatureHelp, GetSignatureHelpResponse, true), - (GetCodeLens, GetCodeLensResponse, true), - (GetDocumentDiagnostics, GetDocumentDiagnosticsResponse, true), - (GetDefinition, GetDefinitionResponse, true), - (GetDeclaration, GetDeclarationResponse, true), - (GetTypeDefinition, GetTypeDefinitionResponse, true), - (GetImplementation, GetImplementationResponse, true), -); - entity_messages!( {project_id, ShareProject}, AddProjectCollaborator, @@ -540,9 +520,6 @@ entity_messages!( LeaveProject, LinkedEditingRange, LoadCommitDiff, - LspQuery, - LspQueryResponse, - // todo(lsp) remove after Zed Stable hits v0.204.x MultiLspQuery, RestartLanguageServers, StopLanguageServers, @@ -800,28 +777,6 @@ pub fn split_repository_update( }]) } -impl LspQuery { - pub fn query_name_and_write_permissions(&self) -> (&str, bool) { - match self.request { - Some(lsp_query::Request::GetHover(_)) => ("GetHover", false), - Some(lsp_query::Request::GetCodeActions(_)) => ("GetCodeActions", true), - Some(lsp_query::Request::GetSignatureHelp(_)) => ("GetSignatureHelp", false), - Some(lsp_query::Request::GetCodeLens(_)) => ("GetCodeLens", true), - Some(lsp_query::Request::GetDocumentDiagnostics(_)) => { - ("GetDocumentDiagnostics", false) - } - Some(lsp_query::Request::GetDefinition(_)) => ("GetDefinition", false), - Some(lsp_query::Request::GetDeclaration(_)) => ("GetDeclaration", false), - Some(lsp_query::Request::GetTypeDefinition(_)) => ("GetTypeDefinition", false), - Some(lsp_query::Request::GetImplementation(_)) => ("GetImplementation", false), - Some(lsp_query::Request::GetReferences(_)) => ("GetReferences", false), - Some(lsp_query::Request::GetDocumentColor(_)) => ("GetDocumentColor", false), - None => ("", true), - } - } -} - -// todo(lsp) remove after Zed Stable hits v0.204.x impl MultiLspQuery { pub fn request_str(&self) -> &str { match self.request { diff --git a/crates/proto/src/typed_envelope.rs b/crates/proto/src/typed_envelope.rs index f677a3b967..381a6379dc 100644 --- a/crates/proto/src/typed_envelope.rs +++ b/crates/proto/src/typed_envelope.rs @@ -31,58 +31,6 @@ pub trait RequestMessage: EnvelopedMessage { type Response: EnvelopedMessage; } -/// A trait to bind LSP request and responses for the proto layer. -/// Should be used for every LSP request that has to traverse through the proto layer. -/// -/// `lsp_messages` macro in the same crate provides a convenient way to implement this. -pub trait LspRequestMessage: EnvelopedMessage { - type Response: EnvelopedMessage; - - fn to_proto_query(self) -> crate::lsp_query::Request; - - fn response_to_proto_query(response: Self::Response) -> crate::lsp_response::Response; - - fn buffer_id(&self) -> u64; - - fn buffer_version(&self) -> &[crate::VectorClockEntry]; - - /// Whether to deduplicate the requests, or keep the previous ones running when another - /// request of the same kind is processed. - fn stop_previous_requests() -> bool; -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct LspRequestId(pub u64); - -/// A response from a single language server. -/// There could be multiple responses for a single LSP request, -/// from different servers. -pub struct ProtoLspResponse { - pub server_id: u64, - pub response: R, -} - -impl ProtoLspResponse> { - pub fn into_response(self) -> Result> { - let envelope = self - .response - .into_any() - .downcast::>() - .map_err(|_| { - anyhow::anyhow!( - "cannot downcast LspResponse to {} for message {}", - T::Response::NAME, - T::NAME, - ) - })?; - - Ok(ProtoLspResponse { - server_id: self.server_id, - response: envelope.payload, - }) - } -} - pub trait AnyTypedEnvelope: Any + Send + Sync { fn payload_type_id(&self) -> TypeId; fn payload_type_name(&self) -> &'static str; diff --git a/crates/recent_projects/src/disconnected_overlay.rs b/crates/recent_projects/src/disconnected_overlay.rs index 8ffe0ef07c..dd4d788cfd 100644 --- a/crates/recent_projects/src/disconnected_overlay.rs +++ b/crates/recent_projects/src/disconnected_overlay.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use gpui::{ClickEvent, DismissEvent, EventEmitter, FocusHandle, Focusable, Render, WeakEntity}; use project::project_settings::ProjectSettings; use remote::SshConnectionOptions; @@ -101,17 +103,17 @@ impl DisconnectedOverlay { return; }; + let Some(ssh_project) = workspace.read(cx).serialized_ssh_project() else { + return; + }; + let Some(window_handle) = window.window_handle().downcast::() else { return; }; let app_state = workspace.read(cx).app_state().clone(); - let paths = workspace - .read(cx) - .root_paths(cx) - .iter() - .map(|path| path.to_path_buf()) - .collect(); + + let paths = ssh_project.paths.iter().map(PathBuf::from).collect(); cx.spawn_in(window, async move |_, cx| { open_ssh_project( diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index fa57b588cd..2093e96cae 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -19,12 +19,15 @@ use picker::{ pub use remote_servers::RemoteServerProjects; use settings::Settings; pub use ssh_connections::SshSettings; -use std::{path::Path, sync::Arc}; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; use ui::{KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*, tooltip_container}; use util::{ResultExt, paths::PathExt}; use workspace::{ - CloseIntent, HistoryManager, ModalView, OpenOptions, PathList, SerializedWorkspaceLocation, - WORKSPACE_DB, Workspace, WorkspaceId, with_active_or_new_workspace, + CloseIntent, HistoryManager, ModalView, OpenOptions, SerializedWorkspaceLocation, WORKSPACE_DB, + Workspace, WorkspaceId, with_active_or_new_workspace, }; use zed_actions::{OpenRecent, OpenRemote}; @@ -151,7 +154,7 @@ impl Render for RecentProjects { pub struct RecentProjectsDelegate { workspace: WeakEntity, - workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>, + workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation)>, selected_match_index: usize, matches: Vec, render_paths: bool, @@ -175,15 +178,12 @@ impl RecentProjectsDelegate { } } - pub fn set_workspaces( - &mut self, - workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>, - ) { + pub fn set_workspaces(&mut self, workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation)>) { self.workspaces = workspaces; self.has_any_non_local_projects = !self .workspaces .iter() - .all(|(_, location, _)| matches!(location, SerializedWorkspaceLocation::Local)); + .all(|(_, location)| matches!(location, SerializedWorkspaceLocation::Local(_, _))); } } impl EventEmitter for RecentProjectsDelegate {} @@ -236,14 +236,15 @@ impl PickerDelegate for RecentProjectsDelegate { .workspaces .iter() .enumerate() - .filter(|(_, (id, _, _))| !self.is_current_workspace(*id, cx)) - .map(|(id, (_, _, paths))| { - let combined_string = paths - .paths() + .filter(|(_, (id, _))| !self.is_current_workspace(*id, cx)) + .map(|(id, (_, location))| { + let combined_string = location + .sorted_paths() .iter() .map(|path| path.compact().to_string_lossy().into_owned()) .collect::>() .join(""); + StringMatchCandidate::new(id, &combined_string) }) .collect::>(); @@ -278,7 +279,7 @@ impl PickerDelegate for RecentProjectsDelegate { .get(self.selected_index()) .zip(self.workspace.upgrade()) { - let (candidate_workspace_id, candidate_workspace_location, candidate_workspace_paths) = + let (candidate_workspace_id, candidate_workspace_location) = &self.workspaces[selected_match.candidate_id]; let replace_current_window = if self.create_new_window { secondary @@ -291,8 +292,8 @@ impl PickerDelegate for RecentProjectsDelegate { Task::ready(Ok(())) } else { match candidate_workspace_location { - SerializedWorkspaceLocation::Local => { - let paths = candidate_workspace_paths.paths().to_vec(); + SerializedWorkspaceLocation::Local(paths, _) => { + let paths = paths.paths().to_vec(); if replace_current_window { cx.spawn_in(window, async move |workspace, cx| { let continue_replacing = workspace @@ -320,7 +321,7 @@ impl PickerDelegate for RecentProjectsDelegate { workspace.open_workspace_for_paths(false, paths, window, cx) } } - SerializedWorkspaceLocation::Ssh(connection) => { + SerializedWorkspaceLocation::Ssh(ssh_project) => { let app_state = workspace.app_state().clone(); let replace_window = if replace_current_window { @@ -336,12 +337,12 @@ impl PickerDelegate for RecentProjectsDelegate { let connection_options = SshSettings::get_global(cx) .connection_options_for( - connection.host.clone(), - connection.port, - connection.user.clone(), + ssh_project.host.clone(), + ssh_project.port, + ssh_project.user.clone(), ); - let paths = candidate_workspace_paths.paths().to_vec(); + let paths = ssh_project.paths.iter().map(PathBuf::from).collect(); cx.spawn_in(window, async move |_, cx| { open_ssh_project( @@ -382,12 +383,12 @@ impl PickerDelegate for RecentProjectsDelegate { ) -> Option { let hit = self.matches.get(ix)?; - let (_, location, paths) = self.workspaces.get(hit.candidate_id)?; + let (_, location) = self.workspaces.get(hit.candidate_id)?; let mut path_start_offset = 0; - let (match_labels, paths): (Vec<_>, Vec<_>) = paths - .paths() + let (match_labels, paths): (Vec<_>, Vec<_>) = location + .sorted_paths() .iter() .map(|p| p.compact()) .map(|path| { @@ -415,9 +416,11 @@ impl PickerDelegate for RecentProjectsDelegate { .gap_3() .when(self.has_any_non_local_projects, |this| { this.child(match location { - SerializedWorkspaceLocation::Local => Icon::new(IconName::Screen) - .color(Color::Muted) - .into_any_element(), + SerializedWorkspaceLocation::Local(_, _) => { + Icon::new(IconName::Screen) + .color(Color::Muted) + .into_any_element() + } SerializedWorkspaceLocation::Ssh(_) => Icon::new(IconName::Server) .color(Color::Muted) .into_any_element(), @@ -565,7 +568,7 @@ impl RecentProjectsDelegate { cx: &mut Context>, ) { if let Some(selected_match) = self.matches.get(ix) { - let (workspace_id, _, _) = self.workspaces[selected_match.candidate_id]; + let (workspace_id, _) = self.workspaces[selected_match.candidate_id]; cx.spawn_in(window, async move |this, cx| { let _ = WORKSPACE_DB.delete_workspace_by_id(workspace_id).await; let workspaces = WORKSPACE_DB @@ -704,8 +707,7 @@ mod tests { }]; delegate.set_workspaces(vec![( WorkspaceId::default(), - SerializedWorkspaceLocation::Local, - PathList::new(&[path!("/test/path")]), + SerializedWorkspaceLocation::from_local_paths(vec![path!("/test/path/")]), )]); }); }) diff --git a/crates/remote/src/remote.rs b/crates/remote/src/remote.rs index 71895f1678..43eb59c0ae 100644 --- a/crates/remote/src/remote.rs +++ b/crates/remote/src/remote.rs @@ -4,6 +4,6 @@ pub mod proxy; pub mod ssh_session; pub use ssh_session::{ - ConnectionState, SshClientDelegate, SshConnectionOptions, SshInfo, SshPlatform, - SshRemoteClient, SshRemoteEvent, + ConnectionState, SshClientDelegate, SshConnectionOptions, SshPlatform, SshRemoteClient, + SshRemoteEvent, }; diff --git a/crates/remote/src/ssh_session.rs b/crates/remote/src/ssh_session.rs index 6794018470..a26f4be661 100644 --- a/crates/remote/src/ssh_session.rs +++ b/crates/remote/src/ssh_session.rs @@ -52,6 +52,11 @@ use util::{ paths::{PathStyle, RemotePathBuf}, }; +#[derive( + Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize, +)] +pub struct SshProjectId(pub u64); + #[derive(Clone)] pub struct SshSocket { connection_options: SshConnectionOptions, @@ -84,19 +89,11 @@ pub struct SshConnectionOptions { pub upload_binary_over_ssh: bool, } -#[derive(Debug, Clone, PartialEq, Eq)] pub struct SshArgs { pub arguments: Vec, pub envs: Option>, } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SshInfo { - pub args: SshArgs, - pub path_style: PathStyle, - pub shell: String, -} - #[macro_export] macro_rules! shell_script { ($fmt:expr, $($name:ident = $arg:expr),+ $(,)?) => {{ @@ -445,7 +442,7 @@ impl SshSocket { } async fn platform(&self) -> Result { - let uname = self.run_command("sh", &["-lc", "uname -sm"]).await?; + let uname = self.run_command("sh", &["-c", "uname -sm"]).await?; let Some((os, arch)) = uname.split_once(" ") else { anyhow::bail!("unknown uname: {uname:?}") }; @@ -474,16 +471,6 @@ impl SshSocket { Ok(SshPlatform { os, arch }) } - - async fn shell(&self) -> String { - match self.run_command("sh", &["-lc", "echo $SHELL"]).await { - Ok(shell) => shell.trim().to_owned(), - Err(e) => { - log::error!("Failed to get shell: {e}"); - "sh".to_owned() - } - } - } } const MAX_MISSED_HEARTBEATS: usize = 5; @@ -1165,16 +1152,12 @@ impl SshRemoteClient { cx.notify(); } - pub fn ssh_info(&self) -> Option { + pub fn ssh_info(&self) -> Option<(SshArgs, PathStyle)> { self.state .lock() .as_ref() .and_then(|state| state.ssh_connection()) - .map(|ssh_connection| SshInfo { - args: ssh_connection.ssh_args(), - path_style: ssh_connection.path_style(), - shell: ssh_connection.shell(), - }) + .map(|ssh_connection| (ssh_connection.ssh_args(), ssh_connection.path_style())) } pub fn upload_directory( @@ -1409,7 +1392,6 @@ trait RemoteConnection: Send + Sync { fn ssh_args(&self) -> SshArgs; fn connection_options(&self) -> SshConnectionOptions; fn path_style(&self) -> PathStyle; - fn shell(&self) -> String; #[cfg(any(test, feature = "test-support"))] fn simulate_disconnect(&self, _: &AsyncApp) {} @@ -1421,7 +1403,6 @@ struct SshRemoteConnection { remote_binary_path: Option, ssh_platform: SshPlatform, ssh_path_style: PathStyle, - ssh_shell: String, _temp_dir: TempDir, } @@ -1448,10 +1429,6 @@ impl RemoteConnection for SshRemoteConnection { self.socket.connection_options.clone() } - fn shell(&self) -> String { - self.ssh_shell.clone() - } - fn upload_directory( &self, src_path: PathBuf, @@ -1533,7 +1510,7 @@ impl RemoteConnection for SshRemoteConnection { let ssh_proxy_process = match self .socket - .ssh_command("sh", &["-lc", &start_proxy_command]) + .ssh_command("sh", &["-c", &start_proxy_command]) // IMPORTANT: we kill this process when we drop the task that uses it. .kill_on_drop(true) .spawn() @@ -1665,7 +1642,6 @@ impl SshRemoteConnection { "windows" => PathStyle::Windows, _ => PathStyle::Posix, }; - let ssh_shell = socket.shell().await; let mut this = Self { socket, @@ -1674,7 +1650,6 @@ impl SshRemoteConnection { remote_binary_path: None, ssh_path_style, ssh_platform, - ssh_shell, }; let (release_channel, version, commit) = cx.update(|cx| { @@ -1910,7 +1885,7 @@ impl SshRemoteConnection { .run_command( "sh", &[ - "-lc", + "-c", &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()), ], ) @@ -1988,7 +1963,7 @@ impl SshRemoteConnection { .run_command( "sh", &[ - "-lc", + "-c", &shell_script!("mkdir -p {parent}", parent = parent.to_string().as_ref()), ], ) @@ -2036,7 +2011,7 @@ impl SshRemoteConnection { dst_path = &dst_path.to_string() ) }; - self.socket.run_command("sh", &["-lc", &script]).await?; + self.socket.run_command("sh", &["-c", &script]).await?; Ok(()) } @@ -2711,10 +2686,6 @@ mod fake { fn path_style(&self) -> PathStyle { PathStyle::current() } - - fn shell(&self) -> String { - "sh".to_owned() - } } pub(super) struct Delegate; diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index 5dbb9a2771..dcec9f6fe0 100644 --- a/crates/remote_server/Cargo.toml +++ b/crates/remote_server/Cargo.toml @@ -65,7 +65,6 @@ telemetry_events.workspace = true util.workspace = true watch.workspace = true worktree.workspace = true -thiserror.workspace = true [target.'cfg(not(windows))'.dependencies] crashes.workspace = true diff --git a/crates/remote_server/src/main.rs b/crates/remote_server/src/main.rs index 368c7cb639..03b0c3eda3 100644 --- a/crates/remote_server/src/main.rs +++ b/crates/remote_server/src/main.rs @@ -1,7 +1,6 @@ #![cfg_attr(target_os = "windows", allow(unused, dead_code))] -use clap::Parser; -use remote_server::Commands; +use clap::{Parser, Subcommand}; use std::path::PathBuf; #[derive(Parser)] @@ -22,34 +21,105 @@ struct Cli { printenv: bool, } +#[derive(Subcommand)] +enum Commands { + Run { + #[arg(long)] + log_file: PathBuf, + #[arg(long)] + pid_file: PathBuf, + #[arg(long)] + stdin_socket: PathBuf, + #[arg(long)] + stdout_socket: PathBuf, + #[arg(long)] + stderr_socket: PathBuf, + }, + Proxy { + #[arg(long)] + reconnect: bool, + #[arg(long)] + identifier: String, + }, + Version, +} + #[cfg(windows)] fn main() { unimplemented!() } #[cfg(not(windows))] -fn main() -> anyhow::Result<()> { +fn main() { + use release_channel::{RELEASE_CHANNEL, ReleaseChannel}; + use remote::proxy::ProxyLaunchError; + use remote_server::unix::{execute_proxy, execute_run}; + let cli = Cli::parse(); if let Some(socket_path) = &cli.askpass { askpass::main(socket_path); - return Ok(()); + return; } if let Some(socket) = &cli.crash_handler { crashes::crash_server(socket.as_path()); - return Ok(()); + return; } if cli.printenv { util::shell_env::print_env(); - return Ok(()); + return; } - if let Some(command) = cli.command { - remote_server::run(command) - } else { - eprintln!("usage: remote "); + let result = match cli.command { + Some(Commands::Run { + log_file, + pid_file, + stdin_socket, + stdout_socket, + stderr_socket, + }) => execute_run( + log_file, + pid_file, + stdin_socket, + stdout_socket, + stderr_socket, + ), + Some(Commands::Proxy { + identifier, + reconnect, + }) => match execute_proxy(identifier, reconnect) { + Ok(_) => Ok(()), + Err(err) => { + if let Some(err) = err.downcast_ref::() { + std::process::exit(err.to_exit_code()); + } + Err(err) + } + }, + Some(Commands::Version) => { + let release_channel = *RELEASE_CHANNEL; + match release_channel { + ReleaseChannel::Stable | ReleaseChannel::Preview => { + println!("{}", env!("ZED_PKG_VERSION")) + } + ReleaseChannel::Nightly | ReleaseChannel::Dev => { + println!( + "{}", + option_env!("ZED_COMMIT_SHA").unwrap_or(release_channel.dev_name()) + ) + } + }; + std::process::exit(0); + } + None => { + eprintln!("usage: remote "); + std::process::exit(1); + } + }; + if let Err(error) = result { + log::error!("exiting due to error: {}", error); std::process::exit(1); } } diff --git a/crates/remote_server/src/remote_server.rs b/crates/remote_server/src/remote_server.rs index c14a4828ac..52003969af 100644 --- a/crates/remote_server/src/remote_server.rs +++ b/crates/remote_server/src/remote_server.rs @@ -6,78 +6,4 @@ pub mod unix; #[cfg(test)] mod remote_editing_tests; -use clap::Subcommand; -use std::path::PathBuf; - pub use headless_project::{HeadlessAppState, HeadlessProject}; - -#[derive(Subcommand)] -pub enum Commands { - Run { - #[arg(long)] - log_file: PathBuf, - #[arg(long)] - pid_file: PathBuf, - #[arg(long)] - stdin_socket: PathBuf, - #[arg(long)] - stdout_socket: PathBuf, - #[arg(long)] - stderr_socket: PathBuf, - }, - Proxy { - #[arg(long)] - reconnect: bool, - #[arg(long)] - identifier: String, - }, - Version, -} - -#[cfg(not(windows))] -pub fn run(command: Commands) -> anyhow::Result<()> { - use anyhow::Context; - use release_channel::{RELEASE_CHANNEL, ReleaseChannel}; - use unix::{ExecuteProxyError, execute_proxy, execute_run}; - - match command { - Commands::Run { - log_file, - pid_file, - stdin_socket, - stdout_socket, - stderr_socket, - } => execute_run( - log_file, - pid_file, - stdin_socket, - stdout_socket, - stderr_socket, - ), - Commands::Proxy { - identifier, - reconnect, - } => execute_proxy(identifier, reconnect) - .inspect_err(|err| { - if let ExecuteProxyError::ServerNotRunning(err) = err { - std::process::exit(err.to_exit_code()); - } - }) - .context("running proxy on the remote server"), - Commands::Version => { - let release_channel = *RELEASE_CHANNEL; - match release_channel { - ReleaseChannel::Stable | ReleaseChannel::Preview => { - println!("{}", env!("ZED_PKG_VERSION")) - } - ReleaseChannel::Nightly | ReleaseChannel::Dev => { - println!( - "{}", - option_env!("ZED_COMMIT_SHA").unwrap_or(release_channel.dev_name()) - ) - } - }; - Ok(()) - } - } -} diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs index c6d1566d60..b8a7351552 100644 --- a/crates/remote_server/src/unix.rs +++ b/crates/remote_server/src/unix.rs @@ -36,7 +36,6 @@ use smol::Async; use smol::{net::unix::UnixListener, stream::StreamExt as _}; use std::ffi::OsStr; use std::ops::ControlFlow; -use std::process::ExitStatus; use std::str::FromStr; use std::sync::LazyLock; use std::{env, thread}; @@ -47,7 +46,6 @@ use std::{ sync::Arc, }; use telemetry_events::LocationData; -use thiserror::Error; use util::ResultExt; pub static VERSION: LazyLock<&str> = LazyLock::new(|| match *RELEASE_CHANNEL { @@ -528,23 +526,7 @@ pub fn execute_run( Ok(()) } -#[derive(Debug, Error)] -pub(crate) enum ServerPathError { - #[error("Failed to create server_dir `{path}`")] - CreateServerDir { - #[source] - source: std::io::Error, - path: PathBuf, - }, - #[error("Failed to create logs_dir `{path}`")] - CreateLogsDir { - #[source] - source: std::io::Error, - path: PathBuf, - }, -} - -#[derive(Clone, Debug)] +#[derive(Clone)] struct ServerPaths { log_file: PathBuf, pid_file: PathBuf, @@ -554,19 +536,10 @@ struct ServerPaths { } impl ServerPaths { - fn new(identifier: &str) -> Result { + fn new(identifier: &str) -> Result { let server_dir = paths::remote_server_state_dir().join(identifier); - std::fs::create_dir_all(&server_dir).map_err(|source| { - ServerPathError::CreateServerDir { - source, - path: server_dir.clone(), - } - })?; - let log_dir = logs_dir(); - std::fs::create_dir_all(log_dir).map_err(|source| ServerPathError::CreateLogsDir { - source: source, - path: log_dir.clone(), - })?; + std::fs::create_dir_all(&server_dir)?; + std::fs::create_dir_all(&logs_dir())?; let pid_file = server_dir.join("server.pid"); let stdin_socket = server_dir.join("stdin.sock"); @@ -584,43 +557,7 @@ impl ServerPaths { } } -#[derive(Debug, Error)] -pub(crate) enum ExecuteProxyError { - #[error("Failed to init server paths")] - ServerPath(#[from] ServerPathError), - - #[error(transparent)] - ServerNotRunning(#[from] ProxyLaunchError), - - #[error("Failed to check PidFile '{path}'")] - CheckPidFile { - #[source] - source: CheckPidError, - path: PathBuf, - }, - - #[error("Failed to kill existing server with pid '{pid}'")] - KillRunningServer { - #[source] - source: std::io::Error, - pid: u32, - }, - - #[error("failed to spawn server")] - SpawnServer(#[source] SpawnServerError), - - #[error("stdin_task failed")] - StdinTask(#[source] anyhow::Error), - #[error("stdout_task failed")] - StdoutTask(#[source] anyhow::Error), - #[error("stderr_task failed")] - StderrTask(#[source] anyhow::Error), -} - -pub(crate) fn execute_proxy( - identifier: String, - is_reconnecting: bool, -) -> Result<(), ExecuteProxyError> { +pub fn execute_proxy(identifier: String, is_reconnecting: bool) -> Result<()> { init_logging_proxy(); let server_paths = ServerPaths::new(&identifier)?; @@ -637,19 +574,12 @@ pub(crate) fn execute_proxy( log::info!("starting proxy process. PID: {}", std::process::id()); - let server_pid = check_pid_file(&server_paths.pid_file).map_err(|source| { - ExecuteProxyError::CheckPidFile { - source, - path: server_paths.pid_file.clone(), - } - })?; + let server_pid = check_pid_file(&server_paths.pid_file)?; let server_running = server_pid.is_some(); if is_reconnecting { if !server_running { log::error!("attempted to reconnect, but no server running"); - return Err(ExecuteProxyError::ServerNotRunning( - ProxyLaunchError::ServerNotRunning, - )); + anyhow::bail!(ProxyLaunchError::ServerNotRunning); } } else { if let Some(pid) = server_pid { @@ -660,7 +590,7 @@ pub(crate) fn execute_proxy( kill_running_server(pid, &server_paths)?; } - spawn_server(&server_paths).map_err(ExecuteProxyError::SpawnServer)?; + spawn_server(&server_paths)?; }; let stdin_task = smol::spawn(async move { @@ -700,9 +630,9 @@ pub(crate) fn execute_proxy( if let Err(forwarding_result) = smol::block_on(async move { futures::select! { - result = stdin_task.fuse() => result.map_err(ExecuteProxyError::StdinTask), - result = stdout_task.fuse() => result.map_err(ExecuteProxyError::StdoutTask), - result = stderr_task.fuse() => result.map_err(ExecuteProxyError::StderrTask), + result = stdin_task.fuse() => result.context("stdin_task failed"), + result = stdout_task.fuse() => result.context("stdout_task failed"), + result = stderr_task.fuse() => result.context("stderr_task failed"), } }) { log::error!( @@ -715,12 +645,12 @@ pub(crate) fn execute_proxy( Ok(()) } -fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<(), ExecuteProxyError> { +fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<()> { log::info!("killing existing server with PID {}", pid); std::process::Command::new("kill") .arg(pid.to_string()) .output() - .map_err(|source| ExecuteProxyError::KillRunningServer { source, pid })?; + .context("failed to kill existing server")?; for file in [ &paths.pid_file, @@ -734,39 +664,18 @@ fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<(), ExecuteProxy Ok(()) } -#[derive(Debug, Error)] -pub(crate) enum SpawnServerError { - #[error("failed to remove stdin socket")] - RemoveStdinSocket(#[source] std::io::Error), - - #[error("failed to remove stdout socket")] - RemoveStdoutSocket(#[source] std::io::Error), - - #[error("failed to remove stderr socket")] - RemoveStderrSocket(#[source] std::io::Error), - - #[error("failed to get current_exe")] - CurrentExe(#[source] std::io::Error), - - #[error("failed to launch server process")] - ProcessStatus(#[source] std::io::Error), - - #[error("failed to launch and detach server process: {status}\n{paths}")] - LaunchStatus { status: ExitStatus, paths: String }, -} - -fn spawn_server(paths: &ServerPaths) -> Result<(), SpawnServerError> { +fn spawn_server(paths: &ServerPaths) -> Result<()> { if paths.stdin_socket.exists() { - std::fs::remove_file(&paths.stdin_socket).map_err(SpawnServerError::RemoveStdinSocket)?; + std::fs::remove_file(&paths.stdin_socket)?; } if paths.stdout_socket.exists() { - std::fs::remove_file(&paths.stdout_socket).map_err(SpawnServerError::RemoveStdoutSocket)?; + std::fs::remove_file(&paths.stdout_socket)?; } if paths.stderr_socket.exists() { - std::fs::remove_file(&paths.stderr_socket).map_err(SpawnServerError::RemoveStderrSocket)?; + std::fs::remove_file(&paths.stderr_socket)?; } - let binary_name = std::env::current_exe().map_err(SpawnServerError::CurrentExe)?; + let binary_name = std::env::current_exe()?; let mut server_process = std::process::Command::new(binary_name); server_process .arg("run") @@ -783,17 +692,11 @@ fn spawn_server(paths: &ServerPaths) -> Result<(), SpawnServerError> { let status = server_process .status() - .map_err(SpawnServerError::ProcessStatus)?; - - if !status.success() { - return Err(SpawnServerError::LaunchStatus { - status, - paths: format!( - "log file: {:?}, pid file: {:?}", - paths.log_file, paths.pid_file, - ), - }); - } + .context("failed to launch server process")?; + anyhow::ensure!( + status.success(), + "failed to launch and detach server process" + ); let mut total_time_waited = std::time::Duration::from_secs(0); let wait_duration = std::time::Duration::from_millis(20); @@ -814,15 +717,7 @@ fn spawn_server(paths: &ServerPaths) -> Result<(), SpawnServerError> { Ok(()) } -#[derive(Debug, Error)] -#[error("Failed to remove PID file for missing process (pid `{pid}`")] -pub(crate) struct CheckPidError { - #[source] - source: std::io::Error, - pid: u32, -} - -fn check_pid_file(path: &Path) -> Result, CheckPidError> { +fn check_pid_file(path: &Path) -> Result> { let Some(pid) = std::fs::read_to_string(&path) .ok() .and_then(|contents| contents.parse::().ok()) @@ -847,7 +742,7 @@ fn check_pid_file(path: &Path) -> Result, CheckPidError> { log::debug!( "Found PID file, but process with that PID does not exist. Removing PID file." ); - std::fs::remove_file(&path).map_err(|source| CheckPidError { source, pid })?; + std::fs::remove_file(&path).context("Failed to remove PID file")?; Ok(None) } } diff --git a/crates/rpc/src/proto_client.rs b/crates/rpc/src/proto_client.rs index a90797ff5d..05b6bd1439 100644 --- a/crates/rpc/src/proto_client.rs +++ b/crates/rpc/src/proto_client.rs @@ -1,48 +1,35 @@ -use anyhow::{Context, Result}; +use anyhow::Context; use collections::HashMap; use futures::{ Future, FutureExt as _, - channel::oneshot, future::{BoxFuture, LocalBoxFuture}, }; -use gpui::{AnyEntity, AnyWeakEntity, AsyncApp, BackgroundExecutor, Entity, FutureExt as _}; -use parking_lot::Mutex; +use gpui::{AnyEntity, AnyWeakEntity, AsyncApp, Entity}; use proto::{ - AnyTypedEnvelope, EntityMessage, Envelope, EnvelopedMessage, LspRequestId, LspRequestMessage, - RequestMessage, TypedEnvelope, error::ErrorExt as _, + AnyTypedEnvelope, EntityMessage, Envelope, EnvelopedMessage, RequestMessage, TypedEnvelope, + error::ErrorExt as _, }; use std::{ any::{Any, TypeId}, - sync::{ - Arc, OnceLock, - atomic::{self, AtomicU64}, - }, - time::Duration, + sync::{Arc, Weak}, }; #[derive(Clone)] -pub struct AnyProtoClient(Arc); +pub struct AnyProtoClient(Arc); -type RequestIds = Arc< - Mutex< - HashMap< - LspRequestId, - oneshot::Sender< - Result< - Option>>>>, - >, - >, - >, - >, ->; +impl AnyProtoClient { + pub fn downgrade(&self) -> AnyWeakProtoClient { + AnyWeakProtoClient(Arc::downgrade(&self.0)) + } +} -static NEXT_LSP_REQUEST_ID: OnceLock> = OnceLock::new(); -static REQUEST_IDS: OnceLock = OnceLock::new(); +#[derive(Clone)] +pub struct AnyWeakProtoClient(Weak); -struct State { - client: Arc, - next_lsp_request_id: Arc, - request_ids: RequestIds, +impl AnyWeakProtoClient { + pub fn upgrade(&self) -> Option { + self.0.upgrade().map(AnyProtoClient) + } } pub trait ProtoClient: Send + Sync { @@ -50,11 +37,11 @@ pub trait ProtoClient: Send + Sync { &self, envelope: Envelope, request_type: &'static str, - ) -> BoxFuture<'static, Result>; + ) -> BoxFuture<'static, anyhow::Result>; - fn send(&self, envelope: Envelope, message_type: &'static str) -> Result<()>; + fn send(&self, envelope: Envelope, message_type: &'static str) -> anyhow::Result<()>; - fn send_response(&self, envelope: Envelope, message_type: &'static str) -> Result<()>; + fn send_response(&self, envelope: Envelope, message_type: &'static str) -> anyhow::Result<()>; fn message_handler_set(&self) -> &parking_lot::Mutex; @@ -78,7 +65,7 @@ pub type ProtoMessageHandler = Arc< Box, AnyProtoClient, AsyncApp, - ) -> LocalBoxFuture<'static, Result<()>>, + ) -> LocalBoxFuture<'static, anyhow::Result<()>>, >; impl ProtoMessageHandlerSet { @@ -126,7 +113,7 @@ impl ProtoMessageHandlerSet { message: Box, client: AnyProtoClient, cx: AsyncApp, - ) -> Option>> { + ) -> Option>> { let payload_type_id = message.payload_type_id(); let mut this = this.lock(); let handler = this.message_handlers.get(&payload_type_id)?.clone(); @@ -182,195 +169,43 @@ where T: ProtoClient + 'static, { fn from(client: Arc) -> Self { - Self::new(client) + Self(client) } } impl AnyProtoClient { pub fn new(client: Arc) -> Self { - Self(Arc::new(State { - client, - next_lsp_request_id: NEXT_LSP_REQUEST_ID - .get_or_init(|| Arc::new(AtomicU64::new(0))) - .clone(), - request_ids: REQUEST_IDS.get_or_init(RequestIds::default).clone(), - })) + Self(client) } pub fn is_via_collab(&self) -> bool { - self.0.client.is_via_collab() + self.0.is_via_collab() } pub fn request( &self, request: T, - ) -> impl Future> + use { + ) -> impl Future> + use { let envelope = request.into_envelope(0, None, None); - let response = self.0.client.request(envelope, T::NAME); + let response = self.0.request(envelope, T::NAME); async move { T::Response::from_envelope(response.await?) .context("received response of the wrong type") } } - pub fn send(&self, request: T) -> Result<()> { + pub fn send(&self, request: T) -> anyhow::Result<()> { let envelope = request.into_envelope(0, None, None); - self.0.client.send(envelope, T::NAME) + self.0.send(envelope, T::NAME) } - pub fn send_response(&self, request_id: u32, request: T) -> Result<()> { - let envelope = request.into_envelope(0, Some(request_id), None); - self.0.client.send(envelope, T::NAME) - } - - pub fn request_lsp( + pub fn send_response( &self, - project_id: u64, - timeout: Duration, - executor: BackgroundExecutor, + request_id: u32, request: T, - ) -> impl Future< - Output = Result>>>>, - > + use - where - T: LspRequestMessage, - { - let new_id = LspRequestId( - self.0 - .next_lsp_request_id - .fetch_add(1, atomic::Ordering::Acquire), - ); - let (tx, rx) = oneshot::channel(); - { - self.0.request_ids.lock().insert(new_id, tx); - } - - let query = proto::LspQuery { - project_id, - lsp_request_id: new_id.0, - request: Some(request.to_proto_query()), - }; - let request = self.request(query); - let request_ids = self.0.request_ids.clone(); - async move { - match request.await { - Ok(_request_enqueued) => {} - Err(e) => { - request_ids.lock().remove(&new_id); - return Err(e).context("sending LSP proto request"); - } - } - - let response = rx.with_timeout(timeout, &executor).await; - { - request_ids.lock().remove(&new_id); - } - match response { - Ok(Ok(response)) => { - let response = response - .context("waiting for LSP proto response")? - .map(|response| { - anyhow::Ok(TypedEnvelope { - payload: response - .payload - .into_iter() - .map(|lsp_response| lsp_response.into_response::()) - .collect::>>()?, - sender_id: response.sender_id, - original_sender_id: response.original_sender_id, - message_id: response.message_id, - received_at: response.received_at, - }) - }) - .transpose() - .context("converting LSP proto response")?; - Ok(response) - } - Err(_cancelled_due_timeout) => Ok(None), - Ok(Err(_channel_dropped)) => Ok(None), - } - } - } - - pub fn send_lsp_response( - &self, - project_id: u64, - lsp_request_id: LspRequestId, - server_responses: HashMap, - ) -> Result<()> { - self.send(proto::LspQueryResponse { - project_id, - lsp_request_id: lsp_request_id.0, - responses: server_responses - .into_iter() - .map(|(server_id, response)| proto::LspResponse { - server_id, - response: Some(T::response_to_proto_query(response)), - }) - .collect(), - }) - } - - pub fn handle_lsp_response(&self, mut envelope: TypedEnvelope) { - let request_id = LspRequestId(envelope.payload.lsp_request_id); - let mut response_senders = self.0.request_ids.lock(); - if let Some(tx) = response_senders.remove(&request_id) { - let responses = envelope.payload.responses.drain(..).collect::>(); - tx.send(Ok(Some(proto::TypedEnvelope { - sender_id: envelope.sender_id, - original_sender_id: envelope.original_sender_id, - message_id: envelope.message_id, - received_at: envelope.received_at, - payload: responses - .into_iter() - .filter_map(|response| { - use proto::lsp_response::Response; - - let server_id = response.server_id; - let response = match response.response? { - Response::GetReferencesResponse(response) => { - to_any_envelope(&envelope, response) - } - Response::GetDocumentColorResponse(response) => { - to_any_envelope(&envelope, response) - } - Response::GetHoverResponse(response) => { - to_any_envelope(&envelope, response) - } - Response::GetCodeActionsResponse(response) => { - to_any_envelope(&envelope, response) - } - Response::GetSignatureHelpResponse(response) => { - to_any_envelope(&envelope, response) - } - Response::GetCodeLensResponse(response) => { - to_any_envelope(&envelope, response) - } - Response::GetDocumentDiagnosticsResponse(response) => { - to_any_envelope(&envelope, response) - } - Response::GetDefinitionResponse(response) => { - to_any_envelope(&envelope, response) - } - Response::GetDeclarationResponse(response) => { - to_any_envelope(&envelope, response) - } - Response::GetTypeDefinitionResponse(response) => { - to_any_envelope(&envelope, response) - } - Response::GetImplementationResponse(response) => { - to_any_envelope(&envelope, response) - } - }; - Some(proto::ProtoLspResponse { - server_id, - response, - }) - }) - .collect(), - }))) - .ok(); - } + ) -> anyhow::Result<()> { + let envelope = request.into_envelope(0, Some(request_id), None); + self.0.send(envelope, T::NAME) } pub fn add_request_handler(&self, entity: gpui::WeakEntity, handler: H) @@ -378,35 +213,31 @@ impl AnyProtoClient { M: RequestMessage, E: 'static, H: 'static + Sync + Fn(Entity, TypedEnvelope, AsyncApp) -> F + Send + Sync, - F: 'static + Future>, + F: 'static + Future>, { - self.0 - .client - .message_handler_set() - .lock() - .add_message_handler( - TypeId::of::(), - entity.into(), - Arc::new(move |entity, envelope, client, cx| { - let entity = entity.downcast::().unwrap(); - let envelope = envelope.into_any().downcast::>().unwrap(); - let request_id = envelope.message_id(); - handler(entity, *envelope, cx) - .then(move |result| async move { - match result { - Ok(response) => { - client.send_response(request_id, response)?; - Ok(()) - } - Err(error) => { - client.send_response(request_id, error.to_proto())?; - Err(error) - } + self.0.message_handler_set().lock().add_message_handler( + TypeId::of::(), + entity.into(), + Arc::new(move |entity, envelope, client, cx| { + let entity = entity.downcast::().unwrap(); + let envelope = envelope.into_any().downcast::>().unwrap(); + let request_id = envelope.message_id(); + handler(entity, *envelope, cx) + .then(move |result| async move { + match result { + Ok(response) => { + client.send_response(request_id, response)?; + Ok(()) } - }) - .boxed_local() - }), - ) + Err(error) => { + client.send_response(request_id, error.to_proto())?; + Err(error) + } + } + }) + .boxed_local() + }), + ) } pub fn add_entity_request_handler(&self, handler: H) @@ -414,7 +245,7 @@ impl AnyProtoClient { M: EnvelopedMessage + RequestMessage + EntityMessage, E: 'static, H: 'static + Sync + Send + Fn(gpui::Entity, TypedEnvelope, AsyncApp) -> F, - F: 'static + Future>, + F: 'static + Future>, { let message_type_id = TypeId::of::(); let entity_type_id = TypeId::of::(); @@ -426,7 +257,6 @@ impl AnyProtoClient { .remote_entity_id() }; self.0 - .client .message_handler_set() .lock() .add_entity_message_handler( @@ -460,7 +290,7 @@ impl AnyProtoClient { M: EnvelopedMessage + EntityMessage, E: 'static, H: 'static + Sync + Send + Fn(gpui::Entity, TypedEnvelope, AsyncApp) -> F, - F: 'static + Future>, + F: 'static + Future>, { let message_type_id = TypeId::of::(); let entity_type_id = TypeId::of::(); @@ -472,7 +302,6 @@ impl AnyProtoClient { .remote_entity_id() }; self.0 - .client .message_handler_set() .lock() .add_entity_message_handler( @@ -490,7 +319,7 @@ impl AnyProtoClient { pub fn subscribe_to_entity(&self, remote_id: u64, entity: &Entity) { let id = (TypeId::of::(), remote_id); - let mut message_handlers = self.0.client.message_handler_set().lock(); + let mut message_handlers = self.0.message_handler_set().lock(); if message_handlers .entities_by_type_and_remote_id .contains_key(&id) @@ -506,16 +335,3 @@ impl AnyProtoClient { ); } } - -fn to_any_envelope( - envelope: &TypedEnvelope, - response: T, -) -> Box { - Box::new(proto::TypedEnvelope { - sender_id: envelope.sender_id, - original_sender_id: envelope.original_sender_id, - message_id: envelope.message_id, - received_at: envelope.received_at, - payload: response, - }) as Box<_> -} diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 8ac12588af..c4ba9b5154 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -3905,7 +3905,7 @@ pub mod tests { assert_eq!(workspace.active_pane(), &second_pane); second_pane.update(cx, |this, cx| { assert_eq!(this.active_item_index(), 1); - this.activate_previous_item(&Default::default(), window, cx); + this.activate_prev_item(false, window, cx); assert_eq!(this.active_item_index(), 0); }); workspace.activate_pane_in_direction(workspace::SplitDirection::Left, window, cx); @@ -3940,9 +3940,7 @@ pub mod tests { // Focus the second pane's non-search item window .update(cx, |_workspace, window, cx| { - second_pane.update(cx, |pane, cx| { - pane.activate_next_item(&Default::default(), window, cx) - }); + second_pane.update(cx, |pane, cx| pane.activate_next_item(true, window, cx)); }) .unwrap(); diff --git a/crates/settings/src/key_equivalents.rs b/crates/settings/src/key_equivalents.rs new file mode 100644 index 0000000000..6580137535 --- /dev/null +++ b/crates/settings/src/key_equivalents.rs @@ -0,0 +1,1424 @@ +use collections::HashMap; + +// On some keyboards (e.g. German QWERTZ) it is not possible to type the full ASCII range +// without using option. This means that some of our built in keyboard shortcuts do not work +// for those users. +// +// The way macOS solves this problem is to move shortcuts around so that they are all reachable, +// even if the mnemonic changes. https://developer.apple.com/documentation/swiftui/keyboardshortcut/localization-swift.struct +// +// For example, cmd-> is the "switch window" shortcut because the > key is right above tab. +// To ensure this doesn't cause problems for shortcuts defined for a QWERTY layout, apple moves +// any shortcuts defined as cmd-> to cmd-:. Coincidentally this s also the same keyboard position +// as cmd-> on a QWERTY layout. +// +// Another example is cmd-[ and cmd-], as they cannot be typed without option, those keys are remapped to cmd-ö +// and cmd-ä. These shortcuts are not in the same position as a QWERTY keyboard, because on a QWERTZ keyboard +// the + key is in the way; and shortcuts bound to cmd-+ are still typed as cmd-+ on either keyboard (though the +// specific key moves) +// +// As far as I can tell, there's no way to query the mappings Apple uses except by rendering a menu with every +// possible key combination, and inspecting the UI to see what it rendered. So that's what we did... +// +// These mappings were generated by running https://github.com/ConradIrwin/keyboard-inspector, tidying up the +// output to remove languages with no mappings and other oddities, and converting it to a less verbose representation with: +// jq -s 'map(to_entries | map({key: .key, value: [(.value | to_entries | map(.key) | join("")), (.value | to_entries | map(.value) | join(""))]}) | from_entries) | add' +// From there I used multi-cursor to produce this match statement. +#[cfg(target_os = "macos")] +pub fn get_key_equivalents(layout: &str) -> Option> { + let mappings: &[(char, char)] = match layout { + "com.apple.keylayout.ABC-AZERTY" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.ABC-QWERTZ" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Albanian" => &[ + ('"', '\''), + (':', 'Ç'), + (';', 'ç'), + ('<', ';'), + ('>', ':'), + ('@', '"'), + ('\'', '@'), + ('\\', 'ë'), + ('`', '<'), + ('|', 'Ë'), + ('~', '>'), + ], + "com.apple.keylayout.Austrian" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Azeri" => &[ + ('"', 'Ə'), + (',', 'ç'), + ('.', 'ş'), + ('/', '.'), + (':', 'I'), + (';', 'ı'), + ('<', 'Ç'), + ('>', 'Ş'), + ('?', ','), + ('W', 'Ü'), + ('[', 'ö'), + ('\'', 'ə'), + (']', 'ğ'), + ('w', 'ü'), + ('{', 'Ö'), + ('|', '/'), + ('}', 'Ğ'), + ], + "com.apple.keylayout.Belgian" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.Brazilian-ABNT2" => &[ + ('"', '`'), + ('/', 'ç'), + ('?', 'Ç'), + ('\'', '´'), + ('\\', '~'), + ('^', '¨'), + ('`', '\''), + ('|', '^'), + ('~', '"'), + ], + "com.apple.keylayout.Brazilian-Pro" => &[('^', 'ˆ'), ('~', '˜')], + "com.apple.keylayout.British" => &[('#', '£')], + "com.apple.keylayout.Canadian-CSA" => &[ + ('"', 'È'), + ('/', 'é'), + ('<', '\''), + ('>', '"'), + ('?', 'É'), + ('[', '^'), + ('\'', 'è'), + ('\\', 'à'), + (']', 'ç'), + ('`', 'ù'), + ('{', '¨'), + ('|', 'À'), + ('}', 'Ç'), + ('~', 'Ù'), + ], + "com.apple.keylayout.Croatian" => &[ + ('"', 'Ć'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Croatian-PC" => &[ + ('"', 'Ć'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Czech" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ě'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ř'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ů'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', ')'), + ('^', '6'), + ('`', '¨'), + ('{', 'Ú'), + ('}', '('), + ('~', '`'), + ], + "com.apple.keylayout.Czech-QWERTY" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ě'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ř'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ů'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', ')'), + ('^', '6'), + ('`', '¨'), + ('{', 'Ú'), + ('}', '('), + ('~', '`'), + ], + "com.apple.keylayout.Danish" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'æ'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ø'), + ('^', '&'), + ('`', '<'), + ('{', 'Æ'), + ('|', '*'), + ('}', 'Ø'), + ('~', '>'), + ], + "com.apple.keylayout.Faroese" => &[ + ('"', 'Ø'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Æ'), + (';', 'æ'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'å'), + ('\'', 'ø'), + ('\\', '\''), + (']', 'ð'), + ('^', '&'), + ('`', '<'), + ('{', 'Å'), + ('|', '*'), + ('}', 'Ð'), + ('~', '>'), + ], + "com.apple.keylayout.Finnish" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.FinnishExtended" => &[ + ('"', 'ˆ'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.FinnishSami-PC" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '@'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.French" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.French-PC" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('-', ')'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '-'), + ('7', 'è'), + ('8', '_'), + ('9', 'ç'), + (':', '§'), + (';', '!'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '*'), + (']', '$'), + ('^', '6'), + ('_', '°'), + ('`', '<'), + ('{', '¨'), + ('|', 'μ'), + ('}', '£'), + ('~', '>'), + ], + "com.apple.keylayout.French-numerical" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('.', ';'), + ('/', ':'), + ('0', 'à'), + ('1', '&'), + ('2', 'é'), + ('3', '"'), + ('4', '\''), + ('5', '('), + ('6', '§'), + ('7', 'è'), + ('8', '!'), + ('9', 'ç'), + (':', '°'), + (';', ')'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', '^'), + ('\'', 'ù'), + ('\\', '`'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '¨'), + ('|', '£'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.German" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.German-DIN-2137" => &[ + ('"', '`'), + ('#', '§'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', 'ß'), + (':', 'Ü'), + (';', 'ü'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '´'), + ('\\', '#'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '\''), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Hawaiian" => &[('\'', 'ʻ')], + "com.apple.keylayout.Hungarian" => &[ + ('!', '\''), + ('"', 'Á'), + ('#', '+'), + ('$', '!'), + ('&', '='), + ('(', ')'), + (')', 'Ö'), + ('*', '('), + ('+', 'Ó'), + ('/', 'ü'), + ('0', 'ö'), + (':', 'É'), + (';', 'é'), + ('<', 'Ü'), + ('=', 'ó'), + ('>', ':'), + ('@', '"'), + ('[', 'ő'), + ('\'', 'á'), + ('\\', 'ű'), + (']', 'ú'), + ('^', '/'), + ('`', 'í'), + ('{', 'Ő'), + ('|', 'Ű'), + ('}', 'Ú'), + ('~', 'Í'), + ], + "com.apple.keylayout.Hungarian-QWERTY" => &[ + ('!', '\''), + ('"', 'Á'), + ('#', '+'), + ('$', '!'), + ('&', '='), + ('(', ')'), + (')', 'Ö'), + ('*', '('), + ('+', 'Ó'), + ('/', 'ü'), + ('0', 'ö'), + (':', 'É'), + (';', 'é'), + ('<', 'Ü'), + ('=', 'ó'), + ('>', ':'), + ('@', '"'), + ('[', 'ő'), + ('\'', 'á'), + ('\\', 'ű'), + (']', 'ú'), + ('^', '/'), + ('`', 'í'), + ('{', 'Ő'), + ('|', 'Ű'), + ('}', 'Ú'), + ('~', 'Í'), + ], + "com.apple.keylayout.Icelandic" => &[ + ('"', 'Ö'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'Ð'), + (';', 'ð'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'æ'), + ('\'', 'ö'), + ('\\', 'þ'), + (']', '´'), + ('^', '&'), + ('`', '<'), + ('{', 'Æ'), + ('|', 'Þ'), + ('}', '´'), + ('~', '>'), + ], + "com.apple.keylayout.Irish" => &[('#', '£')], + "com.apple.keylayout.IrishExtended" => &[('#', '£')], + "com.apple.keylayout.Italian" => &[ + ('!', '1'), + ('"', '%'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + (',', ';'), + ('.', ':'), + ('/', ','), + ('0', 'é'), + ('1', '&'), + ('2', '"'), + ('3', '\''), + ('4', '('), + ('5', 'ç'), + ('6', 'è'), + ('7', ')'), + ('8', '£'), + ('9', 'à'), + (':', '!'), + (';', 'ò'), + ('<', '.'), + ('>', '/'), + ('@', '2'), + ('[', 'ì'), + ('\'', 'ù'), + ('\\', '§'), + (']', '$'), + ('^', '6'), + ('`', '<'), + ('{', '^'), + ('|', '°'), + ('}', '*'), + ('~', '>'), + ], + "com.apple.keylayout.Italian-Pro" => &[ + ('"', '^'), + ('#', '£'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'é'), + (';', 'è'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ò'), + ('\'', 'ì'), + ('\\', 'ù'), + (']', 'à'), + ('^', '&'), + ('`', '<'), + ('{', 'ç'), + ('|', '§'), + ('}', '°'), + ('~', '>'), + ], + "com.apple.keylayout.LatinAmerican" => &[ + ('"', '¨'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'Ñ'), + (';', 'ñ'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', '{'), + ('\'', '´'), + ('\\', '¿'), + (']', '}'), + ('^', '&'), + ('`', '<'), + ('{', '['), + ('|', '¡'), + ('}', ']'), + ('~', '>'), + ], + "com.apple.keylayout.Lithuanian" => &[ + ('!', 'Ą'), + ('#', 'Ę'), + ('$', 'Ė'), + ('%', 'Į'), + ('&', 'Ų'), + ('*', 'Ū'), + ('+', 'Ž'), + ('1', 'ą'), + ('2', 'č'), + ('3', 'ę'), + ('4', 'ė'), + ('5', 'į'), + ('6', 'š'), + ('7', 'ų'), + ('8', 'ū'), + ('=', 'ž'), + ('@', 'Č'), + ('^', 'Š'), + ], + "com.apple.keylayout.Maltese" => &[ + ('#', '£'), + ('[', 'ġ'), + (']', 'ħ'), + ('`', 'ż'), + ('{', 'Ġ'), + ('}', 'Ħ'), + ('~', 'Ż'), + ], + "com.apple.keylayout.NorthernSami" => &[ + ('"', 'Ŋ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('Q', 'Á'), + ('W', 'Š'), + ('X', 'Č'), + ('[', 'ø'), + ('\'', 'ŋ'), + ('\\', 'đ'), + (']', 'æ'), + ('^', '&'), + ('`', 'ž'), + ('q', 'á'), + ('w', 'š'), + ('x', 'č'), + ('{', 'Ø'), + ('|', 'Đ'), + ('}', 'Æ'), + ('~', 'Ž'), + ], + "com.apple.keylayout.Norwegian" => &[ + ('"', '^'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ø'), + ('\'', '¨'), + ('\\', '@'), + (']', 'æ'), + ('^', '&'), + ('`', '<'), + ('{', 'Ø'), + ('|', '*'), + ('}', 'Æ'), + ('~', '>'), + ], + "com.apple.keylayout.NorwegianExtended" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ø'), + ('\\', '@'), + (']', 'æ'), + ('`', '<'), + ('}', 'Æ'), + ('~', '>'), + ], + "com.apple.keylayout.NorwegianSami-PC" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ø'), + ('\'', '¨'), + ('\\', '@'), + (']', 'æ'), + ('^', '&'), + ('`', '<'), + ('{', 'Ø'), + ('|', '*'), + ('}', 'Æ'), + ('~', '>'), + ], + "com.apple.keylayout.Polish" => &[ + ('!', '§'), + ('"', 'ę'), + ('#', '!'), + ('$', '?'), + ('%', '+'), + ('&', ':'), + ('(', '/'), + (')', '"'), + ('*', '_'), + ('+', ']'), + (',', '.'), + ('.', ','), + ('/', 'ż'), + (':', 'Ł'), + (';', 'ł'), + ('<', 'ś'), + ('=', '['), + ('>', 'ń'), + ('?', 'Ż'), + ('@', '%'), + ('[', 'ó'), + ('\'', 'ą'), + ('\\', ';'), + (']', '('), + ('^', '='), + ('_', 'ć'), + ('`', '<'), + ('{', 'ź'), + ('|', '$'), + ('}', ')'), + ('~', '>'), + ], + "com.apple.keylayout.Portuguese" => &[ + ('"', '`'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '\''), + (':', 'ª'), + (';', 'º'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'ç'), + ('\'', '´'), + (']', '~'), + ('^', '&'), + ('`', '<'), + ('{', 'Ç'), + ('}', '^'), + ('~', '>'), + ], + "com.apple.keylayout.Sami-PC" => &[ + ('"', 'Ŋ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('Q', 'Á'), + ('W', 'Š'), + ('X', 'Č'), + ('[', 'ø'), + ('\'', 'ŋ'), + ('\\', 'đ'), + (']', 'æ'), + ('^', '&'), + ('`', 'ž'), + ('q', 'á'), + ('w', 'š'), + ('x', 'č'), + ('{', 'Ø'), + ('|', 'Đ'), + ('}', 'Æ'), + ('~', 'Ž'), + ], + "com.apple.keylayout.Serbian-Latin" => &[ + ('"', 'Ć'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Slovak" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ľ'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ť'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ô'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', 'ä'), + ('^', '6'), + ('`', 'ň'), + ('{', 'Ú'), + ('}', 'Ä'), + ('~', 'Ň'), + ], + "com.apple.keylayout.Slovak-QWERTY" => &[ + ('!', '1'), + ('"', '!'), + ('#', '3'), + ('$', '4'), + ('%', '5'), + ('&', '7'), + ('(', '9'), + (')', '0'), + ('*', '8'), + ('+', '%'), + ('/', '\''), + ('0', 'é'), + ('1', '+'), + ('2', 'ľ'), + ('3', 'š'), + ('4', 'č'), + ('5', 'ť'), + ('6', 'ž'), + ('7', 'ý'), + ('8', 'á'), + ('9', 'í'), + (':', '"'), + (';', 'ô'), + ('<', '?'), + ('>', ':'), + ('?', 'ˇ'), + ('@', '2'), + ('[', 'ú'), + ('\'', '§'), + (']', 'ä'), + ('^', '6'), + ('`', 'ň'), + ('{', 'Ú'), + ('}', 'Ä'), + ('~', 'Ň'), + ], + "com.apple.keylayout.Slovenian" => &[ + ('"', 'Ć'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (':', 'Č'), + (';', 'č'), + ('<', ';'), + ('=', '*'), + ('>', ':'), + ('@', '"'), + ('[', 'š'), + ('\'', 'ć'), + ('\\', 'ž'), + (']', 'đ'), + ('^', '&'), + ('`', '<'), + ('{', 'Š'), + ('|', 'Ž'), + ('}', 'Đ'), + ('~', '>'), + ], + "com.apple.keylayout.Spanish" => &[ + ('!', '¡'), + ('"', '¨'), + ('.', 'ç'), + ('/', '.'), + (':', 'º'), + (';', '´'), + ('<', '¿'), + ('>', 'Ç'), + ('@', '!'), + ('[', 'ñ'), + ('\'', '`'), + ('\\', '\''), + (']', ';'), + ('^', '/'), + ('`', '<'), + ('{', 'Ñ'), + ('|', '"'), + ('}', ':'), + ('~', '>'), + ], + "com.apple.keylayout.Spanish-ISO" => &[ + ('"', '¨'), + ('#', '·'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('.', 'ç'), + ('/', '.'), + (':', 'º'), + (';', '´'), + ('<', '¿'), + ('>', 'Ç'), + ('@', '"'), + ('[', 'ñ'), + ('\'', '`'), + ('\\', '\''), + (']', ';'), + ('^', '&'), + ('`', '<'), + ('{', 'Ñ'), + ('|', '"'), + ('}', '`'), + ('~', '>'), + ], + "com.apple.keylayout.Swedish" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.Swedish-Pro" => &[ + ('"', '^'), + ('$', '€'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '\''), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.SwedishSami-PC" => &[ + ('"', 'ˆ'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('/', '´'), + (':', 'Å'), + (';', 'å'), + ('<', ';'), + ('=', '`'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '¨'), + ('\\', '@'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'Ö'), + ('|', '*'), + ('}', 'Ä'), + ('~', '>'), + ], + "com.apple.keylayout.SwissFrench" => &[ + ('!', '+'), + ('"', '`'), + ('#', '*'), + ('$', 'ç'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('+', '!'), + ('/', '\''), + (':', 'ü'), + (';', 'è'), + ('<', ';'), + ('=', '¨'), + ('>', ':'), + ('@', '"'), + ('[', 'é'), + ('\'', '^'), + ('\\', '$'), + (']', 'à'), + ('^', '&'), + ('`', '<'), + ('{', 'ö'), + ('|', '£'), + ('}', 'ä'), + ('~', '>'), + ], + "com.apple.keylayout.SwissGerman" => &[ + ('!', '+'), + ('"', '`'), + ('#', '*'), + ('$', 'ç'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('+', '!'), + ('/', '\''), + (':', 'è'), + (';', 'ü'), + ('<', ';'), + ('=', '¨'), + ('>', ':'), + ('@', '"'), + ('[', 'ö'), + ('\'', '^'), + ('\\', '$'), + (']', 'ä'), + ('^', '&'), + ('`', '<'), + ('{', 'é'), + ('|', '£'), + ('}', 'à'), + ('~', '>'), + ], + "com.apple.keylayout.Turkish" => &[ + ('"', '-'), + ('#', '"'), + ('$', '\''), + ('%', '('), + ('&', ')'), + ('(', '%'), + (')', ':'), + ('*', '_'), + (',', 'ö'), + ('-', 'ş'), + ('.', 'ç'), + ('/', '.'), + (':', '$'), + ('<', 'Ö'), + ('>', 'Ç'), + ('@', '*'), + ('[', 'ğ'), + ('\'', ','), + ('\\', 'ü'), + (']', 'ı'), + ('^', '/'), + ('_', 'Ş'), + ('`', '<'), + ('{', 'Ğ'), + ('|', 'Ü'), + ('}', 'I'), + ('~', '>'), + ], + "com.apple.keylayout.Turkish-QWERTY-PC" => &[ + ('"', 'I'), + ('#', '^'), + ('$', '+'), + ('&', '/'), + ('(', ')'), + (')', '='), + ('*', '('), + ('+', ':'), + (',', 'ö'), + ('.', 'ç'), + ('/', '*'), + (':', 'Ş'), + (';', 'ş'), + ('<', 'Ö'), + ('=', '.'), + ('>', 'Ç'), + ('@', '\''), + ('[', 'ğ'), + ('\'', 'ı'), + ('\\', ','), + (']', 'ü'), + ('^', '&'), + ('`', '<'), + ('{', 'Ğ'), + ('|', ';'), + ('}', 'Ü'), + ('~', '>'), + ], + "com.apple.keylayout.Turkish-Standard" => &[ + ('"', 'Ş'), + ('#', '^'), + ('&', '\''), + ('(', ')'), + (')', '='), + ('*', '('), + (',', '.'), + ('.', ','), + (':', 'Ç'), + (';', 'ç'), + ('<', ':'), + ('=', '*'), + ('>', ';'), + ('@', '"'), + ('[', 'ğ'), + ('\'', 'ş'), + ('\\', 'ü'), + (']', 'ı'), + ('^', '&'), + ('`', 'ö'), + ('{', 'Ğ'), + ('|', 'Ü'), + ('}', 'I'), + ('~', 'Ö'), + ], + "com.apple.keylayout.Turkmen" => &[ + ('C', 'Ç'), + ('Q', 'Ä'), + ('V', 'Ý'), + ('X', 'Ü'), + ('[', 'ň'), + ('\\', 'ş'), + (']', 'ö'), + ('^', '№'), + ('`', 'ž'), + ('c', 'ç'), + ('q', 'ä'), + ('v', 'ý'), + ('x', 'ü'), + ('{', 'Ň'), + ('|', 'Ş'), + ('}', 'Ö'), + ('~', 'Ž'), + ], + "com.apple.keylayout.USInternational-PC" => &[('^', 'ˆ'), ('~', '˜')], + "com.apple.keylayout.Welsh" => &[('#', '£')], + + _ => return None, + }; + + Some(HashMap::from_iter(mappings.iter().cloned())) +} + +#[cfg(not(target_os = "macos"))] +pub fn get_key_equivalents(_layout: &str) -> Option> { + None +} diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 0e8303c4c1..ae3f42853a 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -3,8 +3,7 @@ use collections::{BTreeMap, HashMap, IndexMap}; use fs::Fs; use gpui::{ Action, ActionBuildError, App, InvalidKeystrokeError, KEYSTROKE_PARSE_EXPECTED_MESSAGE, - KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, KeybindingKeystroke, Keystroke, - NoAction, SharedString, + KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, Keystroke, NoAction, SharedString, }; use schemars::{JsonSchema, json_schema}; use serde::Deserialize; @@ -212,6 +211,9 @@ impl KeymapFile { } pub fn load(content: &str, cx: &App) -> KeymapFileLoadResult { + let key_equivalents = + crate::key_equivalents::get_key_equivalents(cx.keyboard_layout().id()); + if content.is_empty() { return KeymapFileLoadResult::Success { key_bindings: Vec::new(), @@ -253,6 +255,12 @@ impl KeymapFile { } }; + let key_equivalents = if *use_key_equivalents { + key_equivalents.as_ref() + } else { + None + }; + let mut section_errors = String::new(); if !unrecognized_fields.is_empty() { @@ -270,7 +278,7 @@ impl KeymapFile { keystrokes, action, context_predicate.clone(), - *use_key_equivalents, + key_equivalents, cx, ); match result { @@ -328,7 +336,7 @@ impl KeymapFile { keystrokes: &str, action: &KeymapAction, context: Option>, - use_key_equivalents: bool, + key_equivalents: Option<&HashMap>, cx: &App, ) -> std::result::Result { let (build_result, action_input_string) = match &action.0 { @@ -396,9 +404,8 @@ impl KeymapFile { keystrokes, action, context, - use_key_equivalents, + key_equivalents, action_input_string.map(SharedString::from), - cx.keyboard_mapper().as_ref(), ) { Ok(key_binding) => key_binding, Err(InvalidKeystrokeError { keystroke }) => { @@ -600,7 +607,6 @@ impl KeymapFile { mut operation: KeybindUpdateOperation<'a>, mut keymap_contents: String, tab_size: usize, - keyboard_mapper: &dyn gpui::PlatformKeyboardMapper, ) -> Result { match operation { // if trying to replace a keybinding that is not user-defined, treat it as an add operation @@ -640,7 +646,7 @@ impl KeymapFile { .action_value() .context("Failed to generate target action JSON value")?; let Some((index, keystrokes_str)) = - find_binding(&keymap, &target, &target_action_value, keyboard_mapper) + find_binding(&keymap, &target, &target_action_value) else { anyhow::bail!("Failed to find keybinding to remove"); }; @@ -675,7 +681,7 @@ impl KeymapFile { .context("Failed to generate source action JSON value")?; if let Some((index, keystrokes_str)) = - find_binding(&keymap, &target, &target_action_value, keyboard_mapper) + find_binding(&keymap, &target, &target_action_value) { if target.context == source.context { // if we are only changing the keybinding (common case) @@ -775,7 +781,7 @@ impl KeymapFile { } let use_key_equivalents = from.and_then(|from| { let action_value = from.action_value().context("Failed to serialize action value. `use_key_equivalents` on new keybinding may be incorrect.").log_err()?; - let (index, _) = find_binding(&keymap, &from, &action_value, keyboard_mapper)?; + let (index, _) = find_binding(&keymap, &from, &action_value)?; Some(keymap.0[index].use_key_equivalents) }).unwrap_or(false); if use_key_equivalents { @@ -802,7 +808,6 @@ impl KeymapFile { keymap: &'b KeymapFile, target: &KeybindUpdateTarget<'a>, target_action_value: &Value, - keyboard_mapper: &dyn gpui::PlatformKeyboardMapper, ) -> Option<(usize, &'b str)> { let target_context_parsed = KeyBindingContextPredicate::parse(target.context.unwrap_or("")).ok(); @@ -818,11 +823,8 @@ impl KeymapFile { for (keystrokes_str, action) in bindings { let Ok(keystrokes) = keystrokes_str .split_whitespace() - .map(|source| { - let keystroke = Keystroke::parse(source)?; - Ok(KeybindingKeystroke::new(keystroke, false, keyboard_mapper)) - }) - .collect::, InvalidKeystrokeError>>() + .map(Keystroke::parse) + .collect::, _>>() else { continue; }; @@ -830,7 +832,7 @@ impl KeymapFile { || !keystrokes .iter() .zip(target.keystrokes) - .all(|(a, b)| a.inner.should_match(b)) + .all(|(a, b)| a.should_match(b)) { continue; } @@ -845,7 +847,7 @@ impl KeymapFile { } } -#[derive(Clone, Debug)] +#[derive(Clone)] pub enum KeybindUpdateOperation<'a> { Replace { /// Describes the keybind to create @@ -914,7 +916,7 @@ impl<'a> KeybindUpdateOperation<'a> { #[derive(Debug, Clone)] pub struct KeybindUpdateTarget<'a> { pub context: Option<&'a str>, - pub keystrokes: &'a [KeybindingKeystroke], + pub keystrokes: &'a [Keystroke], pub action_name: &'a str, pub action_arguments: Option<&'a str>, } @@ -939,9 +941,6 @@ impl<'a> KeybindUpdateTarget<'a> { fn keystrokes_unparsed(&self) -> String { let mut keystrokes = String::with_capacity(self.keystrokes.len() * 8); for keystroke in self.keystrokes { - // The reason use `keystroke.unparse()` instead of `keystroke.inner.unparse()` - // here is that, we want the user to use `ctrl-shift-4` instead of `ctrl-$` - // by default on Windows. keystrokes.push_str(&keystroke.unparse()); keystrokes.push(' '); } @@ -960,7 +959,7 @@ impl<'a> KeybindUpdateTarget<'a> { } } -#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Debug)] +#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)] pub enum KeybindSource { User, Vim, @@ -1021,7 +1020,7 @@ impl From for KeyBindingMetaIndex { #[cfg(test)] mod tests { - use gpui::{DummyKeyboardMapper, KeybindingKeystroke, Keystroke}; + use gpui::Keystroke; use unindent::Unindent; use crate::{ @@ -1050,27 +1049,16 @@ mod tests { operation: KeybindUpdateOperation, expected: impl ToString, ) { - let result = KeymapFile::update_keybinding( - operation, - input.to_string(), - 4, - &gpui::DummyKeyboardMapper, - ) - .expect("Update succeeded"); + let result = KeymapFile::update_keybinding(operation, input.to_string(), 4) + .expect("Update succeeded"); pretty_assertions::assert_eq!(expected.to_string(), result); } #[track_caller] - fn parse_keystrokes(keystrokes: &str) -> Vec { + fn parse_keystrokes(keystrokes: &str) -> Vec { keystrokes .split(' ') - .map(|s| { - KeybindingKeystroke::new( - Keystroke::parse(s).expect("Keystrokes valid"), - false, - &DummyKeyboardMapper, - ) - }) + .map(|s| Keystroke::parse(s).expect("Keystrokes valid")) .collect() } diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index 1966755d62..b73ab9ae95 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -1,5 +1,6 @@ mod base_keymap_setting; mod editable_setting_control; +mod key_equivalents; mod keymap_file; mod settings_file; mod settings_json; @@ -13,6 +14,7 @@ use util::asset_str; pub use base_keymap_setting::*; pub use editable_setting_control::*; +pub use key_equivalents::*; pub use keymap_file::{ KeyBindingValidator, KeyBindingValidatorRegistration, KeybindSource, KeybindUpdateOperation, KeybindUpdateTarget, KeymapFile, KeymapFileLoadResult, @@ -87,10 +89,7 @@ pub fn default_settings() -> Cow<'static, str> { #[cfg(target_os = "macos")] pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-macos.json"; -#[cfg(target_os = "windows")] -pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-windows.json"; - -#[cfg(not(any(target_os = "macos", target_os = "windows")))] +#[cfg(not(target_os = "macos"))] pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-linux.json"; pub fn default_keymap() -> Cow<'static, str> { diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index 3deaed8b9d..211db46c6c 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -60,11 +60,6 @@ pub trait Settings: 'static + Send + Sync { /// The logic for combining together values from one or more JSON files into the /// final value for this setting. - /// - /// # Warning - /// `Self::FileContent` deserialized field names should match with `Self` deserialized field names - /// otherwise the field won't be deserialized properly and you will get the error: - /// "A default setting must be added to the `default.json` file" fn load(sources: SettingsSources, cx: &mut App) -> Result where Self: Sized; diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 76c7166007..9a2d33ef7c 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -12,10 +12,8 @@ use fs::Fs; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ Action, AppContext as _, AsyncApp, Axis, ClickEvent, Context, DismissEvent, Entity, - EventEmitter, FocusHandle, Focusable, Global, IsZero, - KeyBindingContextPredicate::{And, Descendant, Equal, Identifier, Not, NotEqual, Or}, - KeyContext, KeybindingKeystroke, Keystroke, MouseButton, PlatformKeyboardMapper, Point, - ScrollStrategy, ScrollWheelEvent, Stateful, StyledText, Subscription, Task, + EventEmitter, FocusHandle, Focusable, Global, IsZero, KeyContext, Keystroke, MouseButton, + Point, ScrollStrategy, ScrollWheelEvent, Stateful, StyledText, Subscription, Task, TextStyleRefinement, WeakEntity, actions, anchored, deferred, div, }; use language::{Language, LanguageConfig, ToOffset as _}; @@ -174,7 +172,7 @@ impl FilterState { #[derive(Debug, Default, PartialEq, Eq, Clone, Hash)] struct ActionMapping { - keystrokes: Vec, + keystrokes: Vec, context: Option, } @@ -184,6 +182,15 @@ struct KeybindConflict { remaining_conflict_amount: usize, } +impl KeybindConflict { + fn from_iter<'a>(mut indices: impl Iterator) -> Option { + indices.next().map(|origin| Self { + first_conflict_index: origin.index, + remaining_conflict_amount: indices.count(), + }) + } +} + #[derive(Clone, Copy, PartialEq)] struct ConflictOrigin { override_source: KeybindSource, @@ -231,21 +238,13 @@ impl ConflictOrigin { #[derive(Default)] struct ConflictState { conflicts: Vec>, - keybind_mapping: ConflictKeybindMapping, + keybind_mapping: HashMap>, has_user_conflicts: bool, } -type ConflictKeybindMapping = HashMap< - Vec, - Vec<( - Option, - Vec, - )>, ->; - impl ConflictState { fn new(key_bindings: &[ProcessedBinding]) -> Self { - let mut action_keybind_mapping = ConflictKeybindMapping::default(); + let mut action_keybind_mapping: HashMap<_, Vec> = HashMap::default(); let mut largest_index = 0; for (index, binding) in key_bindings @@ -253,48 +252,29 @@ impl ConflictState { .enumerate() .flat_map(|(index, binding)| Some(index).zip(binding.keybind_information())) { - let mapping = binding.get_action_mapping(); - let predicate = mapping - .context - .and_then(|ctx| gpui::KeyBindingContextPredicate::parse(&ctx).ok()); - let entry = action_keybind_mapping - .entry(mapping.keystrokes) - .or_default(); - let origin = ConflictOrigin::new(binding.source, index); - if let Some((_, origins)) = - entry - .iter_mut() - .find(|(other_predicate, _)| match (&predicate, other_predicate) { - (None, None) => true, - (Some(a), Some(b)) => normalized_ctx_eq(a, b), - _ => false, - }) - { - origins.push(origin); - } else { - entry.push((predicate, vec![origin])); - } + action_keybind_mapping + .entry(binding.get_action_mapping()) + .or_default() + .push(ConflictOrigin::new(binding.source, index)); largest_index = index; } let mut conflicts = vec![None; largest_index + 1]; let mut has_user_conflicts = false; - for entries in action_keybind_mapping.values_mut() { - for (_, indices) in entries.iter_mut() { - indices.sort_unstable_by_key(|origin| origin.override_source); - let Some((fst, snd)) = indices.get(0).zip(indices.get(1)) else { - continue; - }; + for indices in action_keybind_mapping.values_mut() { + indices.sort_unstable_by_key(|origin| origin.override_source); + let Some((fst, snd)) = indices.get(0).zip(indices.get(1)) else { + continue; + }; - for origin in indices.iter() { - conflicts[origin.index] = - origin.get_conflict_with(if origin == fst { snd } else { fst }) - } - - has_user_conflicts |= fst.override_source == KeybindSource::User - && snd.override_source == KeybindSource::User; + for origin in indices.iter() { + conflicts[origin.index] = + origin.get_conflict_with(if origin == fst { snd } else { fst }) } + + has_user_conflicts |= fst.override_source == KeybindSource::User + && snd.override_source == KeybindSource::User; } Self { @@ -309,34 +289,15 @@ impl ConflictState { action_mapping: &ActionMapping, keybind_idx: Option, ) -> Option { - let ActionMapping { - keystrokes, - context, - } = action_mapping; - let predicate = context - .as_deref() - .and_then(|ctx| gpui::KeyBindingContextPredicate::parse(&ctx).ok()); - self.keybind_mapping.get(keystrokes).and_then(|entries| { - entries - .iter() - .find_map(|(other_predicate, indices)| { - match (&predicate, other_predicate) { - (None, None) => true, - (Some(pred), Some(other)) => normalized_ctx_eq(pred, other), - _ => false, - } - .then_some(indices) - }) - .and_then(|indices| { - let mut indices = indices + self.keybind_mapping + .get(action_mapping) + .and_then(|indices| { + KeybindConflict::from_iter( + indices .iter() - .filter(|&conflict| Some(conflict.index) != keybind_idx); - indices.next().map(|origin| KeybindConflict { - first_conflict_index: origin.index, - remaining_conflict_amount: indices.count(), - }) - }) - }) + .filter(|&conflict| Some(conflict.index) != keybind_idx), + ) + }) } fn conflict_for_idx(&self, idx: usize) -> Option { @@ -414,14 +375,12 @@ impl Focusable for KeymapEditor { } } /// Helper function to check if two keystroke sequences match exactly -fn keystrokes_match_exactly( - keystrokes1: &[KeybindingKeystroke], - keystrokes2: &[KeybindingKeystroke], -) -> bool { +fn keystrokes_match_exactly(keystrokes1: &[Keystroke], keystrokes2: &[Keystroke]) -> bool { keystrokes1.len() == keystrokes2.len() - && keystrokes1.iter().zip(keystrokes2).all(|(k1, k2)| { - k1.inner.key == k2.inner.key && k1.inner.modifiers == k2.inner.modifiers - }) + && keystrokes1 + .iter() + .zip(keystrokes2) + .all(|(k1, k2)| k1.key == k2.key && k1.modifiers == k2.modifiers) } impl KeymapEditor { @@ -511,7 +470,7 @@ impl KeymapEditor { self.filter_editor.read(cx).text(cx) } - fn current_keystroke_query(&self, cx: &App) -> Vec { + fn current_keystroke_query(&self, cx: &App) -> Vec { match self.search_mode { SearchMode::KeyStroke { .. } => self.keystroke_editor.read(cx).keystrokes().to_vec(), SearchMode::Normal => Default::default(), @@ -532,7 +491,7 @@ impl KeymapEditor { let keystroke_query = keystroke_query .into_iter() - .map(|keystroke| keystroke.inner.unparse()) + .map(|keystroke| keystroke.unparse()) .collect::>() .join(" "); @@ -556,7 +515,7 @@ impl KeymapEditor { async fn update_matches( this: WeakEntity, action_query: String, - keystroke_query: Vec, + keystroke_query: Vec, cx: &mut AsyncApp, ) -> anyhow::Result<()> { let action_query = command_palette::normalize_action_query(&action_query); @@ -605,15 +564,13 @@ impl KeymapEditor { { let query = &keystroke_query[query_cursor]; let keystroke = &keystrokes[keystroke_cursor]; - let matches = query - .inner - .modifiers - .is_subset_of(&keystroke.inner.modifiers) - && ((query.inner.key.is_empty() - || query.inner.key == keystroke.inner.key) - && query.inner.key_char.as_ref().is_none_or( - |q_kc| q_kc == &keystroke.inner.key, - )); + let matches = + query.modifiers.is_subset_of(&keystroke.modifiers) + && ((query.key.is_empty() + || query.key == keystroke.key) + && query.key_char.as_ref().is_none_or( + |q_kc| q_kc == &keystroke.key, + )); if matches { found_count += 1; query_cursor += 1; @@ -682,7 +639,7 @@ impl KeymapEditor { .map(KeybindSource::from_meta) .unwrap_or(KeybindSource::Unknown); - let keystroke_text = ui::text_for_keybinding_keystrokes(key_binding.keystrokes(), cx); + let keystroke_text = ui::text_for_keystrokes(key_binding.keystrokes(), cx); let ui_key_binding = ui::KeyBinding::new_from_gpui(key_binding.clone(), cx) .vim_mode(source == KeybindSource::Vim); @@ -1206,11 +1163,8 @@ impl KeymapEditor { .read(cx) .get_scrollbar_offset(Axis::Vertical), )); - let keyboard_mapper = cx.keyboard_mapper().clone(); - cx.spawn(async move |_, _| { - remove_keybinding(to_remove, &fs, tab_size, keyboard_mapper.as_ref()).await - }) - .detach_and_notify_err(window, cx); + cx.spawn(async move |_, _| remove_keybinding(to_remove, &fs, tab_size).await) + .detach_and_notify_err(window, cx); } fn copy_context_to_clipboard( @@ -1429,7 +1383,7 @@ impl ProcessedBinding { .map(|keybind| keybind.get_action_mapping()) } - fn keystrokes(&self) -> Option<&[KeybindingKeystroke]> { + fn keystrokes(&self) -> Option<&[Keystroke]> { self.ui_key_binding() .map(|binding| binding.keystrokes.as_slice()) } @@ -2227,7 +2181,7 @@ impl KeybindingEditorModal { Ok(action_arguments) } - fn validate_keystrokes(&self, cx: &App) -> anyhow::Result> { + fn validate_keystrokes(&self, cx: &App) -> anyhow::Result> { let new_keystrokes = self .keybind_editor .read_with(cx, |editor, _| editor.keystrokes().to_vec()); @@ -2323,7 +2277,6 @@ impl KeybindingEditorModal { }).unwrap_or(Ok(()))?; let create = self.creating; - let keyboard_mapper = cx.keyboard_mapper().clone(); cx.spawn(async move |this, cx| { let action_name = existing_keybind.action().name; @@ -2336,7 +2289,6 @@ impl KeybindingEditorModal { new_action_args.as_deref(), &fs, tab_size, - keyboard_mapper.as_ref(), ) .await { @@ -2454,21 +2406,11 @@ impl KeybindingEditorModal { } } -fn remove_key_char( - KeybindingKeystroke { - inner, - display_modifiers, - display_key, - }: KeybindingKeystroke, -) -> KeybindingKeystroke { - KeybindingKeystroke { - inner: Keystroke { - modifiers: inner.modifiers, - key: inner.key, - key_char: None, - }, - display_modifiers, - display_key, +fn remove_key_char(Keystroke { modifiers, key, .. }: Keystroke) -> Keystroke { + Keystroke { + modifiers, + key, + ..Default::default() } } @@ -3011,7 +2953,6 @@ async fn save_keybinding_update( new_args: Option<&str>, fs: &Arc, tab_size: usize, - keyboard_mapper: &dyn PlatformKeyboardMapper, ) -> anyhow::Result<()> { let keymap_contents = settings::KeymapFile::load_keymap_file(fs) .await @@ -3054,13 +2995,9 @@ async fn save_keybinding_update( let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry(); - let updated_keymap_contents = settings::KeymapFile::update_keybinding( - operation, - keymap_contents, - tab_size, - keyboard_mapper, - ) - .map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?; + let updated_keymap_contents = + settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size) + .map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?; fs.write( paths::keymap_file().as_path(), updated_keymap_contents.as_bytes(), @@ -3081,7 +3018,6 @@ async fn remove_keybinding( existing: ProcessedBinding, fs: &Arc, tab_size: usize, - keyboard_mapper: &dyn PlatformKeyboardMapper, ) -> anyhow::Result<()> { let Some(keystrokes) = existing.keystrokes() else { anyhow::bail!("Cannot remove a keybinding that does not exist"); @@ -3105,13 +3041,9 @@ async fn remove_keybinding( }; let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry(); - let updated_keymap_contents = settings::KeymapFile::update_keybinding( - operation, - keymap_contents, - tab_size, - keyboard_mapper, - ) - .context("Failed to update keybinding")?; + let updated_keymap_contents = + settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size) + .context("Failed to update keybinding")?; fs.write( paths::keymap_file().as_path(), updated_keymap_contents.as_bytes(), @@ -3157,29 +3089,29 @@ fn collect_contexts_from_assets() -> Vec { queue.push(root_context); while let Some(context) = queue.pop() { match context { - Identifier(ident) => { + gpui::KeyBindingContextPredicate::Identifier(ident) => { contexts.insert(ident); } - Equal(ident_a, ident_b) => { + gpui::KeyBindingContextPredicate::Equal(ident_a, ident_b) => { contexts.insert(ident_a); contexts.insert(ident_b); } - NotEqual(ident_a, ident_b) => { + gpui::KeyBindingContextPredicate::NotEqual(ident_a, ident_b) => { contexts.insert(ident_a); contexts.insert(ident_b); } - Descendant(ctx_a, ctx_b) => { + gpui::KeyBindingContextPredicate::Descendant(ctx_a, ctx_b) => { queue.push(*ctx_a); queue.push(*ctx_b); } - Not(ctx) => { + gpui::KeyBindingContextPredicate::Not(ctx) => { queue.push(*ctx); } - And(ctx_a, ctx_b) => { + gpui::KeyBindingContextPredicate::And(ctx_a, ctx_b) => { queue.push(*ctx_a); queue.push(*ctx_b); } - Or(ctx_a, ctx_b) => { + gpui::KeyBindingContextPredicate::Or(ctx_a, ctx_b) => { queue.push(*ctx_a); queue.push(*ctx_b); } @@ -3194,127 +3126,6 @@ fn collect_contexts_from_assets() -> Vec { contexts } -fn normalized_ctx_eq( - a: &gpui::KeyBindingContextPredicate, - b: &gpui::KeyBindingContextPredicate, -) -> bool { - use gpui::KeyBindingContextPredicate::*; - return match (a, b) { - (Identifier(_), Identifier(_)) => a == b, - (Equal(a_left, a_right), Equal(b_left, b_right)) => { - (a_left == b_left && a_right == b_right) || (a_left == b_right && a_right == b_left) - } - (NotEqual(a_left, a_right), NotEqual(b_left, b_right)) => { - (a_left == b_left && a_right == b_right) || (a_left == b_right && a_right == b_left) - } - (Descendant(a_parent, a_child), Descendant(b_parent, b_child)) => { - normalized_ctx_eq(a_parent, b_parent) && normalized_ctx_eq(a_child, b_child) - } - (Not(a_expr), Not(b_expr)) => normalized_ctx_eq(a_expr, b_expr), - // Handle double negation: !(!a) == a - (Not(a_expr), b) if matches!(a_expr.as_ref(), Not(_)) => { - let Not(a_inner) = a_expr.as_ref() else { - unreachable!(); - }; - normalized_ctx_eq(b, a_inner) - } - (a, Not(b_expr)) if matches!(b_expr.as_ref(), Not(_)) => { - let Not(b_inner) = b_expr.as_ref() else { - unreachable!(); - }; - normalized_ctx_eq(a, b_inner) - } - (And(a_left, a_right), And(b_left, b_right)) - if matches!(a_left.as_ref(), And(_, _)) - || matches!(a_right.as_ref(), And(_, _)) - || matches!(b_left.as_ref(), And(_, _)) - || matches!(b_right.as_ref(), And(_, _)) => - { - let mut a_operands = Vec::new(); - flatten_and(a, &mut a_operands); - let mut b_operands = Vec::new(); - flatten_and(b, &mut b_operands); - compare_operand_sets(&a_operands, &b_operands) - } - (And(a_left, a_right), And(b_left, b_right)) => { - (normalized_ctx_eq(a_left, b_left) && normalized_ctx_eq(a_right, b_right)) - || (normalized_ctx_eq(a_left, b_right) && normalized_ctx_eq(a_right, b_left)) - } - (Or(a_left, a_right), Or(b_left, b_right)) - if matches!(a_left.as_ref(), Or(_, _)) - || matches!(a_right.as_ref(), Or(_, _)) - || matches!(b_left.as_ref(), Or(_, _)) - || matches!(b_right.as_ref(), Or(_, _)) => - { - let mut a_operands = Vec::new(); - flatten_or(a, &mut a_operands); - let mut b_operands = Vec::new(); - flatten_or(b, &mut b_operands); - compare_operand_sets(&a_operands, &b_operands) - } - (Or(a_left, a_right), Or(b_left, b_right)) => { - (normalized_ctx_eq(a_left, b_left) && normalized_ctx_eq(a_right, b_right)) - || (normalized_ctx_eq(a_left, b_right) && normalized_ctx_eq(a_right, b_left)) - } - _ => false, - }; - - fn flatten_and<'a>( - pred: &'a gpui::KeyBindingContextPredicate, - operands: &mut Vec<&'a gpui::KeyBindingContextPredicate>, - ) { - use gpui::KeyBindingContextPredicate::*; - match pred { - And(left, right) => { - flatten_and(left, operands); - flatten_and(right, operands); - } - _ => operands.push(pred), - } - } - - fn flatten_or<'a>( - pred: &'a gpui::KeyBindingContextPredicate, - operands: &mut Vec<&'a gpui::KeyBindingContextPredicate>, - ) { - use gpui::KeyBindingContextPredicate::*; - match pred { - Or(left, right) => { - flatten_or(left, operands); - flatten_or(right, operands); - } - _ => operands.push(pred), - } - } - - fn compare_operand_sets( - a: &[&gpui::KeyBindingContextPredicate], - b: &[&gpui::KeyBindingContextPredicate], - ) -> bool { - if a.len() != b.len() { - return false; - } - - // For each operand in a, find a matching operand in b - let mut b_matched = vec![false; b.len()]; - for a_operand in a { - let mut found = false; - for (b_idx, b_operand) in b.iter().enumerate() { - if !b_matched[b_idx] && normalized_ctx_eq(a_operand, b_operand) { - b_matched[b_idx] = true; - found = true; - break; - } - } - if !found { - return false; - } - } - - true - } -} - impl SerializableItem for KeymapEditor { fn serialized_item_kind() -> &'static str { "KeymapEditor" @@ -3377,15 +3188,12 @@ impl SerializableItem for KeymapEditor { } mod persistence { - use db::{query, sqlez::domain::Domain, sqlez_macros::sql}; + use db::{define_connection, query, sqlez_macros::sql}; use workspace::WorkspaceDb; - pub struct KeybindingEditorDb(db::sqlez::thread_safe_connection::ThreadSafeConnection); - - impl Domain for KeybindingEditorDb { - const NAME: &str = stringify!(KeybindingEditorDb); - - const MIGRATIONS: &[&str] = &[sql!( + define_connection! { + pub static ref KEYBINDING_EDITORS: KeybindingEditorDb = + &[sql!( CREATE TABLE keybinding_editors ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -3394,11 +3202,9 @@ mod persistence { FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; - )]; + )]; } - db::static_connection!(KEYBINDING_EDITORS, KeybindingEditorDb, [WorkspaceDb]); - impl KeybindingEditorDb { query! { pub async fn save_keybinding_editor( @@ -3422,152 +3228,3 @@ mod persistence { } } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn normalized_ctx_cmp() { - #[track_caller] - fn cmp(a: &str, b: &str) -> bool { - let a = gpui::KeyBindingContextPredicate::parse(a) - .expect("Failed to parse keybinding context a"); - let b = gpui::KeyBindingContextPredicate::parse(b) - .expect("Failed to parse keybinding context b"); - normalized_ctx_eq(&a, &b) - } - - // Basic equality - identical expressions - assert!(cmp("a && b", "a && b")); - assert!(cmp("a || b", "a || b")); - assert!(cmp("a == b", "a == b")); - assert!(cmp("a != b", "a != b")); - assert!(cmp("a > b", "a > b")); - assert!(cmp("!a", "!a")); - - // AND operator - associative/commutative - assert!(cmp("a && b", "b && a")); - assert!(cmp("a && b && c", "c && b && a")); - assert!(cmp("a && b && c", "b && a && c")); - assert!(cmp("a && b && c && d", "d && c && b && a")); - - // OR operator - associative/commutative - assert!(cmp("a || b", "b || a")); - assert!(cmp("a || b || c", "c || b || a")); - assert!(cmp("a || b || c", "b || a || c")); - assert!(cmp("a || b || c || d", "d || c || b || a")); - - // Equality operator - associative/commutative - assert!(cmp("a == b", "b == a")); - assert!(cmp("x == y", "y == x")); - - // Inequality operator - associative/commutative - assert!(cmp("a != b", "b != a")); - assert!(cmp("x != y", "y != x")); - - // Complex nested expressions with associative operators - assert!(cmp("(a && b) || c", "c || (a && b)")); - assert!(cmp("(a && b) || c", "c || (b && a)")); - assert!(cmp("(a || b) && c", "c && (a || b)")); - assert!(cmp("(a || b) && c", "c && (b || a)")); - assert!(cmp("(a && b) || (c && d)", "(c && d) || (a && b)")); - assert!(cmp("(a && b) || (c && d)", "(d && c) || (b && a)")); - - // Multiple levels of nesting - assert!(cmp("((a && b) || c) && d", "d && ((a && b) || c)")); - assert!(cmp("((a && b) || c) && d", "d && (c || (b && a))")); - assert!(cmp("a && (b || (c && d))", "(b || (c && d)) && a")); - assert!(cmp("a && (b || (c && d))", "(b || (d && c)) && a")); - - // Negation with associative operators - assert!(cmp("!a && b", "b && !a")); - assert!(cmp("!a || b", "b || !a")); - assert!(cmp("!(a && b) || c", "c || !(a && b)")); - assert!(cmp("!(a && b) || c", "c || !(b && a)")); - - // Descendant operator (>) - NOT associative/commutative - assert!(cmp("a > b", "a > b")); - assert!(!cmp("a > b", "b > a")); - assert!(!cmp("a > b > c", "c > b > a")); - assert!(!cmp("a > b > c", "a > c > b")); - - // Mixed operators with descendant - assert!(cmp("(a > b) && c", "c && (a > b)")); - assert!(!cmp("(a > b) && c", "c && (b > a)")); - assert!(cmp("(a > b) || (c > d)", "(c > d) || (a > b)")); - assert!(!cmp("(a > b) || (c > d)", "(b > a) || (d > c)")); - - // Negative cases - different operators - assert!(!cmp("a && b", "a || b")); - assert!(!cmp("a == b", "a != b")); - assert!(!cmp("a && b", "a > b")); - assert!(!cmp("a || b", "a > b")); - assert!(!cmp("a == b", "a && b")); - assert!(!cmp("a != b", "a || b")); - - // Negative cases - different operands - assert!(!cmp("a && b", "a && c")); - assert!(!cmp("a && b", "c && d")); - assert!(!cmp("a || b", "a || c")); - assert!(!cmp("a || b", "c || d")); - assert!(!cmp("a == b", "a == c")); - assert!(!cmp("a != b", "a != c")); - assert!(!cmp("a > b", "a > c")); - assert!(!cmp("a > b", "c > b")); - - // Negative cases - with negation - assert!(!cmp("!a", "a")); - assert!(!cmp("!a && b", "a && b")); - assert!(!cmp("!(a && b)", "a && b")); - assert!(!cmp("!a || b", "a || b")); - assert!(!cmp("!(a || b)", "a || b")); - - // Negative cases - complex expressions - assert!(!cmp("(a && b) || c", "(a || b) && c")); - assert!(!cmp("a && (b || c)", "a || (b && c)")); - assert!(!cmp("(a && b) || (c && d)", "(a || b) && (c || d)")); - assert!(!cmp("a > b && c", "a && b > c")); - - // Edge cases - multiple same operands - assert!(cmp("a && a", "a && a")); - assert!(cmp("a || a", "a || a")); - assert!(cmp("a && a && b", "b && a && a")); - assert!(cmp("a || a || b", "b || a || a")); - - // Edge cases - deeply nested - assert!(cmp( - "((a && b) || (c && d)) && ((e || f) && g)", - "((e || f) && g) && ((c && d) || (a && b))" - )); - assert!(cmp( - "((a && b) || (c && d)) && ((e || f) && g)", - "(g && (f || e)) && ((d && c) || (b && a))" - )); - - // Edge cases - repeated patterns - assert!(cmp("(a && b) || (a && b)", "(b && a) || (b && a)")); - assert!(cmp("(a || b) && (a || b)", "(b || a) && (b || a)")); - - // Negative cases - subtle differences - assert!(!cmp("a && b && c", "a && b")); - assert!(!cmp("a || b || c", "a || b")); - assert!(!cmp("(a && b) || c", "a && (b || c)")); - - // a > b > c is not the same as a > c, should not be equal - assert!(!cmp("a > b > c", "a > c")); - - // Double negation with complex expressions - assert!(cmp("!(!(a && b))", "a && b")); - assert!(cmp("!(!(a || b))", "a || b")); - assert!(cmp("!(!(a > b))", "a > b")); - assert!(cmp("!(!a) && b", "a && b")); - assert!(cmp("!(!a) || b", "a || b")); - assert!(cmp("!(!(a && b)) || c", "(a && b) || c")); - assert!(cmp("!(!(a && b)) || c", "(b && a) || c")); - assert!(cmp("!(!a)", "a")); - assert!(cmp("a", "!(!a)")); - assert!(cmp("!(!(!a))", "!a")); - assert!(cmp("!(!(!(!a)))", "a")); - } -} diff --git a/crates/settings_ui/src/ui_components/keystroke_input.rs b/crates/settings_ui/src/ui_components/keystroke_input.rs index ca50d5c03d..1b8010853e 100644 --- a/crates/settings_ui/src/ui_components/keystroke_input.rs +++ b/crates/settings_ui/src/ui_components/keystroke_input.rs @@ -1,6 +1,6 @@ use gpui::{ Animation, AnimationExt, Context, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext, - KeybindingKeystroke, Keystroke, Modifiers, ModifiersChangedEvent, Subscription, Task, actions, + Keystroke, Modifiers, ModifiersChangedEvent, Subscription, Task, actions, }; use ui::{ ActiveTheme as _, Color, IconButton, IconButtonShape, IconName, IconSize, Label, LabelSize, @@ -42,8 +42,8 @@ impl PartialEq for CloseKeystrokeResult { } pub struct KeystrokeInput { - keystrokes: Vec, - placeholder_keystrokes: Option>, + keystrokes: Vec, + placeholder_keystrokes: Option>, outer_focus_handle: FocusHandle, inner_focus_handle: FocusHandle, intercept_subscription: Option, @@ -70,7 +70,7 @@ impl KeystrokeInput { const KEYSTROKE_COUNT_MAX: usize = 3; pub fn new( - placeholder_keystrokes: Option>, + placeholder_keystrokes: Option>, window: &mut Window, cx: &mut Context, ) -> Self { @@ -97,7 +97,7 @@ impl KeystrokeInput { } } - pub fn set_keystrokes(&mut self, keystrokes: Vec, cx: &mut Context) { + pub fn set_keystrokes(&mut self, keystrokes: Vec, cx: &mut Context) { self.keystrokes = keystrokes; self.keystrokes_changed(cx); } @@ -106,7 +106,7 @@ impl KeystrokeInput { self.search = search; } - pub fn keystrokes(&self) -> &[KeybindingKeystroke] { + pub fn keystrokes(&self) -> &[Keystroke] { if let Some(placeholders) = self.placeholder_keystrokes.as_ref() && self.keystrokes.is_empty() { @@ -116,22 +116,18 @@ impl KeystrokeInput { && self .keystrokes .last() - .is_some_and(|last| last.display_key.is_empty()) + .is_some_and(|last| last.key.is_empty()) { return &self.keystrokes[..self.keystrokes.len() - 1]; } &self.keystrokes } - fn dummy(modifiers: Modifiers) -> KeybindingKeystroke { - KeybindingKeystroke { - inner: Keystroke { - modifiers, - key: "".to_string(), - key_char: None, - }, - display_modifiers: modifiers, - display_key: "".to_string(), + fn dummy(modifiers: Modifiers) -> Keystroke { + Keystroke { + modifiers, + key: "".to_string(), + key_char: None, } } @@ -258,7 +254,7 @@ impl KeystrokeInput { self.keystrokes_changed(cx); if let Some(last) = self.keystrokes.last_mut() - && last.display_key.is_empty() + && last.key.is_empty() && keystrokes_len <= Self::KEYSTROKE_COUNT_MAX { if !self.search && !event.modifiers.modified() { @@ -267,15 +263,13 @@ impl KeystrokeInput { } if self.search { if self.previous_modifiers.modified() { - last.display_modifiers |= event.modifiers; - last.inner.modifiers |= event.modifiers; + last.modifiers |= event.modifiers; } else { self.keystrokes.push(Self::dummy(event.modifiers)); } self.previous_modifiers |= event.modifiers; } else { - last.display_modifiers = event.modifiers; - last.inner.modifiers = event.modifiers; + last.modifiers = event.modifiers; return; } } else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX { @@ -303,17 +297,14 @@ impl KeystrokeInput { return; } - let mut keystroke = - KeybindingKeystroke::new(keystroke.clone(), false, cx.keyboard_mapper().as_ref()); + let mut keystroke = keystroke.clone(); if let Some(last) = self.keystrokes.last() - && last.display_key.is_empty() + && last.key.is_empty() && (!self.search || self.previous_modifiers.modified()) { - let display_key = keystroke.display_key.clone(); - let inner_key = keystroke.inner.key.clone(); + let key = keystroke.key.clone(); keystroke = last.clone(); - keystroke.display_key = display_key; - keystroke.inner.key = inner_key; + keystroke.key = key; self.keystrokes.pop(); } @@ -333,14 +324,11 @@ impl KeystrokeInput { self.keystrokes_changed(cx); if self.search { - self.previous_modifiers = keystroke.display_modifiers; + self.previous_modifiers = keystroke.modifiers; return; } - if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX - && keystroke.display_modifiers.modified() - { - self.keystrokes - .push(Self::dummy(keystroke.display_modifiers)); + if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX && keystroke.modifiers.modified() { + self.keystrokes.push(Self::dummy(keystroke.modifiers)); } } @@ -376,7 +364,7 @@ impl KeystrokeInput { &self.keystrokes }; keystrokes.iter().map(move |keystroke| { - h_flex().children(ui::render_keybinding_keystroke( + h_flex().children(ui::render_keystroke( keystroke, Some(Color::Default), Some(rems(0.875).into()), @@ -821,13 +809,9 @@ mod tests { /// Verifies that the keystrokes match the expected strings #[track_caller] pub fn expect_keystrokes(&mut self, expected: &[&str]) -> &mut Self { - let actual: Vec = self.input.read_with(&self.cx, |input, _| { - input - .keystrokes - .iter() - .map(|keystroke| keystroke.inner.clone()) - .collect() - }); + let actual = self + .input + .read_with(&self.cx, |input, _| input.keystrokes.clone()); Self::expect_keystrokes_equal(&actual, expected); self } @@ -955,7 +939,7 @@ mod tests { } struct KeystrokeUpdateTracker { - initial_keystrokes: Vec, + initial_keystrokes: Vec, _subscription: Subscription, input: Entity, received_keystrokes_updated: bool, @@ -999,8 +983,8 @@ mod tests { ); } - fn keystrokes_str(ks: &[KeybindingKeystroke]) -> String { - ks.iter().map(|ks| ks.inner.unparse()).join(" ") + fn keystrokes_str(ks: &[Keystroke]) -> String { + ks.iter().map(|ks| ks.unparse()).join(" ") } } } diff --git a/crates/sqlez/src/domain.rs b/crates/sqlez/src/domain.rs index 5744a67da2..a83f4e18d6 100644 --- a/crates/sqlez/src/domain.rs +++ b/crates/sqlez/src/domain.rs @@ -1,12 +1,8 @@ use crate::connection::Connection; pub trait Domain: 'static { - const NAME: &str; - const MIGRATIONS: &[&str]; - - fn should_allow_migration_change(_index: usize, _old: &str, _new: &str) -> bool { - false - } + fn name() -> &'static str; + fn migrations() -> &'static [&'static str]; } pub trait Migrator: 'static { @@ -21,11 +17,7 @@ impl Migrator for () { impl Migrator for D { fn migrate(connection: &Connection) -> anyhow::Result<()> { - connection.migrate( - Self::NAME, - Self::MIGRATIONS, - Self::should_allow_migration_change, - ) + connection.migrate(Self::name(), Self::migrations()) } } diff --git a/crates/sqlez/src/migrations.rs b/crates/sqlez/src/migrations.rs index 2429ddeb41..7c59ffe658 100644 --- a/crates/sqlez/src/migrations.rs +++ b/crates/sqlez/src/migrations.rs @@ -34,12 +34,7 @@ impl Connection { /// Note: Unlike everything else in SQLez, migrations are run eagerly, without first /// preparing the SQL statements. This makes it possible to do multi-statement schema /// updates in a single string without running into prepare errors. - pub fn migrate( - &self, - domain: &'static str, - migrations: &[&'static str], - mut should_allow_migration_change: impl FnMut(usize, &str, &str) -> bool, - ) -> Result<()> { + pub fn migrate(&self, domain: &'static str, migrations: &[&'static str]) -> Result<()> { self.with_savepoint("migrating", || { // Setup the migrations table unconditionally self.exec(indoc! {" @@ -70,14 +65,9 @@ impl Connection { &sqlformat::QueryParams::None, Default::default(), ); - if completed_migration == migration - || migration.trim().starts_with("-- ALLOW_MIGRATION_CHANGE") - { + if completed_migration == migration { // Migration already run. Continue continue; - } else if should_allow_migration_change(index, &completed_migration, &migration) - { - continue; } else { anyhow::bail!(formatdoc! {" Migration changed for {domain} at step {index} @@ -118,7 +108,6 @@ mod test { a TEXT, b TEXT )"}], - disallow_migration_change, ) .unwrap(); @@ -147,7 +136,6 @@ mod test { d TEXT )"}, ], - disallow_migration_change, ) .unwrap(); @@ -226,11 +214,7 @@ mod test { // Run the migration verifying that the row got dropped connection - .migrate( - "test", - &["DELETE FROM test_table"], - disallow_migration_change, - ) + .migrate("test", &["DELETE FROM test_table"]) .unwrap(); assert_eq!( connection @@ -248,11 +232,7 @@ mod test { // Run the same migration again and verify that the table was left unchanged connection - .migrate( - "test", - &["DELETE FROM test_table"], - disallow_migration_change, - ) + .migrate("test", &["DELETE FROM test_table"]) .unwrap(); assert_eq!( connection @@ -272,28 +252,27 @@ mod test { .migrate( "test migration", &[ - "CREATE TABLE test (col INTEGER)", - "INSERT INTO test (col) VALUES (1)", + indoc! {" + CREATE TABLE test ( + col INTEGER + )"}, + indoc! {" + INSERT INTO test (col) VALUES (1)"}, ], - disallow_migration_change, ) .unwrap(); - let mut migration_changed = false; - // Create another migration with the same domain but different steps let second_migration_result = connection.migrate( "test migration", &[ - "CREATE TABLE test (color INTEGER )", - "INSERT INTO test (color) VALUES (1)", + indoc! {" + CREATE TABLE test ( + color INTEGER + )"}, + indoc! {" + INSERT INTO test (color) VALUES (1)"}, ], - |_, old, new| { - assert_eq!(old, "CREATE TABLE test (col INTEGER)"); - assert_eq!(new, "CREATE TABLE test (color INTEGER)"); - migration_changed = true; - false - }, ); // Verify new migration returns error when run @@ -305,11 +284,7 @@ mod test { let connection = Connection::open_memory(Some("test_create_alter_drop")); connection - .migrate( - "first_migration", - &["CREATE TABLE table1(a TEXT) STRICT;"], - disallow_migration_change, - ) + .migrate("first_migration", &["CREATE TABLE table1(a TEXT) STRICT;"]) .unwrap(); connection @@ -330,7 +305,6 @@ mod test { ALTER TABLE table2 RENAME TO table1; "}], - disallow_migration_change, ) .unwrap(); @@ -338,8 +312,4 @@ mod test { assert_eq!(res, "test text"); } - - fn disallow_migration_change(_: usize, _: &str, _: &str) -> bool { - false - } } diff --git a/crates/sqlez/src/thread_safe_connection.rs b/crates/sqlez/src/thread_safe_connection.rs index 58d3afe78f..afdc96586e 100644 --- a/crates/sqlez/src/thread_safe_connection.rs +++ b/crates/sqlez/src/thread_safe_connection.rs @@ -278,8 +278,12 @@ mod test { enum TestDomain {} impl Domain for TestDomain { - const NAME: &str = "test"; - const MIGRATIONS: &[&str] = &["CREATE TABLE test(col1 TEXT, col2 TEXT) STRICT;"]; + fn name() -> &'static str { + "test" + } + fn migrations() -> &'static [&'static str] { + &["CREATE TABLE test(col1 TEXT, col2 TEXT) STRICT;"] + } } for _ in 0..100 { @@ -308,9 +312,12 @@ mod test { fn wild_zed_lost_failure() { enum TestWorkspace {} impl Domain for TestWorkspace { - const NAME: &str = "workspace"; + fn name() -> &'static str { + "workspace" + } - const MIGRATIONS: &[&str] = &[" + fn migrations() -> &'static [&'static str] { + &[" CREATE TABLE workspaces( workspace_id INTEGER PRIMARY KEY, dock_visible INTEGER, -- Boolean @@ -329,7 +336,8 @@ mod test { ON DELETE CASCADE ON UPDATE CASCADE ) STRICT; - "]; + "] + } } let builder = diff --git a/crates/system_specs/Cargo.toml b/crates/system_specs/Cargo.toml deleted file mode 100644 index 8ef1b581ae..0000000000 --- a/crates/system_specs/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "system_specs" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/system_specs.rs" - -[features] -default = [] - -[dependencies] -anyhow.workspace = true -client.workspace = true -gpui.workspace = true -human_bytes.workspace = true -release_channel.workspace = true -serde.workspace = true -sysinfo.workspace = true -workspace-hack.workspace = true - -[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies] -pciid-parser.workspace = true diff --git a/crates/system_specs/LICENSE-GPL b/crates/system_specs/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/system_specs/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/tab_switcher/src/tab_switcher.rs b/crates/tab_switcher/src/tab_switcher.rs index bf3ce7b568..11e32523b4 100644 --- a/crates/tab_switcher/src/tab_switcher.rs +++ b/crates/tab_switcher/src/tab_switcher.rs @@ -2,14 +2,12 @@ mod tab_switcher_tests; use collections::HashMap; -use editor::items::{ - entry_diagnostic_aware_icon_decoration_and_color, entry_git_aware_label_color, -}; +use editor::items::entry_git_aware_label_color; use fuzzy::StringMatchCandidate; use gpui::{ Action, AnyElement, App, Context, DismissEvent, Entity, EntityId, EventEmitter, FocusHandle, - Focusable, Modifiers, ModifiersChangedEvent, MouseButton, MouseUpEvent, ParentElement, Point, - Render, Styled, Task, WeakEntity, Window, actions, rems, + Focusable, Modifiers, ModifiersChangedEvent, MouseButton, MouseUpEvent, ParentElement, Render, + Styled, Task, WeakEntity, Window, actions, rems, }; use picker::{Picker, PickerDelegate}; use project::Project; @@ -17,14 +15,11 @@ use schemars::JsonSchema; use serde::Deserialize; use settings::Settings; use std::{cmp::Reverse, sync::Arc}; -use ui::{ - DecoratedIcon, IconDecoration, IconDecorationKind, ListItem, ListItemSpacing, Tooltip, - prelude::*, -}; +use ui::{ListItem, ListItemSpacing, Tooltip, prelude::*}; use util::ResultExt; use workspace::{ ModalView, Pane, SaveIntent, Workspace, - item::{ItemHandle, ItemSettings, ShowDiagnostics, TabContentParams}, + item::{ItemHandle, ItemSettings, TabContentParams}, pane::{Event as PaneEvent, render_item_indicator, tab_details}, }; @@ -118,13 +113,7 @@ impl TabSwitcher { } let weak_workspace = workspace.weak_handle(); - let project = workspace.project().clone(); - let original_items: Vec<_> = workspace - .panes() - .iter() - .map(|p| (p.clone(), p.read(cx).active_item_index())) - .collect(); workspace.toggle_modal(window, cx, |window, cx| { let delegate = TabSwitcherDelegate::new( project, @@ -135,7 +124,6 @@ impl TabSwitcher { is_global, window, cx, - original_items, ); TabSwitcher::new(delegate, window, is_global, cx) }); @@ -233,80 +221,7 @@ pub struct TabSwitcherDelegate { workspace: WeakEntity, project: Entity, matches: Vec, - original_items: Vec<(Entity, usize)>, is_all_panes: bool, - restored_items: bool, -} - -impl TabMatch { - fn icon( - &self, - project: &Entity, - selected: bool, - window: &Window, - cx: &App, - ) -> Option { - let icon = self.item.tab_icon(window, cx)?; - let item_settings = ItemSettings::get_global(cx); - let show_diagnostics = item_settings.show_diagnostics; - let git_status_color = item_settings - .git_status - .then(|| { - let path = self.item.project_path(cx)?; - let project = project.read(cx); - let entry = project.entry_for_path(&path, cx)?; - let git_status = project - .project_path_git_status(&path, cx) - .map(|status| status.summary()) - .unwrap_or_default(); - Some(entry_git_aware_label_color( - git_status, - entry.is_ignored, - selected, - )) - }) - .flatten(); - let colored_icon = icon.color(git_status_color.unwrap_or_default()); - - let most_sever_diagostic_level = if show_diagnostics == ShowDiagnostics::Off { - None - } else { - let buffer_store = project.read(cx).buffer_store().read(cx); - let buffer = self - .item - .project_path(cx) - .and_then(|path| buffer_store.get_by_path(&path)) - .map(|buffer| buffer.read(cx)); - buffer.and_then(|buffer| { - buffer - .buffer_diagnostics(None) - .iter() - .map(|diagnostic_entry| diagnostic_entry.diagnostic.severity) - .min() - }) - }; - - let decorations = - entry_diagnostic_aware_icon_decoration_and_color(most_sever_diagostic_level) - .filter(|(d, _)| { - *d != IconDecorationKind::Triangle - || show_diagnostics != ShowDiagnostics::Errors - }) - .map(|(icon, color)| { - let knockout_item_color = if selected { - cx.theme().colors().element_selected - } else { - cx.theme().colors().element_background - }; - IconDecoration::new(icon, knockout_item_color, cx) - .color(color.color(cx)) - .position(Point { - x: px(-2.), - y: px(-2.), - }) - }); - Some(DecoratedIcon::new(colored_icon, decorations)) - } } impl TabSwitcherDelegate { @@ -320,7 +235,6 @@ impl TabSwitcherDelegate { is_all_panes: bool, window: &mut Window, cx: &mut Context, - original_items: Vec<(Entity, usize)>, ) -> Self { Self::subscribe_to_updates(&pane, window, cx); Self { @@ -332,8 +246,6 @@ impl TabSwitcherDelegate { project, matches: Vec::new(), is_all_panes, - original_items, - restored_items: false, } } @@ -388,6 +300,13 @@ impl TabSwitcherDelegate { let matches = if query.is_empty() { let history = workspace.read(cx).recently_activated_items(cx); + for item in &all_items { + eprintln!( + "{:?} {:?}", + item.item.tab_content_text(0, cx), + (Reverse(history.get(&item.item.item_id())), item.item_index) + ) + } all_items .sort_by_key(|tab| (Reverse(history.get(&tab.item.item_id())), tab.item_index)); all_items @@ -554,25 +473,8 @@ impl PickerDelegate for TabSwitcherDelegate { self.selected_index } - fn set_selected_index( - &mut self, - ix: usize, - window: &mut Window, - cx: &mut Context>, - ) { + fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context>) { self.selected_index = ix; - - let Some(selected_match) = self.matches.get(self.selected_index()) else { - return; - }; - selected_match - .pane - .update(cx, |pane, cx| { - if let Some(index) = pane.index_for_item(selected_match.item.as_ref()) { - pane.activate_item(index, false, false, window, cx); - } - }) - .ok(); cx.notify(); } @@ -599,13 +501,6 @@ impl PickerDelegate for TabSwitcherDelegate { let Some(selected_match) = self.matches.get(self.selected_index()) else { return; }; - - self.restored_items = true; - for (pane, index) in self.original_items.iter() { - pane.update(cx, |this, cx| { - this.activate_item(*index, false, false, window, cx); - }) - } selected_match .pane .update(cx, |pane, cx| { @@ -616,15 +511,7 @@ impl PickerDelegate for TabSwitcherDelegate { .ok(); } - fn dismissed(&mut self, window: &mut Window, cx: &mut Context>) { - if !self.restored_items { - for (pane, index) in self.original_items.iter() { - pane.update(cx, |this, cx| { - this.activate_item(*index, false, false, window, cx); - }) - } - } - + fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { self.tab_switcher .update(cx, |_, cx| cx.emit(DismissEvent)) .log_err(); @@ -650,7 +537,31 @@ impl PickerDelegate for TabSwitcherDelegate { }; let label = tab_match.item.tab_content(params, window, cx); - let icon = tab_match.icon(&self.project, selected, window, cx); + let icon = tab_match.item.tab_icon(window, cx).map(|icon| { + let git_status_color = ItemSettings::get_global(cx) + .git_status + .then(|| { + tab_match + .item + .project_path(cx) + .as_ref() + .and_then(|path| { + let project = self.project.read(cx); + let entry = project.entry_for_path(path, cx)?; + let git_status = project + .project_path_git_status(path, cx) + .map(|status| status.summary()) + .unwrap_or_default(); + Some((entry, git_status)) + }) + .map(|(entry, git_status)| { + entry_git_aware_label_color(git_status, entry.is_ignored, selected) + }) + }) + .flatten(); + + icon.color(git_status_color.unwrap_or_default()) + }); let indicator = render_item_indicator(tab_match.item.boxed_clone(), cx); let indicator_color = if let Some(ref indicator) = indicator { @@ -692,7 +603,7 @@ impl PickerDelegate for TabSwitcherDelegate { .inset(true) .toggle_state(selected) .child(h_flex().w_full().child(label)) - .start_slot::(icon) + .start_slot::(icon) .map(|el| { if self.selected_index == ix { el.end_slot::(close_button) diff --git a/crates/task/src/shell_builder.rs b/crates/task/src/shell_builder.rs index de4ddc00f4..770312bafc 100644 --- a/crates/task/src/shell_builder.rs +++ b/crates/task/src/shell_builder.rs @@ -1,40 +1,26 @@ use crate::Shell; -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] -pub enum ShellKind { +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +enum ShellKind { #[default] Posix, - Csh, - Fish, Powershell, Nushell, Cmd, } impl ShellKind { - pub fn system() -> Self { - Self::new(&system_shell()) - } - - pub fn new(program: &str) -> Self { - #[cfg(windows)] - let (_, program) = program.rsplit_once('\\').unwrap_or(("", program)); - #[cfg(not(windows))] - let (_, program) = program.rsplit_once('/').unwrap_or(("", program)); + fn new(program: &str) -> Self { if program == "powershell" - || program == "powershell.exe" + || program.ends_with("powershell.exe") || program == "pwsh" - || program == "pwsh.exe" + || program.ends_with("pwsh.exe") { ShellKind::Powershell - } else if program == "cmd" || program == "cmd.exe" { + } else if program == "cmd" || program.ends_with("cmd.exe") { ShellKind::Cmd } else if program == "nu" { ShellKind::Nushell - } else if program == "fish" { - ShellKind::Fish - } else if program == "csh" { - ShellKind::Csh } else { // Someother shell detected, the user might install and use a // unix-like shell. @@ -47,8 +33,6 @@ impl ShellKind { Self::Powershell => Self::to_powershell_variable(input), Self::Cmd => Self::to_cmd_variable(input), Self::Posix => input.to_owned(), - Self::Fish => input.to_owned(), - Self::Csh => input.to_owned(), Self::Nushell => Self::to_nushell_variable(input), } } @@ -169,7 +153,7 @@ impl ShellKind { match self { ShellKind::Powershell => vec!["-C".to_owned(), combined_command], ShellKind::Cmd => vec!["/C".to_owned(), combined_command], - ShellKind::Posix | ShellKind::Nushell | ShellKind::Fish | ShellKind::Csh => interactive + ShellKind::Posix | ShellKind::Nushell => interactive .then(|| "-i".to_owned()) .into_iter() .chain(["-c".to_owned(), combined_command]) @@ -200,14 +184,19 @@ pub struct ShellBuilder { kind: ShellKind, } +pub static DEFAULT_REMOTE_SHELL: &str = "\"${SHELL:-sh}\""; + impl ShellBuilder { /// Create a new ShellBuilder as configured. - pub fn new(remote_system_shell: Option<&str>, shell: &Shell) -> Self { + pub fn new(is_local: bool, shell: &Shell) -> Self { let (program, args) = match shell { - Shell::System => match remote_system_shell { - Some(remote_shell) => (remote_shell.to_string(), Vec::new()), - None => (system_shell(), Vec::new()), - }, + Shell::System => { + if is_local { + (system_shell(), Vec::new()) + } else { + (DEFAULT_REMOTE_SHELL.to_string(), Vec::new()) + } + } Shell::Program(shell) => (shell.clone(), Vec::new()), Shell::WithArguments { program, args, .. } => (program.clone(), args.clone()), }; @@ -223,7 +212,6 @@ impl ShellBuilder { self.interactive = false; self } - /// Returns the label to show in the terminal tab pub fn command_label(&self, command_label: &str) -> String { match self.kind { @@ -233,7 +221,7 @@ impl ShellBuilder { ShellKind::Cmd => { format!("{} /C '{}'", self.program, command_label) } - ShellKind::Posix | ShellKind::Nushell | ShellKind::Fish | ShellKind::Csh => { + ShellKind::Posix | ShellKind::Nushell => { let interactivity = self.interactive.then_some("-i ").unwrap_or_default(); format!( "{} {interactivity}-c '$\"{}\"'", @@ -246,7 +234,7 @@ impl ShellBuilder { pub fn build( mut self, task_command: Option, - task_args: &[String], + task_args: &Vec, ) -> (String, Vec) { if let Some(task_command) = task_command { let combined_command = task_args.iter().fold(task_command, |mut command, arg| { @@ -270,11 +258,11 @@ mod test { #[test] fn test_nu_shell_variable_substitution() { let shell = Shell::Program("nu".to_owned()); - let shell_builder = ShellBuilder::new(None, &shell); + let shell_builder = ShellBuilder::new(true, &shell); let (program, args) = shell_builder.build( Some("echo".into()), - &[ + &vec![ "${hello}".to_string(), "$world".to_string(), "nothing".to_string(), diff --git a/crates/task/src/task.rs b/crates/task/src/task.rs index eb9e59f087..85e654eff4 100644 --- a/crates/task/src/task.rs +++ b/crates/task/src/task.rs @@ -22,7 +22,7 @@ pub use debug_format::{ AttachRequest, BuildTaskDefinition, DebugRequest, DebugScenario, DebugTaskFile, LaunchRequest, Request, TcpArgumentsTemplate, ZedDebugConfig, }; -pub use shell_builder::{ShellBuilder, ShellKind}; +pub use shell_builder::{DEFAULT_REMOTE_SHELL, ShellBuilder}; pub use task_template::{ DebugArgsRequest, HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates, substitute_variables_in_map, substitute_variables_in_str, diff --git a/crates/ui/src/utils/apca_contrast.rs b/crates/terminal_view/src/color_contrast.rs similarity index 100% rename from crates/ui/src/utils/apca_contrast.rs rename to crates/terminal_view/src/color_contrast.rs diff --git a/crates/terminal_view/src/persistence.rs b/crates/terminal_view/src/persistence.rs index c7ebd314e4..b93b267f58 100644 --- a/crates/terminal_view/src/persistence.rs +++ b/crates/terminal_view/src/persistence.rs @@ -9,11 +9,7 @@ use std::path::{Path, PathBuf}; use ui::{App, Context, Pixels, Window}; use util::ResultExt as _; -use db::{ - query, - sqlez::{domain::Domain, statement::Statement, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, -}; +use db::{define_connection, query, sqlez::statement::Statement, sqlez_macros::sql}; use workspace::{ ItemHandle, ItemId, Member, Pane, PaneAxis, PaneGroup, SerializableItem as _, Workspace, WorkspaceDb, WorkspaceId, @@ -379,13 +375,9 @@ impl<'de> Deserialize<'de> for SerializedAxis { } } -pub struct TerminalDb(ThreadSafeConnection); - -impl Domain for TerminalDb { - const NAME: &str = stringify!(TerminalDb); - - const MIGRATIONS: &[&str] = &[ - sql!( +define_connection! { + pub static ref TERMINAL_DB: TerminalDb = + &[sql!( CREATE TABLE terminals ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -422,8 +414,6 @@ impl Domain for TerminalDb { ]; } -db::static_connection!(TERMINAL_DB, TerminalDb, [WorkspaceDb]); - impl TerminalDb { query! { pub async fn update_workspace_id( diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index fe3301fb89..c2fbeb7ee6 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -1,3 +1,4 @@ +use crate::color_contrast; use editor::{CursorLayout, HighlightedRange, HighlightedRangeLine}; use gpui::{ AbsoluteLength, AnyElement, App, AvailableSpace, Bounds, ContentMask, Context, DispatchPhase, @@ -26,7 +27,6 @@ use terminal::{ terminal_settings::TerminalSettings, }; use theme::{ActiveTheme, Theme, ThemeSettings}; -use ui::utils::ensure_minimum_contrast; use ui::{ParentElement, Tooltip}; use util::ResultExt; use workspace::Workspace; @@ -534,7 +534,7 @@ impl TerminalElement { // Only apply contrast adjustment to non-decorative characters if !Self::is_decorative_character(indexed.c) { - fg = ensure_minimum_contrast(fg, bg, minimum_contrast); + fg = color_contrast::ensure_minimum_contrast(fg, bg, minimum_contrast); } // Ghostty uses (175/255) as the multiplier (~0.69), Alacritty uses 0.66, Kitty @@ -1598,7 +1598,6 @@ pub fn convert_color(fg: &terminal::alacritty_terminal::vte::ansi::Color, theme: mod tests { use super::*; use gpui::{AbsoluteLength, Hsla, font}; - use ui::utils::apca_contrast; #[test] fn test_is_decorative_character() { @@ -1714,7 +1713,7 @@ mod tests { }; // Should have poor contrast - let actual_contrast = apca_contrast(white_fg, light_gray_bg).abs(); + let actual_contrast = color_contrast::apca_contrast(white_fg, light_gray_bg).abs(); assert!( actual_contrast < 30.0, "White on light gray should have poor APCA contrast: {}", @@ -1722,12 +1721,12 @@ mod tests { ); // After adjustment with minimum APCA contrast of 45, should be darker - let adjusted = ensure_minimum_contrast(white_fg, light_gray_bg, 45.0); + let adjusted = color_contrast::ensure_minimum_contrast(white_fg, light_gray_bg, 45.0); assert!( adjusted.l < white_fg.l, "Adjusted color should be darker than original" ); - let adjusted_contrast = apca_contrast(adjusted, light_gray_bg).abs(); + let adjusted_contrast = color_contrast::apca_contrast(adjusted, light_gray_bg).abs(); assert!(adjusted_contrast >= 45.0, "Should meet minimum contrast"); // Test case 2: Dark colors (poor contrast) @@ -1745,7 +1744,7 @@ mod tests { }; // Should have poor contrast - let actual_contrast = apca_contrast(black_fg, dark_gray_bg).abs(); + let actual_contrast = color_contrast::apca_contrast(black_fg, dark_gray_bg).abs(); assert!( actual_contrast < 30.0, "Black on dark gray should have poor APCA contrast: {}", @@ -1753,16 +1752,16 @@ mod tests { ); // After adjustment with minimum APCA contrast of 45, should be lighter - let adjusted = ensure_minimum_contrast(black_fg, dark_gray_bg, 45.0); + let adjusted = color_contrast::ensure_minimum_contrast(black_fg, dark_gray_bg, 45.0); assert!( adjusted.l > black_fg.l, "Adjusted color should be lighter than original" ); - let adjusted_contrast = apca_contrast(adjusted, dark_gray_bg).abs(); + let adjusted_contrast = color_contrast::apca_contrast(adjusted, dark_gray_bg).abs(); assert!(adjusted_contrast >= 45.0, "Should meet minimum contrast"); // Test case 3: Already good contrast - let good_contrast = ensure_minimum_contrast(black_fg, white_fg, 45.0); + let good_contrast = color_contrast::ensure_minimum_contrast(black_fg, white_fg, 45.0); assert_eq!( good_contrast, black_fg, "Good contrast should not be adjusted" @@ -1789,11 +1788,11 @@ mod tests { }; // With minimum contrast of 0.0, no adjustment should happen - let no_adjust = ensure_minimum_contrast(white_fg, white_bg, 0.0); + let no_adjust = color_contrast::ensure_minimum_contrast(white_fg, white_bg, 0.0); assert_eq!(no_adjust, white_fg, "No adjustment with min_contrast 0.0"); // With minimum APCA contrast of 15, it should adjust to a darker color - let adjusted = ensure_minimum_contrast(white_fg, white_bg, 15.0); + let adjusted = color_contrast::ensure_minimum_contrast(white_fg, white_bg, 15.0); assert!( adjusted.l < white_fg.l, "White on white should become darker, got l={}", @@ -1801,7 +1800,7 @@ mod tests { ); // Verify the contrast is now acceptable - let new_contrast = apca_contrast(adjusted, white_bg).abs(); + let new_contrast = color_contrast::apca_contrast(adjusted, white_bg).abs(); assert!( new_contrast >= 15.0, "Adjusted APCA contrast {} should be >= 15.0", diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 6b17911487..f40c4870f1 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -481,17 +481,14 @@ impl TerminalPanel { window: &mut Window, cx: &mut Context, ) -> Task>> { - let Ok((ssh_client, false)) = self.workspace.update(cx, |workspace, cx| { - let project = workspace.project().read(cx); - ( - project.ssh_client().and_then(|it| it.read(cx).ssh_info()), - project.is_via_collab(), - ) - }) else { + let Ok(is_local) = self + .workspace + .update(cx, |workspace, cx| workspace.project().read(cx).is_local()) + else { return Task::ready(Err(anyhow!("Project is not local"))); }; - let builder = ShellBuilder::new(ssh_client.as_ref().map(|info| &*info.shell), &task.shell); + let builder = ShellBuilder::new(is_local, &task.shell); let command_label = builder.command_label(&task.command_label); let (command, args) = builder.build(task.command.clone(), &task.args); diff --git a/crates/terminal_view/src/terminal_path_like_target.rs b/crates/terminal_view/src/terminal_path_like_target.rs deleted file mode 100644 index e20df7f001..0000000000 --- a/crates/terminal_view/src/terminal_path_like_target.rs +++ /dev/null @@ -1,825 +0,0 @@ -use super::{HoverTarget, HoveredWord, TerminalView}; -use anyhow::{Context as _, Result}; -use editor::Editor; -use gpui::{App, AppContext, Context, Task, WeakEntity, Window}; -use itertools::Itertools; -use project::{Entry, Metadata}; -use std::path::PathBuf; -use terminal::PathLikeTarget; -use util::{ResultExt, debug_panic, paths::PathWithPosition}; -use workspace::{OpenOptions, OpenVisible, Workspace}; - -#[derive(Debug, Clone)] -enum OpenTarget { - Worktree(PathWithPosition, Entry), - File(PathWithPosition, Metadata), -} - -impl OpenTarget { - fn is_file(&self) -> bool { - match self { - OpenTarget::Worktree(_, entry) => entry.is_file(), - OpenTarget::File(_, metadata) => !metadata.is_dir, - } - } - - fn is_dir(&self) -> bool { - match self { - OpenTarget::Worktree(_, entry) => entry.is_dir(), - OpenTarget::File(_, metadata) => metadata.is_dir, - } - } - - fn path(&self) -> &PathWithPosition { - match self { - OpenTarget::Worktree(path, _) => path, - OpenTarget::File(path, _) => path, - } - } -} - -pub(super) fn hover_path_like_target( - workspace: &WeakEntity, - hovered_word: HoveredWord, - path_like_target: &PathLikeTarget, - cx: &mut Context, -) -> Task<()> { - let file_to_open_task = possible_open_target(workspace, path_like_target, cx); - cx.spawn(async move |terminal_view, cx| { - let file_to_open = file_to_open_task.await; - terminal_view - .update(cx, |terminal_view, _| match file_to_open { - Some(OpenTarget::File(path, _) | OpenTarget::Worktree(path, _)) => { - terminal_view.hover = Some(HoverTarget { - tooltip: path.to_string(|path| path.to_string_lossy().to_string()), - hovered_word, - }); - } - None => { - terminal_view.hover = None; - } - }) - .ok(); - }) -} - -fn possible_open_target( - workspace: &WeakEntity, - path_like_target: &PathLikeTarget, - cx: &App, -) -> Task> { - let Some(workspace) = workspace.upgrade() else { - return Task::ready(None); - }; - // We have to check for both paths, as on Unix, certain paths with positions are valid file paths too. - // We can be on FS remote part, without real FS, so cannot canonicalize or check for existence the path right away. - let mut potential_paths = Vec::new(); - let cwd = path_like_target.terminal_dir.as_ref(); - let maybe_path = &path_like_target.maybe_path; - let original_path = PathWithPosition::from_path(PathBuf::from(maybe_path)); - let path_with_position = PathWithPosition::parse_str(maybe_path); - let worktree_candidates = workspace - .read(cx) - .worktrees(cx) - .sorted_by_key(|worktree| { - let worktree_root = worktree.read(cx).abs_path(); - match cwd.and_then(|cwd| worktree_root.strip_prefix(cwd).ok()) { - Some(cwd_child) => cwd_child.components().count(), - None => usize::MAX, - } - }) - .collect::>(); - // Since we do not check paths via FS and joining, we need to strip off potential `./`, `a/`, `b/` prefixes out of it. - const GIT_DIFF_PATH_PREFIXES: &[&str] = &["a", "b"]; - for prefix_str in GIT_DIFF_PATH_PREFIXES.iter().chain(std::iter::once(&".")) { - if let Some(stripped) = original_path.path.strip_prefix(prefix_str).ok() { - potential_paths.push(PathWithPosition { - path: stripped.to_owned(), - row: original_path.row, - column: original_path.column, - }); - } - if let Some(stripped) = path_with_position.path.strip_prefix(prefix_str).ok() { - potential_paths.push(PathWithPosition { - path: stripped.to_owned(), - row: path_with_position.row, - column: path_with_position.column, - }); - } - } - - let insert_both_paths = original_path != path_with_position; - potential_paths.insert(0, original_path); - if insert_both_paths { - potential_paths.insert(1, path_with_position); - } - - // If we won't find paths "easily", we can traverse the entire worktree to look what ends with the potential path suffix. - // That will be slow, though, so do the fast checks first. - let mut worktree_paths_to_check = Vec::new(); - for worktree in &worktree_candidates { - let worktree_root = worktree.read(cx).abs_path(); - let mut paths_to_check = Vec::with_capacity(potential_paths.len()); - - for path_with_position in &potential_paths { - let path_to_check = if worktree_root.ends_with(&path_with_position.path) { - let root_path_with_position = PathWithPosition { - path: worktree_root.to_path_buf(), - row: path_with_position.row, - column: path_with_position.column, - }; - match worktree.read(cx).root_entry() { - Some(root_entry) => { - return Task::ready(Some(OpenTarget::Worktree( - root_path_with_position, - root_entry.clone(), - ))); - } - None => root_path_with_position, - } - } else { - PathWithPosition { - path: path_with_position - .path - .strip_prefix(&worktree_root) - .unwrap_or(&path_with_position.path) - .to_owned(), - row: path_with_position.row, - column: path_with_position.column, - } - }; - - if path_to_check.path.is_relative() - && let Some(entry) = worktree.read(cx).entry_for_path(&path_to_check.path) - { - return Task::ready(Some(OpenTarget::Worktree( - PathWithPosition { - path: worktree_root.join(&entry.path), - row: path_to_check.row, - column: path_to_check.column, - }, - entry.clone(), - ))); - } - - paths_to_check.push(path_to_check); - } - - if !paths_to_check.is_empty() { - worktree_paths_to_check.push((worktree.clone(), paths_to_check)); - } - } - - // Before entire worktree traversal(s), make an attempt to do FS checks if available. - let fs_paths_to_check = if workspace.read(cx).project().read(cx).is_local() { - potential_paths - .into_iter() - .flat_map(|path_to_check| { - let mut paths_to_check = Vec::new(); - let maybe_path = &path_to_check.path; - if maybe_path.starts_with("~") { - if let Some(home_path) = - maybe_path - .strip_prefix("~") - .ok() - .and_then(|stripped_maybe_path| { - Some(dirs::home_dir()?.join(stripped_maybe_path)) - }) - { - paths_to_check.push(PathWithPosition { - path: home_path, - row: path_to_check.row, - column: path_to_check.column, - }); - } - } else { - paths_to_check.push(PathWithPosition { - path: maybe_path.clone(), - row: path_to_check.row, - column: path_to_check.column, - }); - if maybe_path.is_relative() { - if let Some(cwd) = &cwd { - paths_to_check.push(PathWithPosition { - path: cwd.join(maybe_path), - row: path_to_check.row, - column: path_to_check.column, - }); - } - for worktree in &worktree_candidates { - paths_to_check.push(PathWithPosition { - path: worktree.read(cx).abs_path().join(maybe_path), - row: path_to_check.row, - column: path_to_check.column, - }); - } - } - } - paths_to_check - }) - .collect() - } else { - Vec::new() - }; - - let worktree_check_task = cx.spawn(async move |cx| { - for (worktree, worktree_paths_to_check) in worktree_paths_to_check { - let found_entry = worktree - .update(cx, |worktree, _| { - let worktree_root = worktree.abs_path(); - let traversal = worktree.traverse_from_path(true, true, false, "".as_ref()); - for entry in traversal { - if let Some(path_in_worktree) = worktree_paths_to_check - .iter() - .find(|path_to_check| entry.path.ends_with(&path_to_check.path)) - { - return Some(OpenTarget::Worktree( - PathWithPosition { - path: worktree_root.join(&entry.path), - row: path_in_worktree.row, - column: path_in_worktree.column, - }, - entry.clone(), - )); - } - } - None - }) - .ok()?; - if let Some(found_entry) = found_entry { - return Some(found_entry); - } - } - None - }); - - let fs = workspace.read(cx).project().read(cx).fs().clone(); - cx.background_spawn(async move { - for mut path_to_check in fs_paths_to_check { - if let Some(fs_path_to_check) = fs.canonicalize(&path_to_check.path).await.ok() - && let Some(metadata) = fs.metadata(&fs_path_to_check).await.ok().flatten() - { - path_to_check.path = fs_path_to_check; - return Some(OpenTarget::File(path_to_check, metadata)); - } - } - - worktree_check_task.await - }) -} - -pub(super) fn open_path_like_target( - workspace: &WeakEntity, - terminal_view: &mut TerminalView, - path_like_target: &PathLikeTarget, - window: &mut Window, - cx: &mut Context, -) { - possibly_open_target(workspace, terminal_view, path_like_target, window, cx) - .detach_and_log_err(cx) -} - -fn possibly_open_target( - workspace: &WeakEntity, - terminal_view: &mut TerminalView, - path_like_target: &PathLikeTarget, - window: &mut Window, - cx: &mut Context, -) -> Task>> { - if terminal_view.hover.is_none() { - return Task::ready(Ok(None)); - } - let workspace = workspace.clone(); - let path_like_target = path_like_target.clone(); - cx.spawn_in(window, async move |terminal_view, cx| { - let Some(open_target) = terminal_view - .update(cx, |_, cx| { - possible_open_target(&workspace, &path_like_target, cx) - })? - .await - else { - return Ok(None); - }; - - let path_to_open = open_target.path(); - let opened_items = workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_paths( - vec![path_to_open.path.clone()], - OpenOptions { - visible: Some(OpenVisible::OnlyDirectories), - ..Default::default() - }, - None, - window, - cx, - ) - }) - .context("workspace update")? - .await; - if opened_items.len() != 1 { - debug_panic!( - "Received {} items for one path {path_to_open:?}", - opened_items.len(), - ); - } - - if let Some(opened_item) = opened_items.first() { - if open_target.is_file() { - if let Some(Ok(opened_item)) = opened_item { - if let Some(row) = path_to_open.row { - let col = path_to_open.column.unwrap_or(0); - if let Some(active_editor) = opened_item.downcast::() { - active_editor - .downgrade() - .update_in(cx, |editor, window, cx| { - editor.go_to_singleton_buffer_point( - language::Point::new( - row.saturating_sub(1), - col.saturating_sub(1), - ), - window, - cx, - ) - }) - .log_err(); - } - } - return Ok(Some(open_target)); - } - } else if open_target.is_dir() { - workspace.update(cx, |workspace, cx| { - workspace.project().update(cx, |_, cx| { - cx.emit(project::Event::ActivateProjectPanel); - }) - })?; - return Ok(Some(open_target)); - } - } - Ok(None) - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::TestAppContext; - use project::{Project, terminals::TerminalKind}; - use serde_json::json; - use std::path::{Path, PathBuf}; - use terminal::{HoveredWord, alacritty_terminal::index::Point as AlacPoint}; - use util::path; - use workspace::AppState; - - async fn init_test( - app_cx: &mut TestAppContext, - trees: impl IntoIterator, - worktree_roots: impl IntoIterator, - ) -> impl AsyncFnMut(HoveredWord, PathLikeTarget) -> (Option, Option) - { - let fs = app_cx.update(AppState::test).fs.as_fake().clone(); - - app_cx.update(|cx| { - terminal::init(cx); - theme::init(theme::LoadThemes::JustBase, cx); - Project::init_settings(cx); - language::init(cx); - editor::init(cx); - }); - - for (path, tree) in trees { - fs.insert_tree(path, tree).await; - } - - let project = Project::test( - fs.clone(), - worktree_roots - .into_iter() - .map(Path::new) - .collect::>(), - app_cx, - ) - .await; - - let (workspace, cx) = - app_cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let terminal = project - .update(cx, |project, cx| { - project.create_terminal(TerminalKind::Shell(None), cx) - }) - .await - .expect("Failed to create a terminal"); - - let workspace_a = workspace.clone(); - let (terminal_view, cx) = app_cx.add_window_view(|window, cx| { - TerminalView::new( - terminal, - workspace_a.downgrade(), - None, - project.downgrade(), - window, - cx, - ) - }); - - async move |hovered_word: HoveredWord, - path_like_target: PathLikeTarget| - -> (Option, Option) { - let workspace_a = workspace.clone(); - terminal_view - .update(cx, |_, cx| { - hover_path_like_target( - &workspace_a.downgrade(), - hovered_word, - &path_like_target, - cx, - ) - }) - .await; - - let hover_target = - terminal_view.read_with(cx, |terminal_view, _| terminal_view.hover.clone()); - - let open_target = terminal_view - .update_in(cx, |terminal_view, window, cx| { - possibly_open_target( - &workspace.downgrade(), - terminal_view, - &path_like_target, - window, - cx, - ) - }) - .await - .expect("Failed to possibly open target"); - - (hover_target, open_target) - } - } - - async fn test_path_like_simple( - test_path_like: &mut impl AsyncFnMut( - HoveredWord, - PathLikeTarget, - ) -> (Option, Option), - maybe_path: &str, - tooltip: &str, - terminal_dir: Option, - file: &str, - line: u32, - ) { - let (hover_target, open_target) = test_path_like( - HoveredWord { - word: maybe_path.to_string(), - word_match: AlacPoint::default()..=AlacPoint::default(), - id: 0, - }, - PathLikeTarget { - maybe_path: maybe_path.to_string(), - terminal_dir, - }, - ) - .await; - - let Some(hover_target) = hover_target else { - assert!( - hover_target.is_some(), - "Hover target should not be `None` at {file}:{line}:" - ); - return; - }; - - assert_eq!( - hover_target.tooltip, tooltip, - "Tooltip mismatch at {file}:{line}:" - ); - assert_eq!( - hover_target.hovered_word.word, maybe_path, - "Hovered word mismatch at {file}:{line}:" - ); - - let Some(open_target) = open_target else { - assert!( - open_target.is_some(), - "Open target should not be `None` at {file}:{line}:" - ); - return; - }; - - assert_eq!( - open_target.path().path, - Path::new(tooltip), - "Open target path mismatch at {file}:{line}:" - ); - } - - macro_rules! none_or_some { - () => { - None - }; - ($some:expr) => { - Some($some) - }; - } - - macro_rules! test_path_like { - ($test_path_like:expr, $maybe_path:literal, $tooltip:literal $(, $cwd:literal)?) => { - test_path_like_simple( - &mut $test_path_like, - path!($maybe_path), - path!($tooltip), - none_or_some!($($crate::PathBuf::from(path!($cwd)))?), - std::file!(), - std::line!(), - ) - .await - }; - } - - #[doc = "test_path_likes!(, , , { $(;)+ })"] - macro_rules! test_path_likes { - ($cx:expr, $trees:expr, $worktrees:expr, { $($tests:expr;)+ }) => { { - let mut test_path_like = init_test($cx, $trees, $worktrees).await; - #[doc ="test!(, , )"] - macro_rules! test { - ($maybe_path:literal, $tooltip:literal) => { - test_path_like!(test_path_like, $maybe_path, $tooltip) - }; - ($maybe_path:literal, $tooltip:literal, $cwd:literal) => { - test_path_like!(test_path_like, $maybe_path, $tooltip, $cwd) - } - } - $($tests);+ - } } - } - - #[gpui::test] - async fn one_folder_worktree(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![( - path!("/test"), - json!({ - "lib.rs": "", - "test.rs": "", - }), - )], - vec![path!("/test")], - { - test!("lib.rs", "/test/lib.rs"); - test!("test.rs", "/test/test.rs"); - } - ) - } - - #[gpui::test] - async fn mixed_worktrees(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![ - ( - path!("/"), - json!({ - "file.txt": "", - }), - ), - ( - path!("/test"), - json!({ - "lib.rs": "", - "test.rs": "", - "file.txt": "", - }), - ), - ], - vec![path!("/file.txt"), path!("/test")], - { - test!("file.txt", "/file.txt", "/"); - test!("lib.rs", "/test/lib.rs", "/test"); - test!("test.rs", "/test/test.rs", "/test"); - test!("file.txt", "/test/file.txt", "/test"); - } - ) - } - - #[gpui::test] - async fn worktree_file_preferred(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![ - ( - path!("/"), - json!({ - "file.txt": "", - }), - ), - ( - path!("/test"), - json!({ - "file.txt": "", - }), - ), - ], - vec![path!("/test")], - { - test!("file.txt", "/test/file.txt", "/test"); - } - ) - } - - mod issues { - use super::*; - - // https://github.com/zed-industries/zed/issues/28407 - #[gpui::test] - async fn issue_28407_siblings(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![( - path!("/dir1"), - json!({ - "dir 2": { - "C.py": "" - }, - "dir 3": { - "C.py": "" - }, - }), - )], - vec![path!("/dir1")], - { - test!("C.py", "/dir1/dir 2/C.py", "/dir1"); - test!("C.py", "/dir1/dir 2/C.py", "/dir1/dir 2"); - test!("C.py", "/dir1/dir 3/C.py", "/dir1/dir 3"); - } - ) - } - - // https://github.com/zed-industries/zed/issues/28407 - // See https://github.com/zed-industries/zed/issues/34027 - // See https://github.com/zed-industries/zed/issues/33498 - #[gpui::test] - #[should_panic(expected = "Tooltip mismatch")] - async fn issue_28407_nesting(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![( - path!("/project"), - json!({ - "lib": { - "src": { - "main.rs": "" - }, - }, - "src": { - "main.rs": "" - }, - }), - )], - vec![path!("/project")], - { - // Failing currently - test!("main.rs", "/project/src/main.rs", "/project"); - test!("main.rs", "/project/src/main.rs", "/project/src"); - test!("main.rs", "/project/lib/src/main.rs", "/project/lib"); - test!("main.rs", "/project/lib/src/main.rs", "/project/lib/src"); - - test!("src/main.rs", "/project/src/main.rs", "/project"); - test!("src/main.rs", "/project/src/main.rs", "/project/src"); - // Failing currently - test!("src/main.rs", "/project/lib/src/main.rs", "/project/lib"); - // Failing currently - test!( - "src/main.rs", - "/project/lib/src/main.rs", - "/project/lib/src" - ); - - test!("lib/src/main.rs", "/project/lib/src/main.rs", "/project"); - test!( - "lib/src/main.rs", - "/project/lib/src/main.rs", - "/project/src" - ); - test!( - "lib/src/main.rs", - "/project/lib/src/main.rs", - "/project/lib" - ); - test!( - "lib/src/main.rs", - "/project/lib/src/main.rs", - "/project/lib/src" - ); - } - ) - } - - // https://github.com/zed-industries/zed/issues/28339 - #[gpui::test] - async fn issue_28339(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![( - path!("/tmp"), - json!({ - "issue28339": { - "foo": { - "bar.txt": "" - }, - }, - }), - )], - vec![path!("/tmp")], - { - test!( - "foo/./bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339" - ); - test!( - "foo/../foo/bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339" - ); - test!( - "foo/..///foo/bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339" - ); - test!( - "issue28339/../issue28339/foo/../foo/bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339" - ); - test!( - "./bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339/foo" - ); - test!( - "../foo/bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339/foo" - ); - } - ) - } - - // https://github.com/zed-industries/zed/issues/34027 - #[gpui::test] - #[should_panic(expected = "Tooltip mismatch")] - async fn issue_34027(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![( - path!("/tmp/issue34027"), - json!({ - "test.txt": "", - "foo": { - "test.txt": "", - } - }), - ),], - vec![path!("/tmp/issue34027")], - { - test!("test.txt", "/tmp/issue34027/test.txt", "/tmp/issue34027"); - test!( - "test.txt", - "/tmp/issue34027/foo/test.txt", - "/tmp/issue34027/foo" - ); - } - ) - } - - // https://github.com/zed-industries/zed/issues/34027 - #[gpui::test] - #[should_panic(expected = "Tooltip mismatch")] - async fn issue_34027_non_worktree_file(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![ - ( - path!("/"), - json!({ - "file.txt": "", - }), - ), - ( - path!("/test"), - json!({ - "file.txt": "", - }), - ), - ], - vec![path!("/test")], - { - test!("file.txt", "/file.txt", "/"); - test!("file.txt", "/test/file.txt", "/test"); - } - ) - } - } -} diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 9aa855acb7..5b4d327140 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1,21 +1,22 @@ +mod color_contrast; mod persistence; pub mod terminal_element; pub mod terminal_panel; -mod terminal_path_like_target; pub mod terminal_scrollbar; mod terminal_slash_command; pub mod terminal_tab_tooltip; use assistant_slash_command::SlashCommandRegistry; -use editor::{EditorSettings, actions::SelectAll, scroll::ScrollbarAutoHide}; +use editor::{Editor, EditorSettings, actions::SelectAll, scroll::ScrollbarAutoHide}; use gpui::{ Action, AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext, KeyDownEvent, Keystroke, MouseButton, MouseDownEvent, Pixels, Render, ScrollWheelEvent, Stateful, Styled, Subscription, Task, WeakEntity, actions, anchored, deferred, div, }; +use itertools::Itertools; use persistence::TERMINAL_DB; -use project::{Project, search::SearchQuery, terminals::TerminalKind}; +use project::{Entry, Metadata, Project, search::SearchQuery, terminals::TerminalKind}; use schemars::JsonSchema; use task::TaskId; use terminal::{ @@ -30,17 +31,16 @@ use terminal::{ }; use terminal_element::TerminalElement; use terminal_panel::TerminalPanel; -use terminal_path_like_target::{hover_path_like_target, open_path_like_target}; use terminal_scrollbar::TerminalScrollHandle; use terminal_slash_command::TerminalSlashCommand; use terminal_tab_tooltip::TerminalTooltip; use ui::{ ContextMenu, Icon, IconName, Label, Scrollbar, ScrollbarState, Tooltip, h_flex, prelude::*, }; -use util::ResultExt; +use util::{ResultExt, debug_panic, paths::PathWithPosition}; use workspace::{ - CloseActiveItem, NewCenterTerminal, NewTerminal, ToolbarItemLocation, Workspace, WorkspaceId, - delete_unloaded_items, + CloseActiveItem, NewCenterTerminal, NewTerminal, OpenOptions, OpenVisible, ToolbarItemLocation, + Workspace, WorkspaceId, delete_unloaded_items, item::{ BreadcrumbText, Item, ItemEvent, SerializableItem, TabContentParams, TabTooltipContent, }, @@ -48,6 +48,7 @@ use workspace::{ searchable::{Direction, SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle}, }; +use anyhow::Context as _; use serde::Deserialize; use settings::{Settings, SettingsStore}; use smol::Timer; @@ -63,6 +64,7 @@ use std::{ }; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); +const GIT_DIFF_PATH_PREFIXES: &[&str] = &["a", "b"]; const TERMINAL_SCROLLBAR_WIDTH: Pixels = px(12.); /// Event to transmit the scroll from the element to the view @@ -179,7 +181,6 @@ impl ContentMode { } #[derive(Debug)] -#[cfg_attr(test, derive(Clone, Eq, PartialEq))] struct HoverTarget { tooltip: String, hovered_word: HoveredWord, @@ -1065,13 +1066,37 @@ fn subscribe_for_terminal_events( .as_ref() .map(|hover| &hover.hovered_word) { - terminal_view.hover = None; - terminal_view.hover_tooltip_update = hover_path_like_target( + let valid_files_to_open_task = possible_open_target( &workspace, - hovered_word.clone(), - path_like_target, + &path_like_target.terminal_dir, + &path_like_target.maybe_path, cx, ); + let hovered_word = hovered_word.clone(); + + terminal_view.hover = None; + terminal_view.hover_tooltip_update = + cx.spawn(async move |terminal_view, cx| { + let file_to_open = valid_files_to_open_task.await; + terminal_view + .update(cx, |terminal_view, _| match file_to_open { + Some( + OpenTarget::File(path, _) + | OpenTarget::Worktree(path, _), + ) => { + terminal_view.hover = Some(HoverTarget { + tooltip: path.to_string(|path| { + path.to_string_lossy().to_string() + }), + hovered_word, + }); + } + None => { + terminal_view.hover = None; + } + }) + .ok(); + }); cx.notify(); } } @@ -1085,13 +1110,86 @@ fn subscribe_for_terminal_events( Event::Open(maybe_navigation_target) => match maybe_navigation_target { MaybeNavigationTarget::Url(url) => cx.open_url(url), - MaybeNavigationTarget::PathLike(path_like_target) => open_path_like_target( - &workspace, - terminal_view, - path_like_target, - window, - cx, - ), + + MaybeNavigationTarget::PathLike(path_like_target) => { + if terminal_view.hover.is_none() { + return; + } + let task_workspace = workspace.clone(); + let path_like_target = path_like_target.clone(); + cx.spawn_in(window, async move |terminal_view, cx| { + let open_target = terminal_view + .update(cx, |_, cx| { + possible_open_target( + &task_workspace, + &path_like_target.terminal_dir, + &path_like_target.maybe_path, + cx, + ) + })? + .await; + if let Some(open_target) = open_target { + let path_to_open = open_target.path(); + let opened_items = task_workspace + .update_in(cx, |workspace, window, cx| { + workspace.open_paths( + vec![path_to_open.path.clone()], + OpenOptions { + visible: Some(OpenVisible::OnlyDirectories), + ..Default::default() + }, + None, + window, + cx, + ) + }) + .context("workspace update")? + .await; + if opened_items.len() != 1 { + debug_panic!( + "Received {} items for one path {path_to_open:?}", + opened_items.len(), + ); + } + + if let Some(opened_item) = opened_items.first() { + if open_target.is_file() { + if let Some(Ok(opened_item)) = opened_item + && let Some(row) = path_to_open.row + { + let col = path_to_open.column.unwrap_or(0); + if let Some(active_editor) = + opened_item.downcast::() + { + active_editor + .downgrade() + .update_in(cx, |editor, window, cx| { + editor.go_to_singleton_buffer_point( + language::Point::new( + row.saturating_sub(1), + col.saturating_sub(1), + ), + window, + cx, + ) + }) + .log_err(); + } + } + } else if open_target.is_dir() { + task_workspace.update(cx, |workspace, cx| { + workspace.project().update(cx, |_, cx| { + cx.emit(project::Event::ActivateProjectPanel); + }) + })?; + } + } + } + + anyhow::Ok(()) + }) + .detach_and_log_err(cx) + } }, Event::BreadcrumbsChanged => cx.emit(ItemEvent::UpdateBreadcrumbs), Event::CloseTerminal => cx.emit(ItemEvent::CloseItem), @@ -1105,6 +1203,241 @@ fn subscribe_for_terminal_events( vec![terminal_subscription, terminal_events_subscription] } +#[derive(Debug, Clone)] +enum OpenTarget { + Worktree(PathWithPosition, Entry), + File(PathWithPosition, Metadata), +} + +impl OpenTarget { + fn is_file(&self) -> bool { + match self { + OpenTarget::Worktree(_, entry) => entry.is_file(), + OpenTarget::File(_, metadata) => !metadata.is_dir, + } + } + + fn is_dir(&self) -> bool { + match self { + OpenTarget::Worktree(_, entry) => entry.is_dir(), + OpenTarget::File(_, metadata) => metadata.is_dir, + } + } + + fn path(&self) -> &PathWithPosition { + match self { + OpenTarget::Worktree(path, _) => path, + OpenTarget::File(path, _) => path, + } + } +} + +fn possible_open_target( + workspace: &WeakEntity, + cwd: &Option, + maybe_path: &str, + cx: &App, +) -> Task> { + let Some(workspace) = workspace.upgrade() else { + return Task::ready(None); + }; + // We have to check for both paths, as on Unix, certain paths with positions are valid file paths too. + // We can be on FS remote part, without real FS, so cannot canonicalize or check for existence the path right away. + let mut potential_paths = Vec::new(); + let original_path = PathWithPosition::from_path(PathBuf::from(maybe_path)); + let path_with_position = PathWithPosition::parse_str(maybe_path); + let worktree_candidates = workspace + .read(cx) + .worktrees(cx) + .sorted_by_key(|worktree| { + let worktree_root = worktree.read(cx).abs_path(); + match cwd + .as_ref() + .and_then(|cwd| worktree_root.strip_prefix(cwd).ok()) + { + Some(cwd_child) => cwd_child.components().count(), + None => usize::MAX, + } + }) + .collect::>(); + // Since we do not check paths via FS and joining, we need to strip off potential `./`, `a/`, `b/` prefixes out of it. + for prefix_str in GIT_DIFF_PATH_PREFIXES.iter().chain(std::iter::once(&".")) { + if let Some(stripped) = original_path.path.strip_prefix(prefix_str).ok() { + potential_paths.push(PathWithPosition { + path: stripped.to_owned(), + row: original_path.row, + column: original_path.column, + }); + } + if let Some(stripped) = path_with_position.path.strip_prefix(prefix_str).ok() { + potential_paths.push(PathWithPosition { + path: stripped.to_owned(), + row: path_with_position.row, + column: path_with_position.column, + }); + } + } + + let insert_both_paths = original_path != path_with_position; + potential_paths.insert(0, original_path); + if insert_both_paths { + potential_paths.insert(1, path_with_position); + } + + // If we won't find paths "easily", we can traverse the entire worktree to look what ends with the potential path suffix. + // That will be slow, though, so do the fast checks first. + let mut worktree_paths_to_check = Vec::new(); + for worktree in &worktree_candidates { + let worktree_root = worktree.read(cx).abs_path(); + let mut paths_to_check = Vec::with_capacity(potential_paths.len()); + + for path_with_position in &potential_paths { + let path_to_check = if worktree_root.ends_with(&path_with_position.path) { + let root_path_with_position = PathWithPosition { + path: worktree_root.to_path_buf(), + row: path_with_position.row, + column: path_with_position.column, + }; + match worktree.read(cx).root_entry() { + Some(root_entry) => { + return Task::ready(Some(OpenTarget::Worktree( + root_path_with_position, + root_entry.clone(), + ))); + } + None => root_path_with_position, + } + } else { + PathWithPosition { + path: path_with_position + .path + .strip_prefix(&worktree_root) + .unwrap_or(&path_with_position.path) + .to_owned(), + row: path_with_position.row, + column: path_with_position.column, + } + }; + + if path_to_check.path.is_relative() + && let Some(entry) = worktree.read(cx).entry_for_path(&path_to_check.path) + { + return Task::ready(Some(OpenTarget::Worktree( + PathWithPosition { + path: worktree_root.join(&entry.path), + row: path_to_check.row, + column: path_to_check.column, + }, + entry.clone(), + ))); + } + + paths_to_check.push(path_to_check); + } + + if !paths_to_check.is_empty() { + worktree_paths_to_check.push((worktree.clone(), paths_to_check)); + } + } + + // Before entire worktree traversal(s), make an attempt to do FS checks if available. + let fs_paths_to_check = if workspace.read(cx).project().read(cx).is_local() { + potential_paths + .into_iter() + .flat_map(|path_to_check| { + let mut paths_to_check = Vec::new(); + let maybe_path = &path_to_check.path; + if maybe_path.starts_with("~") { + if let Some(home_path) = + maybe_path + .strip_prefix("~") + .ok() + .and_then(|stripped_maybe_path| { + Some(dirs::home_dir()?.join(stripped_maybe_path)) + }) + { + paths_to_check.push(PathWithPosition { + path: home_path, + row: path_to_check.row, + column: path_to_check.column, + }); + } + } else { + paths_to_check.push(PathWithPosition { + path: maybe_path.clone(), + row: path_to_check.row, + column: path_to_check.column, + }); + if maybe_path.is_relative() { + if let Some(cwd) = &cwd { + paths_to_check.push(PathWithPosition { + path: cwd.join(maybe_path), + row: path_to_check.row, + column: path_to_check.column, + }); + } + for worktree in &worktree_candidates { + paths_to_check.push(PathWithPosition { + path: worktree.read(cx).abs_path().join(maybe_path), + row: path_to_check.row, + column: path_to_check.column, + }); + } + } + } + paths_to_check + }) + .collect() + } else { + Vec::new() + }; + + let worktree_check_task = cx.spawn(async move |cx| { + for (worktree, worktree_paths_to_check) in worktree_paths_to_check { + let found_entry = worktree + .update(cx, |worktree, _| { + let worktree_root = worktree.abs_path(); + let traversal = worktree.traverse_from_path(true, true, false, "".as_ref()); + for entry in traversal { + if let Some(path_in_worktree) = worktree_paths_to_check + .iter() + .find(|path_to_check| entry.path.ends_with(&path_to_check.path)) + { + return Some(OpenTarget::Worktree( + PathWithPosition { + path: worktree_root.join(&entry.path), + row: path_in_worktree.row, + column: path_in_worktree.column, + }, + entry.clone(), + )); + } + } + None + }) + .ok()?; + if let Some(found_entry) = found_entry { + return Some(found_entry); + } + } + None + }); + + let fs = workspace.read(cx).project().read(cx).fs().clone(); + cx.background_spawn(async move { + for mut path_to_check in fs_paths_to_check { + if let Some(fs_path_to_check) = fs.canonicalize(&path_to_check.path).await.ok() + && let Some(metadata) = fs.metadata(&fs_path_to_check).await.ok().flatten() + { + path_to_check.path = fs_path_to_check; + return Some(OpenTarget::File(path_to_check, metadata)); + } + } + + worktree_check_task.await + }) +} + fn regex_search_for_query(query: &project::search::SearchQuery) -> Option { let str = query.as_str(); if query.is_regex() { diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs index 81817045dc..1e7bb40c40 100644 --- a/crates/ui/src/components/keybinding.rs +++ b/crates/ui/src/components/keybinding.rs @@ -1,8 +1,8 @@ use crate::PlatformStyle; use crate::{Icon, IconName, IconSize, h_flex, prelude::*}; use gpui::{ - Action, AnyElement, App, FocusHandle, Global, IntoElement, KeybindingKeystroke, Keystroke, - Modifiers, Window, relative, + Action, AnyElement, App, FocusHandle, Global, IntoElement, Keystroke, Modifiers, Window, + relative, }; use itertools::Itertools; @@ -13,7 +13,7 @@ pub struct KeyBinding { /// More than one keystroke produces a chord. /// /// This should always contain at least one keystroke. - pub keystrokes: Vec, + pub keystrokes: Vec, /// The [`PlatformStyle`] to use when displaying this keybinding. platform_style: PlatformStyle, @@ -59,7 +59,7 @@ impl KeyBinding { cx.try_global::().is_some_and(|g| g.0) } - pub fn new(keystrokes: Vec, cx: &App) -> Self { + pub fn new(keystrokes: Vec, cx: &App) -> Self { Self { keystrokes, platform_style: PlatformStyle::platform(), @@ -99,16 +99,16 @@ impl KeyBinding { } fn render_key( - key: &str, + keystroke: &Keystroke, color: Option, platform_style: PlatformStyle, size: impl Into>, ) -> AnyElement { - let key_icon = icon_for_key(key, platform_style); + let key_icon = icon_for_key(keystroke, platform_style); match key_icon { Some(icon) => KeyIcon::new(icon, color).size(size).into_any_element(), None => { - let key = util::capitalize(key); + let key = util::capitalize(&keystroke.key); Key::new(&key, color).size(size).into_any_element() } } @@ -124,7 +124,7 @@ impl RenderOnce for KeyBinding { "KEY_BINDING-{}", self.keystrokes .iter() - .map(|k| k.display_key.to_string()) + .map(|k| k.key.to_string()) .collect::>() .join(" ") ) @@ -137,7 +137,7 @@ impl RenderOnce for KeyBinding { .py_0p5() .rounded_xs() .text_color(cx.theme().colors().text_muted) - .children(render_keybinding_keystroke( + .children(render_keystroke( keystroke, color, self.size, @@ -148,8 +148,8 @@ impl RenderOnce for KeyBinding { } } -pub fn render_keybinding_keystroke( - keystroke: &KeybindingKeystroke, +pub fn render_keystroke( + keystroke: &Keystroke, color: Option, size: impl Into>, platform_style: PlatformStyle, @@ -163,39 +163,26 @@ pub fn render_keybinding_keystroke( let size = size.into(); if use_text { - let element = Key::new( - keystroke_text( - &keystroke.display_modifiers, - &keystroke.display_key, - platform_style, - vim_mode, - ), - color, - ) - .size(size) - .into_any_element(); + let element = Key::new(keystroke_text(keystroke, platform_style, vim_mode), color) + .size(size) + .into_any_element(); vec![element] } else { let mut elements = Vec::new(); elements.extend(render_modifiers( - &keystroke.display_modifiers, + &keystroke.modifiers, platform_style, color, size, true, )); - elements.push(render_key( - &keystroke.display_key, - color, - platform_style, - size, - )); + elements.push(render_key(keystroke, color, platform_style, size)); elements } } -fn icon_for_key(key: &str, platform_style: PlatformStyle) -> Option { - match key { +fn icon_for_key(keystroke: &Keystroke, platform_style: PlatformStyle) -> Option { + match keystroke.key.as_str() { "left" => Some(IconName::ArrowLeft), "right" => Some(IconName::ArrowRight), "up" => Some(IconName::ArrowUp), @@ -392,7 +379,7 @@ impl KeyIcon { /// Returns a textual representation of the key binding for the given [`Action`]. pub fn text_for_action(action: &dyn Action, window: &Window, cx: &App) -> Option { let key_binding = window.highest_precedence_binding_for_action(action)?; - Some(text_for_keybinding_keystrokes(key_binding.keystrokes(), cx)) + Some(text_for_keystrokes(key_binding.keystrokes(), cx)) } pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String { @@ -400,50 +387,22 @@ pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String { let vim_enabled = cx.try_global::().is_some(); keystrokes .iter() - .map(|keystroke| { - keystroke_text( - &keystroke.modifiers, - &keystroke.key, - platform_style, - vim_enabled, - ) - }) + .map(|keystroke| keystroke_text(keystroke, platform_style, vim_enabled)) .join(" ") } -pub fn text_for_keybinding_keystrokes(keystrokes: &[KeybindingKeystroke], cx: &App) -> String { +pub fn text_for_keystroke(keystroke: &Keystroke, cx: &App) -> String { let platform_style = PlatformStyle::platform(); let vim_enabled = cx.try_global::().is_some(); - keystrokes - .iter() - .map(|keystroke| { - keystroke_text( - &keystroke.display_modifiers, - &keystroke.display_key, - platform_style, - vim_enabled, - ) - }) - .join(" ") -} - -pub fn text_for_keystroke(modifiers: &Modifiers, key: &str, cx: &App) -> String { - let platform_style = PlatformStyle::platform(); - let vim_enabled = cx.try_global::().is_some(); - keystroke_text(modifiers, key, platform_style, vim_enabled) + keystroke_text(keystroke, platform_style, vim_enabled) } /// Returns a textual representation of the given [`Keystroke`]. -fn keystroke_text( - modifiers: &Modifiers, - key: &str, - platform_style: PlatformStyle, - vim_mode: bool, -) -> String { +fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode: bool) -> String { let mut text = String::new(); let delimiter = '-'; - if modifiers.function { + if keystroke.modifiers.function { match vim_mode { false => text.push_str("Fn"), true => text.push_str("fn"), @@ -452,7 +411,7 @@ fn keystroke_text( text.push(delimiter); } - if modifiers.control { + if keystroke.modifiers.control { match (platform_style, vim_mode) { (PlatformStyle::Mac, false) => text.push_str("Control"), (PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Ctrl"), @@ -462,7 +421,7 @@ fn keystroke_text( text.push(delimiter); } - if modifiers.platform { + if keystroke.modifiers.platform { match (platform_style, vim_mode) { (PlatformStyle::Mac, false) => text.push_str("Command"), (PlatformStyle::Mac, true) => text.push_str("cmd"), @@ -475,7 +434,7 @@ fn keystroke_text( text.push(delimiter); } - if modifiers.alt { + if keystroke.modifiers.alt { match (platform_style, vim_mode) { (PlatformStyle::Mac, false) => text.push_str("Option"), (PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Alt"), @@ -485,7 +444,7 @@ fn keystroke_text( text.push(delimiter); } - if modifiers.shift { + if keystroke.modifiers.shift { match (platform_style, vim_mode) { (_, false) => text.push_str("Shift"), (_, true) => text.push_str("shift"), @@ -494,9 +453,9 @@ fn keystroke_text( } if vim_mode { - text.push_str(key) + text.push_str(&keystroke.key) } else { - let key = match key { + let key = match keystroke.key.as_str() { "pageup" => "PageUp", "pagedown" => "PageDown", key => &util::capitalize(key), @@ -603,11 +562,9 @@ mod tests { #[test] fn test_text_for_keystroke() { - let keystroke = Keystroke::parse("cmd-c").unwrap(); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Mac, false ), @@ -615,8 +572,7 @@ mod tests { ); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Linux, false ), @@ -624,19 +580,16 @@ mod tests { ); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("cmd-c").unwrap(), PlatformStyle::Windows, false ), "Win-C".to_string() ); - let keystroke = Keystroke::parse("ctrl-alt-delete").unwrap(); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("ctrl-alt-delete").unwrap(), PlatformStyle::Mac, false ), @@ -644,8 +597,7 @@ mod tests { ); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("ctrl-alt-delete").unwrap(), PlatformStyle::Linux, false ), @@ -653,19 +605,16 @@ mod tests { ); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("ctrl-alt-delete").unwrap(), PlatformStyle::Windows, false ), "Ctrl-Alt-Delete".to_string() ); - let keystroke = Keystroke::parse("shift-pageup").unwrap(); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("shift-pageup").unwrap(), PlatformStyle::Mac, false ), @@ -673,8 +622,7 @@ mod tests { ); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("shift-pageup").unwrap(), PlatformStyle::Linux, false, ), @@ -682,8 +630,7 @@ mod tests { ); assert_eq!( keystroke_text( - &keystroke.modifiers, - &keystroke.key, + &Keystroke::parse("shift-pageup").unwrap(), PlatformStyle::Windows, false ), diff --git a/crates/ui/src/utils.rs b/crates/ui/src/utils.rs index cd7d8eb497..26a59001f6 100644 --- a/crates/ui/src/utils.rs +++ b/crates/ui/src/utils.rs @@ -3,14 +3,12 @@ use gpui::App; use theme::ActiveTheme; -mod apca_contrast; mod color_contrast; mod corner_solver; mod format_distance; mod search_input; mod with_rem_size; -pub use apca_contrast::*; pub use color_contrast::*; pub use corner_solver::{CornerSolver, inner_corner_radius}; pub use format_distance::*; diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 1192b14812..b430120314 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -166,7 +166,7 @@ impl> From for SanitizedPath { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy)] pub enum PathStyle { Posix, Windows, diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index 726022021d..2bc531268d 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -23,8 +23,6 @@ actions!( HelixInsert, /// Appends at the end of the selection. HelixAppend, - /// Goes to the location of the last modification. - HelixGotoLastModification, ] ); @@ -33,7 +31,6 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { Vim::action(editor, cx, Vim::helix_insert); Vim::action(editor, cx, Vim::helix_append); Vim::action(editor, cx, Vim::helix_yank); - Vim::action(editor, cx, Vim::helix_goto_last_modification); } impl Vim { @@ -433,15 +430,6 @@ impl Vim { }); self.switch_mode(Mode::HelixNormal, true, window, cx); } - - pub fn helix_goto_last_modification( - &mut self, - _: &HelixGotoLastModification, - window: &mut Window, - cx: &mut Context, - ) { - self.jump(".".into(), false, false, window, cx); - } } #[cfg(test)] @@ -453,7 +441,6 @@ mod test { #[gpui::test] async fn test_word_motions(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); // « // ˇ // » @@ -515,7 +502,6 @@ mod test { #[gpui::test] async fn test_delete(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); // test delete a selection cx.set_state( @@ -596,7 +582,6 @@ mod test { #[gpui::test] async fn test_f_and_t(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); cx.set_state( indoc! {" @@ -650,7 +635,6 @@ mod test { #[gpui::test] async fn test_newline_char(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); cx.set_state("aa«\nˇ»bb cc", Mode::HelixNormal); @@ -668,7 +652,6 @@ mod test { #[gpui::test] async fn test_insert_selected(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); cx.set_state( indoc! {" «The ˇ»quick brown @@ -691,7 +674,6 @@ mod test { #[gpui::test] async fn test_append(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); // test from the end of the selection cx.set_state( indoc! {" @@ -734,7 +716,6 @@ mod test { #[gpui::test] async fn test_replace(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); // No selection (single character) cx.set_state("ˇaa", Mode::HelixNormal); @@ -782,72 +763,4 @@ mod test { cx.shared_clipboard().assert_eq("worl"); cx.assert_state("hello «worlˇ»d", Mode::HelixNormal); } - #[gpui::test] - async fn test_shift_r_paste(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - // First copy some text to clipboard - cx.set_state("«hello worldˇ»", Mode::HelixNormal); - cx.simulate_keystrokes("y"); - - // Test paste with shift-r on single cursor - cx.set_state("foo ˇbar", Mode::HelixNormal); - cx.simulate_keystrokes("shift-r"); - - cx.assert_state("foo hello worldˇbar", Mode::HelixNormal); - - // Test paste with shift-r on selection - cx.set_state("foo «barˇ» baz", Mode::HelixNormal); - cx.simulate_keystrokes("shift-r"); - - cx.assert_state("foo hello worldˇ baz", Mode::HelixNormal); - } - - #[gpui::test] - async fn test_insert_mode_stickiness(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - // Make a modification at a specific location - cx.set_state("ˇhello", Mode::HelixNormal); - assert_eq!(cx.mode(), Mode::HelixNormal); - cx.simulate_keystrokes("i"); - assert_eq!(cx.mode(), Mode::Insert); - cx.simulate_keystrokes("escape"); - assert_eq!(cx.mode(), Mode::HelixNormal); - } - - #[gpui::test] - async fn test_goto_last_modification(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - // Make a modification at a specific location - cx.set_state("line one\nline ˇtwo\nline three", Mode::HelixNormal); - cx.assert_state("line one\nline ˇtwo\nline three", Mode::HelixNormal); - cx.simulate_keystrokes("i"); - cx.simulate_keystrokes("escape"); - cx.simulate_keystrokes("i"); - cx.simulate_keystrokes("m o d i f i e d space"); - cx.simulate_keystrokes("escape"); - - // TODO: this fails, because state is no longer helix - cx.assert_state( - "line one\nline modified ˇtwo\nline three", - Mode::HelixNormal, - ); - - // Move cursor away from the modification - cx.simulate_keystrokes("up"); - - // Use "g ." to go back to last modification - cx.simulate_keystrokes("g ."); - - // Verify we're back at the modification location and still in HelixNormal mode - cx.assert_state( - "line one\nline modifiedˇ two\nline three", - Mode::HelixNormal, - ); - } } diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index a54d3caa60..a2f165e9fe 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -1610,20 +1610,10 @@ fn up_down_buffer_rows( map.line_len(begin_folded_line.row()) }; - let point = DisplayPoint::new(begin_folded_line.row(), new_col); - let mut clipped_point = map.clip_point(point, bias); - - // When navigating vertically in vim mode with inlay hints present, - // we need to handle the case where clipping moves us to a different row. - // This can happen when moving down (Bias::Right) and hitting an inlay hint. - // Re-clip with opposite bias to stay on the intended line. - // - // See: https://github.com/zed-industries/zed/issues/29134 - if clipped_point.row() > point.row() { - clipped_point = map.clip_point(point, Bias::Left); - } - - (clipped_point, goal) + ( + map.clip_point(DisplayPoint::new(begin_folded_line.row(), new_col), bias), + goal, + ) } fn down_display( @@ -3852,84 +3842,6 @@ mod test { ); } - #[gpui::test] - async fn test_visual_mode_with_inlay_hints_on_empty_line(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // Test the exact scenario from issue #29134 - cx.set_state( - indoc! {" - fn main() { - let this_is_a_long_name = Vec::::new(); - let new_oneˇ = this_is_a_long_name - .iter() - .map(|i| i + 1) - .map(|i| i * 2) - .collect::>(); - } - "}, - Mode::Normal, - ); - - // Add type hint inlay on the empty line (line 3, after "this_is_a_long_name") - cx.update_editor(|editor, _window, cx| { - let snapshot = editor.buffer().read(cx).snapshot(cx); - // The empty line is at line 3 (0-indexed) - let line_start = snapshot.anchor_after(Point::new(3, 0)); - let inlay_text = ": Vec"; - let inlay = Inlay::edit_prediction(1, line_start, inlay_text); - editor.splice_inlays(&[], vec![inlay], cx); - }); - - // Enter visual mode - cx.simulate_keystrokes("v"); - cx.assert_state( - indoc! {" - fn main() { - let this_is_a_long_name = Vec::::new(); - let new_one« ˇ»= this_is_a_long_name - .iter() - .map(|i| i + 1) - .map(|i| i * 2) - .collect::>(); - } - "}, - Mode::Visual, - ); - - // Move down - should go to the beginning of line 4, not skip to line 5 - cx.simulate_keystrokes("j"); - cx.assert_state( - indoc! {" - fn main() { - let this_is_a_long_name = Vec::::new(); - let new_one« = this_is_a_long_name - ˇ» .iter() - .map(|i| i + 1) - .map(|i| i * 2) - .collect::>(); - } - "}, - Mode::Visual, - ); - - // Test with multiple movements - cx.set_state("let aˇ = 1;\nlet b = 2;\n\nlet c = 3;", Mode::Normal); - - // Add type hint on the empty line - cx.update_editor(|editor, _window, cx| { - let snapshot = editor.buffer().read(cx).snapshot(cx); - let empty_line_start = snapshot.anchor_after(Point::new(2, 0)); - let inlay_text = ": i32"; - let inlay = Inlay::edit_prediction(2, empty_line_start, inlay_text); - editor.splice_inlays(&[], vec![inlay], cx); - }); - - // Enter visual mode and move down twice - cx.simulate_keystrokes("v j j"); - cx.assert_state("let a« = 1;\nlet b = 2;\n\nˇ»let c = 3;", Mode::Visual); - } - #[gpui::test] async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; diff --git a/crates/vim/src/normal/increment.rs b/crates/vim/src/normal/increment.rs index 34ac4aab1f..1d2a4e9b61 100644 --- a/crates/vim/src/normal/increment.rs +++ b/crates/vim/src/normal/increment.rs @@ -70,19 +70,8 @@ impl Vim { } else { Point::new(row, 0) }; - let end = if row == selection.end.row { - selection.end - } else { - Point::new(row, snapshot.line_len(multi_buffer::MultiBufferRow(row))) - }; - let number_result = if !selection.is_empty() { - find_number_in_range(&snapshot, start, end) - } else { - find_number(&snapshot, start) - }; - - if let Some((range, num, radix)) = number_result { + if let Some((range, num, radix)) = find_number(&snapshot, start) { let replace = match radix { 10 => increment_decimal_string(&num, delta), 16 => increment_hex_string(&num, delta), @@ -200,90 +189,6 @@ fn increment_binary_string(num: &str, delta: i64) -> String { format!("{:0width$b}", result, width = num.len()) } -fn find_number_in_range( - snapshot: &MultiBufferSnapshot, - start: Point, - end: Point, -) -> Option<(Range, String, u32)> { - let start_offset = start.to_offset(snapshot); - let end_offset = end.to_offset(snapshot); - - let mut offset = start_offset; - - // Backward scan to find the start of the number, but stop at start_offset - for ch in snapshot.reversed_chars_at(offset) { - if ch.is_ascii_hexdigit() || ch == '-' || ch == 'b' || ch == 'x' { - if offset == 0 { - break; - } - offset -= ch.len_utf8(); - if offset < start_offset { - offset = start_offset; - break; - } - } else { - break; - } - } - - let mut begin = None; - let mut end_num = None; - let mut num = String::new(); - let mut radix = 10; - - let mut chars = snapshot.chars_at(offset).peekable(); - - while let Some(ch) = chars.next() { - if offset >= end_offset { - break; // stop at end of selection - } - - if num == "0" && ch == 'b' && chars.peek().is_some() && chars.peek().unwrap().is_digit(2) { - radix = 2; - begin = None; - num = String::new(); - } else if num == "0" - && ch == 'x' - && chars.peek().is_some() - && chars.peek().unwrap().is_ascii_hexdigit() - { - radix = 16; - begin = None; - num = String::new(); - } - - if ch.is_digit(radix) - || (begin.is_none() - && ch == '-' - && chars.peek().is_some() - && chars.peek().unwrap().is_digit(radix)) - { - if begin.is_none() { - begin = Some(offset); - } - num.push(ch); - } else if begin.is_some() { - end_num = Some(offset); - break; - } else if ch == '\n' { - break; - } - - offset += ch.len_utf8(); - } - - if let Some(begin) = begin { - let end_num = end_num.unwrap_or(offset); - Some(( - begin.to_point(snapshot)..end_num.to_point(snapshot), - num, - radix, - )) - } else { - None - } -} - fn find_number( snapshot: &MultiBufferSnapshot, start: Point, @@ -859,18 +764,4 @@ mod test { cx.simulate_keystrokes("v b ctrl-a"); cx.assert_state("let enabled = ˇOff;", Mode::Normal); } - - #[gpui::test] - async fn test_increment_visual_partial_number(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇ123").await; - cx.simulate_shared_keystrokes("v l ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {"ˇ133"}); - cx.simulate_shared_keystrokes("l v l ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {"1ˇ34"}); - cx.simulate_shared_keystrokes("shift-v y p p ctrl-v k k l ctrl-a") - .await; - cx.shared_state().await.assert_eq(indoc! {"ˇ144\n144\n144"}); - } } diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index dba003ec5f..4fbeec7236 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -203,10 +203,7 @@ impl Vim { // hook into the existing to clear out any vim search state on cmd+f or edit -> find. fn search_deploy(&mut self, _: &buffer_search::Deploy, _: &mut Window, cx: &mut Context) { - // Preserve the current mode when resetting search state - let current_mode = self.mode; self.search = Default::default(); - self.search.prior_mode = current_mode; cx.propagate(); } diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index fe4bc7433d..c0176cb12c 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -7,10 +7,8 @@ use crate::{motion::Motion, object::Object}; use anyhow::Result; use collections::HashMap; use command_palette_hooks::{CommandPaletteFilter, CommandPaletteInterceptor}; -use db::{ - sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, -}; +use db::define_connection; +use db::sqlez_macros::sql; use editor::display_map::{is_invisible, replacement}; use editor::{Anchor, ClipboardSelection, Editor, MultiBuffer, ToPoint as EditorToPoint}; use gpui::{ @@ -1670,12 +1668,8 @@ impl MarksView { } } -pub struct VimDb(ThreadSafeConnection); - -impl Domain for VimDb { - const NAME: &str = stringify!(VimDb); - - const MIGRATIONS: &[&str] = &[ +define_connection! ( + pub static ref DB: VimDb = &[ sql! ( CREATE TABLE vim_marks ( workspace_id INTEGER, @@ -1695,9 +1689,7 @@ impl Domain for VimDb { ON vim_global_marks_paths(workspace_id, mark_name); ), ]; -} - -db::static_connection!(DB, VimDb, [WorkspaceDb]); +); struct SerializedMark { path: Arc, diff --git a/crates/vim/test_data/test_increment_visual_partial_number.json b/crates/vim/test_data/test_increment_visual_partial_number.json deleted file mode 100644 index ebb4eece78..0000000000 --- a/crates/vim/test_data/test_increment_visual_partial_number.json +++ /dev/null @@ -1,20 +0,0 @@ -{"Put":{"state":"ˇ123"}} -{"Key":"v"} -{"Key":"l"} -{"Key":"ctrl-a"} -{"Get":{"state":"ˇ133","mode":"Normal"}} -{"Key":"l"} -{"Key":"v"} -{"Key":"l"} -{"Key":"ctrl-a"} -{"Get":{"state":"1ˇ34","mode":"Normal"}} -{"Key":"shift-v"} -{"Key":"y"} -{"Key":"p"} -{"Key":"p"} -{"Key":"ctrl-v"} -{"Key":"k"} -{"Key":"k"} -{"Key":"l"} -{"Key":"ctrl-a"} -{"Get":{"state":"ˇ144\n144\n144","mode":"Normal"}} diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 869aa5322e..e1bda7ad36 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -29,6 +29,7 @@ test-support = [ any_vec.workspace = true anyhow.workspace = true async-recursion.workspace = true +bincode = "1.2.1" call.workspace = true client.workspace = true clock.workspace = true @@ -79,6 +80,5 @@ project = { workspace = true, features = ["test-support"] } session = { workspace = true, features = ["test-support"] } settings = { workspace = true, features = ["test-support"] } http_client = { workspace = true, features = ["test-support"] } -pretty_assertions.workspace = true tempfile.workspace = true zlog.workspace = true diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 149a122c0c..7a8de6e910 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -915,11 +915,6 @@ impl Render for PanelButtons { .on_click({ let action = action.boxed_clone(); move |_, window, cx| { - telemetry::event!( - "Panel Button Clicked", - name = name, - toggle_state = !is_open - ); window.focus(&focus_handle); window.dispatch_action(action.boxed_clone(), cx) } diff --git a/crates/workspace/src/history_manager.rs b/crates/workspace/src/history_manager.rs index f68b58ff82..a8387369f4 100644 --- a/crates/workspace/src/history_manager.rs +++ b/crates/workspace/src/history_manager.rs @@ -5,9 +5,7 @@ use smallvec::SmallVec; use ui::App; use util::{ResultExt, paths::PathExt}; -use crate::{ - NewWindow, SerializedWorkspaceLocation, WORKSPACE_DB, WorkspaceId, path_list::PathList, -}; +use crate::{NewWindow, SerializedWorkspaceLocation, WORKSPACE_DB, WorkspaceId}; pub fn init(cx: &mut App) { let manager = cx.new(|_| HistoryManager::new()); @@ -46,13 +44,7 @@ impl HistoryManager { .unwrap_or_default() .into_iter() .rev() - .filter_map(|(id, location, paths)| { - if matches!(location, SerializedWorkspaceLocation::Local) { - Some(HistoryManagerEntry::new(id, &paths)) - } else { - None - } - }) + .map(|(id, location)| HistoryManagerEntry::new(id, &location)) .collect::>(); this.update(cx, |this, cx| { this.history = recent_folders; @@ -126,9 +118,9 @@ impl HistoryManager { } impl HistoryManagerEntry { - pub fn new(id: WorkspaceId, paths: &PathList) -> Self { - let path = paths - .paths() + pub fn new(id: WorkspaceId, location: &SerializedWorkspaceLocation) -> Self { + let path = location + .sorted_paths() .iter() .map(|path| path.compact()) .collect::>(); diff --git a/crates/workspace/src/invalid_buffer_view.rs b/crates/workspace/src/invalid_buffer_view.rs deleted file mode 100644 index b8c0db29d3..0000000000 --- a/crates/workspace/src/invalid_buffer_view.rs +++ /dev/null @@ -1,111 +0,0 @@ -use std::{path::Path, sync::Arc}; - -use gpui::{EventEmitter, FocusHandle, Focusable}; -use ui::{ - App, Button, ButtonCommon, ButtonStyle, Clickable, Context, FluentBuilder, InteractiveElement, - KeyBinding, ParentElement, Render, SharedString, Styled as _, Window, h_flex, v_flex, -}; -use zed_actions::workspace::OpenWithSystem; - -use crate::Item; - -/// A view to display when a certain buffer fails to open. -pub struct InvalidBufferView { - /// Which path was attempted to open. - pub abs_path: Arc, - /// An error message, happened when opening the buffer. - pub error: SharedString, - is_local: bool, - focus_handle: FocusHandle, -} - -impl InvalidBufferView { - pub fn new( - abs_path: &Path, - is_local: bool, - e: &anyhow::Error, - _: &mut Window, - cx: &mut App, - ) -> Self { - Self { - is_local, - abs_path: Arc::from(abs_path), - error: format!("{e}").into(), - focus_handle: cx.focus_handle(), - } - } -} - -impl Item for InvalidBufferView { - type Event = (); - - fn tab_content_text(&self, mut detail: usize, _: &App) -> SharedString { - // Ensure we always render at least the filename. - detail += 1; - - let path = self.abs_path.as_ref(); - - let mut prefix = path; - while detail > 0 { - if let Some(parent) = prefix.parent() { - prefix = parent; - detail -= 1; - } else { - break; - } - } - - let path = if detail > 0 { - path - } else { - path.strip_prefix(prefix).unwrap_or(path) - }; - - SharedString::new(path.to_string_lossy()) - } -} - -impl EventEmitter<()> for InvalidBufferView {} - -impl Focusable for InvalidBufferView { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Render for InvalidBufferView { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl gpui::IntoElement { - let abs_path = self.abs_path.clone(); - v_flex() - .size_full() - .track_focus(&self.focus_handle(cx)) - .flex_none() - .justify_center() - .overflow_hidden() - .key_context("InvalidBuffer") - .child( - h_flex().size_full().justify_center().child( - v_flex() - .justify_center() - .gap_2() - .child(h_flex().justify_center().child("Unsupported file type")) - .when(self.is_local, |contents| { - contents.child( - h_flex().justify_center().child( - Button::new("open-with-system", "Open in Default App") - .on_click(move |_, _, cx| { - cx.open_with_system(&abs_path); - }) - .style(ButtonStyle::Outlined) - .key_binding(KeyBinding::for_action( - &OpenWithSystem, - window, - cx, - )), - ), - ) - }), - ), - ) - } -} diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index db91bd82b9..5a497398f9 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -1,7 +1,6 @@ use crate::{ CollaboratorId, DelayedDebouncedEditAction, FollowableViewRegistry, ItemNavHistory, SerializableItemRegistry, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, - invalid_buffer_view::InvalidBufferView, pane::{self, Pane}, persistence::model::ItemId, searchable::SearchableItemHandle, @@ -23,7 +22,6 @@ use std::{ any::{Any, TypeId}, cell::RefCell, ops::Range, - path::Path, rc::Rc, sync::Arc, time::Duration, @@ -1163,22 +1161,6 @@ pub trait ProjectItem: Item { ) -> Self where Self: Sized; - - /// A fallback handler, which will be called after [`project::ProjectItem::try_open`] fails, - /// with the error from that failure as an argument. - /// Allows to open an item that can gracefully display and handle errors. - fn for_broken_project_item( - _abs_path: &Path, - _is_local: bool, - _e: &anyhow::Error, - _window: &mut Window, - _cx: &mut App, - ) -> Option - where - Self: Sized, - { - None - } } #[derive(Debug)] diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index fe8014d9f7..23c8c0b185 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2,7 +2,6 @@ use crate::{ CloseWindow, NewFile, NewTerminal, OpenInTerminal, OpenOptions, OpenTerminal, OpenVisible, SplitDirection, ToggleFileFinder, ToggleProjectSymbols, ToggleZoom, Workspace, WorkspaceItemBuilder, - invalid_buffer_view::InvalidBufferView, item::{ ActivateOnClose, ClosePosition, Item, ItemHandle, ItemSettings, PreviewTabsSettings, ProjectItemKind, SaveOptions, ShowCloseButton, ShowDiagnostics, TabContentParams, @@ -514,7 +513,7 @@ impl Pane { } } - fn alternate_file(&mut self, _: &AlternateFile, window: &mut Window, cx: &mut Context) { + fn alternate_file(&mut self, window: &mut Window, cx: &mut Context) { let (_, alternative) = &self.alternate_file_items; if let Some(alternative) = alternative { let existing = self @@ -788,7 +787,7 @@ impl Pane { !self.nav_history.0.lock().forward_stack.is_empty() } - pub fn navigate_backward(&mut self, _: &GoBack, window: &mut Window, cx: &mut Context) { + pub fn navigate_backward(&mut self, window: &mut Window, cx: &mut Context) { if let Some(workspace) = self.workspace.upgrade() { let pane = cx.entity().downgrade(); window.defer(cx, move |window, cx| { @@ -799,7 +798,7 @@ impl Pane { } } - fn navigate_forward(&mut self, _: &GoForward, window: &mut Window, cx: &mut Context) { + fn navigate_forward(&mut self, window: &mut Window, cx: &mut Context) { if let Some(workspace) = self.workspace.upgrade() { let pane = cx.entity().downgrade(); window.defer(cx, move |window, cx| { @@ -898,43 +897,19 @@ impl Pane { } } } - - let set_up_existing_item = - |index: usize, pane: &mut Self, window: &mut Window, cx: &mut Context| { - // If the item is already open, and the item is a preview item - // and we are not allowing items to open as preview, mark the item as persistent. - if let Some(preview_item_id) = pane.preview_item_id - && let Some(tab) = pane.items.get(index) - && tab.item_id() == preview_item_id - && !allow_preview - { - pane.set_preview_item_id(None, cx); - } - if activate { - pane.activate_item(index, focus_item, focus_item, window, cx); - } - }; - let set_up_new_item = |new_item: Box, - destination_index: Option, - pane: &mut Self, - window: &mut Window, - cx: &mut Context| { - if allow_preview { - pane.set_preview_item_id(Some(new_item.item_id()), cx); - } - pane.add_item_inner( - new_item, - true, - focus_item, - activate, - destination_index, - window, - cx, - ); - }; - if let Some((index, existing_item)) = existing_item { - set_up_existing_item(index, self, window, cx); + // If the item is already open, and the item is a preview item + // and we are not allowing items to open as preview, mark the item as persistent. + if let Some(preview_item_id) = self.preview_item_id + && let Some(tab) = self.items.get(index) + && tab.item_id() == preview_item_id + && !allow_preview + { + self.set_preview_item_id(None, cx); + } + if activate { + self.activate_item(index, focus_item, focus_item, window, cx); + } existing_item } else { // If the item is being opened as preview and we have an existing preview tab, @@ -946,46 +921,21 @@ impl Pane { }; let new_item = build_item(self, window, cx); - // A special case that won't ever get a `project_entry_id` but has to be deduplicated nonetheless. - if let Some(invalid_buffer_view) = new_item.downcast::() { - let mut already_open_view = None; - let mut views_to_close = HashSet::default(); - for existing_error_view in self - .items_of_type::() - .filter(|item| item.read(cx).abs_path == invalid_buffer_view.read(cx).abs_path) - { - if already_open_view.is_none() - && existing_error_view.read(cx).error == invalid_buffer_view.read(cx).error - { - already_open_view = Some(existing_error_view); - } else { - views_to_close.insert(existing_error_view.item_id()); - } - } - let resulting_item = match already_open_view { - Some(already_open_view) => { - if let Some(index) = self.index_for_item_id(already_open_view.item_id()) { - set_up_existing_item(index, self, window, cx); - } - Box::new(already_open_view) as Box<_> - } - None => { - set_up_new_item(new_item.clone(), destination_index, self, window, cx); - new_item - } - }; - - self.close_items(window, cx, SaveIntent::Skip, |existing_item| { - views_to_close.contains(&existing_item) - }) - .detach(); - - resulting_item - } else { - set_up_new_item(new_item.clone(), destination_index, self, window, cx); - new_item + if allow_preview { + self.set_preview_item_id(Some(new_item.item_id()), cx); } + self.add_item_inner( + new_item.clone(), + true, + focus_item, + activate, + destination_index, + window, + cx, + ); + + new_item } } @@ -1283,9 +1233,9 @@ impl Pane { } } - pub fn activate_previous_item( + pub fn activate_prev_item( &mut self, - _: &ActivatePreviousItem, + activate_pane: bool, window: &mut Window, cx: &mut Context, ) { @@ -1295,12 +1245,12 @@ impl Pane { } else if !self.items.is_empty() { index = self.items.len() - 1; } - self.activate_item(index, true, true, window, cx); + self.activate_item(index, activate_pane, activate_pane, window, cx); } pub fn activate_next_item( &mut self, - _: &ActivateNextItem, + activate_pane: bool, window: &mut Window, cx: &mut Context, ) { @@ -1310,15 +1260,10 @@ impl Pane { } else { index = 0; } - self.activate_item(index, true, true, window, cx); + self.activate_item(index, activate_pane, activate_pane, window, cx); } - pub fn swap_item_left( - &mut self, - _: &SwapItemLeft, - window: &mut Window, - cx: &mut Context, - ) { + pub fn swap_item_left(&mut self, window: &mut Window, cx: &mut Context) { let index = self.active_item_index; if index == 0 { return; @@ -1328,14 +1273,9 @@ impl Pane { self.activate_item(index - 1, true, true, window, cx); } - pub fn swap_item_right( - &mut self, - _: &SwapItemRight, - window: &mut Window, - cx: &mut Context, - ) { + pub fn swap_item_right(&mut self, window: &mut Window, cx: &mut Context) { let index = self.active_item_index; - if index + 1 >= self.items.len() { + if index + 1 == self.items.len() { return; } @@ -1343,16 +1283,6 @@ impl Pane { self.activate_item(index + 1, true, true, window, cx); } - pub fn activate_last_item( - &mut self, - _: &ActivateLastItem, - window: &mut Window, - cx: &mut Context, - ) { - let index = self.items.len().saturating_sub(1); - self.activate_item(index, true, true, window, cx); - } - pub fn close_active_item( &mut self, action: &CloseActiveItem, @@ -2901,9 +2831,7 @@ impl Pane { .on_click({ let entity = cx.entity(); move |_, window, cx| { - entity.update(cx, |pane, cx| { - pane.navigate_backward(&Default::default(), window, cx) - }) + entity.update(cx, |pane, cx| pane.navigate_backward(window, cx)) } }) .disabled(!self.can_navigate_backward()) @@ -2918,11 +2846,7 @@ impl Pane { .icon_size(IconSize::Small) .on_click({ let entity = cx.entity(); - move |_, window, cx| { - entity.update(cx, |pane, cx| { - pane.navigate_forward(&Default::default(), window, cx) - }) - } + move |_, window, cx| entity.update(cx, |pane, cx| pane.navigate_forward(window, cx)) }) .disabled(!self.can_navigate_forward()) .tooltip({ @@ -3554,6 +3478,9 @@ impl Render for Pane { .size_full() .flex_none() .overflow_hidden() + .on_action(cx.listener(|pane, _: &AlternateFile, window, cx| { + pane.alternate_file(window, cx); + })) .on_action( cx.listener(|pane, _: &SplitLeft, _, cx| pane.split(SplitDirection::Left, cx)), ) @@ -3570,6 +3497,12 @@ impl Render for Pane { .on_action( cx.listener(|pane, _: &SplitDown, _, cx| pane.split(SplitDirection::Down, cx)), ) + .on_action( + cx.listener(|pane, _: &GoBack, window, cx| pane.navigate_backward(window, cx)), + ) + .on_action( + cx.listener(|pane, _: &GoForward, window, cx| pane.navigate_forward(window, cx)), + ) .on_action(cx.listener(|_, _: &JoinIntoNext, _, cx| { cx.emit(Event::JoinIntoNext); })) @@ -3577,8 +3510,6 @@ impl Render for Pane { cx.emit(Event::JoinAll); })) .on_action(cx.listener(Pane::toggle_zoom)) - .on_action(cx.listener(Self::navigate_backward)) - .on_action(cx.listener(Self::navigate_forward)) .on_action( cx.listener(|pane: &mut Pane, action: &ActivateItem, window, cx| { pane.activate_item( @@ -3590,14 +3521,33 @@ impl Render for Pane { ); }), ) - .on_action(cx.listener(Self::alternate_file)) - .on_action(cx.listener(Self::activate_last_item)) - .on_action(cx.listener(Self::activate_previous_item)) - .on_action(cx.listener(Self::activate_next_item)) - .on_action(cx.listener(Self::swap_item_left)) - .on_action(cx.listener(Self::swap_item_right)) - .on_action(cx.listener(Self::toggle_pin_tab)) - .on_action(cx.listener(Self::unpin_all_tabs)) + .on_action( + cx.listener(|pane: &mut Pane, _: &ActivateLastItem, window, cx| { + pane.activate_item(pane.items.len().saturating_sub(1), true, true, window, cx); + }), + ) + .on_action( + cx.listener(|pane: &mut Pane, _: &ActivatePreviousItem, window, cx| { + pane.activate_prev_item(true, window, cx); + }), + ) + .on_action( + cx.listener(|pane: &mut Pane, _: &ActivateNextItem, window, cx| { + pane.activate_next_item(true, window, cx); + }), + ) + .on_action( + cx.listener(|pane, _: &SwapItemLeft, window, cx| pane.swap_item_left(window, cx)), + ) + .on_action( + cx.listener(|pane, _: &SwapItemRight, window, cx| pane.swap_item_right(window, cx)), + ) + .on_action(cx.listener(|pane, action, window, cx| { + pane.toggle_pin_tab(action, window, cx); + })) + .on_action(cx.listener(|pane, action, window, cx| { + pane.unpin_all_tabs(action, window, cx); + })) .when(PreviewTabsSettings::get_global(cx).enabled, |this| { this.on_action(cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, _, cx| { if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) { @@ -6452,57 +6402,6 @@ mod tests { .unwrap(); } - #[gpui::test] - async fn test_item_swapping_actions(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, None, cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - assert_item_labels(&pane, [], cx); - - // Test that these actions do not panic - pane.update_in(cx, |pane, window, cx| { - pane.swap_item_right(&Default::default(), window, cx); - }); - - pane.update_in(cx, |pane, window, cx| { - pane.swap_item_left(&Default::default(), window, cx); - }); - - add_labeled_item(&pane, "A", false, cx); - add_labeled_item(&pane, "B", false, cx); - add_labeled_item(&pane, "C", false, cx); - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.swap_item_right(&Default::default(), window, cx); - }); - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.swap_item_left(&Default::default(), window, cx); - }); - assert_item_labels(&pane, ["A", "C*", "B"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.swap_item_left(&Default::default(), window, cx); - }); - assert_item_labels(&pane, ["C*", "A", "B"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.swap_item_left(&Default::default(), window, cx); - }); - assert_item_labels(&pane, ["C*", "A", "B"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.swap_item_right(&Default::default(), window, cx); - }); - assert_item_labels(&pane, ["A", "C*", "B"], cx); - } - fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { let settings_store = SettingsStore::test(cx); diff --git a/crates/workspace/src/path_list.rs b/crates/workspace/src/path_list.rs deleted file mode 100644 index cf463e6b22..0000000000 --- a/crates/workspace/src/path_list.rs +++ /dev/null @@ -1,123 +0,0 @@ -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; - -use util::paths::SanitizedPath; - -/// A list of absolute paths, in a specific order. -/// -/// The paths are stored in lexicographic order, so that they can be compared to -/// other path lists without regard to the order of the paths. -#[derive(Default, PartialEq, Eq, Debug, Clone)] -pub struct PathList { - paths: Arc<[PathBuf]>, - order: Arc<[usize]>, -} - -#[derive(Debug)] -pub struct SerializedPathList { - pub paths: String, - pub order: String, -} - -impl PathList { - pub fn new>(paths: &[P]) -> Self { - let mut indexed_paths: Vec<(usize, PathBuf)> = paths - .iter() - .enumerate() - .map(|(ix, path)| (ix, SanitizedPath::from(path).into())) - .collect(); - indexed_paths.sort_by(|(_, a), (_, b)| a.cmp(b)); - let order = indexed_paths.iter().map(|e| e.0).collect::>().into(); - let paths = indexed_paths - .into_iter() - .map(|e| e.1) - .collect::>() - .into(); - Self { order, paths } - } - - pub fn is_empty(&self) -> bool { - self.paths.is_empty() - } - - pub fn paths(&self) -> &[PathBuf] { - self.paths.as_ref() - } - - pub fn order(&self) -> &[usize] { - self.order.as_ref() - } - - pub fn is_lexicographically_ordered(&self) -> bool { - self.order.iter().enumerate().all(|(i, &j)| i == j) - } - - pub fn deserialize(serialized: &SerializedPathList) -> Self { - let mut paths: Vec = if serialized.paths.is_empty() { - Vec::new() - } else { - serialized.paths.split('\n').map(PathBuf::from).collect() - }; - - let mut order: Vec = serialized - .order - .split(',') - .filter_map(|s| s.parse().ok()) - .collect(); - - if !paths.is_sorted() || order.len() != paths.len() { - order = (0..paths.len()).collect(); - paths.sort(); - } - - Self { - paths: paths.into(), - order: order.into(), - } - } - - pub fn serialize(&self) -> SerializedPathList { - use std::fmt::Write as _; - - let mut paths = String::new(); - for path in self.paths.iter() { - if !paths.is_empty() { - paths.push('\n'); - } - paths.push_str(&path.to_string_lossy()); - } - - let mut order = String::new(); - for ix in self.order.iter() { - if !order.is_empty() { - order.push(','); - } - write!(&mut order, "{}", *ix).unwrap(); - } - SerializedPathList { paths, order } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_path_list() { - let list1 = PathList::new(&["a/d", "a/c"]); - let list2 = PathList::new(&["a/c", "a/d"]); - - assert_eq!(list1.paths(), list2.paths()); - assert_ne!(list1, list2); - assert_eq!(list1.order(), &[1, 0]); - assert_eq!(list2.order(), &[0, 1]); - - let list1_deserialized = PathList::deserialize(&list1.serialize()); - assert_eq!(list1_deserialized, list1); - - let list2_deserialized = PathList::deserialize(&list2.serialize()); - assert_eq!(list2_deserialized, list2); - } -} diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index c4ba93bcec..b2d1340a7b 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -9,17 +9,15 @@ use std::{ }; use anyhow::{Context as _, Result, bail}; -use collections::HashMap; -use db::{ - query, - sqlez::{connection::Connection, domain::Domain}, - sqlez_macros::sql, -}; +use client::DevServerProjectId; +use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql}; use gpui::{Axis, Bounds, Task, WindowBounds, WindowId, point, size}; +use itertools::Itertools; use project::debugger::breakpoint_store::{BreakpointState, SourceBreakpoint}; use language::{LanguageName, Toolchain}; use project::WorktreeId; +use remote::ssh_session::SshProjectId; use sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::{SqlType, Statement}, @@ -30,17 +28,14 @@ use ui::{App, px}; use util::{ResultExt, maybe}; use uuid::Uuid; -use crate::{ - WorkspaceId, - path_list::{PathList, SerializedPathList}, -}; +use crate::WorkspaceId; use model::{ - GroupId, ItemId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, - SerializedSshConnection, SerializedWorkspace, SshConnectionId, + GroupId, ItemId, LocalPaths, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, + SerializedSshProject, SerializedWorkspace, }; -use self::model::{DockStructure, SerializedWorkspaceLocation}; +use self::model::{DockStructure, LocalPathsOrder, SerializedWorkspaceLocation}; #[derive(Copy, Clone, Debug, PartialEq)] pub(crate) struct SerializedAxis(pub(crate) gpui::Axis); @@ -279,189 +274,247 @@ impl sqlez::bindable::Bind for SerializedPixels { } } -pub struct WorkspaceDb(ThreadSafeConnection); +define_connection! { + // Current schema shape using pseudo-rust syntax: + // + // workspaces( + // workspace_id: usize, // Primary key for workspaces + // local_paths: Bincode>, + // local_paths_order: Bincode>, + // dock_visible: bool, // Deprecated + // dock_anchor: DockAnchor, // Deprecated + // dock_pane: Option, // Deprecated + // left_sidebar_open: boolean, + // timestamp: String, // UTC YYYY-MM-DD HH:MM:SS + // window_state: String, // WindowBounds Discriminant + // window_x: Option, // WindowBounds::Fixed RectF x + // window_y: Option, // WindowBounds::Fixed RectF y + // window_width: Option, // WindowBounds::Fixed RectF width + // window_height: Option, // WindowBounds::Fixed RectF height + // display: Option, // Display id + // fullscreen: Option, // Is the window fullscreen? + // centered_layout: Option, // Is the Centered Layout mode activated? + // session_id: Option, // Session id + // window_id: Option, // Window Id + // ) + // + // pane_groups( + // group_id: usize, // Primary key for pane_groups + // workspace_id: usize, // References workspaces table + // parent_group_id: Option, // None indicates that this is the root node + // position: Option, // None indicates that this is the root node + // axis: Option, // 'Vertical', 'Horizontal' + // flexes: Option>, // A JSON array of floats + // ) + // + // panes( + // pane_id: usize, // Primary key for panes + // workspace_id: usize, // References workspaces table + // active: bool, + // ) + // + // center_panes( + // pane_id: usize, // Primary key for center_panes + // parent_group_id: Option, // References pane_groups. If none, this is the root + // position: Option, // None indicates this is the root + // ) + // + // CREATE TABLE items( + // item_id: usize, // This is the item's view id, so this is not unique + // workspace_id: usize, // References workspaces table + // pane_id: usize, // References panes table + // kind: String, // Indicates which view this connects to. This is the key in the item_deserializers global + // position: usize, // Position of the item in the parent pane. This is equivalent to panes' position column + // active: bool, // Indicates if this item is the active one in the pane + // preview: bool // Indicates if this item is a preview item + // ) + // + // CREATE TABLE breakpoints( + // workspace_id: usize Foreign Key, // References workspace table + // path: PathBuf, // The absolute path of the file that this breakpoint belongs to + // breakpoint_location: Vec, // A list of the locations of breakpoints + // kind: int, // The kind of breakpoint (standard, log) + // log_message: String, // log message for log breakpoints, otherwise it's Null + // ) + pub static ref DB: WorkspaceDb<()> = + &[ + sql!( + CREATE TABLE workspaces( + workspace_id INTEGER PRIMARY KEY, + workspace_location BLOB UNIQUE, + dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. + dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. + dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. + left_sidebar_open INTEGER, // Boolean + timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, + FOREIGN KEY(dock_pane) REFERENCES panes(pane_id) + ) STRICT; -impl Domain for WorkspaceDb { - const NAME: &str = stringify!(WorkspaceDb); + CREATE TABLE pane_groups( + group_id INTEGER PRIMARY KEY, + workspace_id INTEGER NOT NULL, + parent_group_id INTEGER, // NULL indicates that this is a root node + position INTEGER, // NULL indicates that this is a root node + axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal' + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE, + FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE + ) STRICT; - const MIGRATIONS: &[&str] = &[ - sql!( - CREATE TABLE workspaces( - workspace_id INTEGER PRIMARY KEY, - workspace_location BLOB UNIQUE, - dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. - dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. - dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. - left_sidebar_open INTEGER, // Boolean - timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, - FOREIGN KEY(dock_pane) REFERENCES panes(pane_id) - ) STRICT; + CREATE TABLE panes( + pane_id INTEGER PRIMARY KEY, + workspace_id INTEGER NOT NULL, + active INTEGER NOT NULL, // Boolean + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE + ) STRICT; - CREATE TABLE pane_groups( - group_id INTEGER PRIMARY KEY, - workspace_id INTEGER NOT NULL, - parent_group_id INTEGER, // NULL indicates that this is a root node - position INTEGER, // NULL indicates that this is a root node - axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal' - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ON UPDATE CASCADE, - FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE - ) STRICT; + CREATE TABLE center_panes( + pane_id INTEGER PRIMARY KEY, + parent_group_id INTEGER, // NULL means that this is a root pane + position INTEGER, // NULL means that this is a root pane + FOREIGN KEY(pane_id) REFERENCES panes(pane_id) + ON DELETE CASCADE, + FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE + ) STRICT; - CREATE TABLE panes( - pane_id INTEGER PRIMARY KEY, - workspace_id INTEGER NOT NULL, - active INTEGER NOT NULL, // Boolean - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ON UPDATE CASCADE - ) STRICT; - - CREATE TABLE center_panes( - pane_id INTEGER PRIMARY KEY, - parent_group_id INTEGER, // NULL means that this is a root pane - position INTEGER, // NULL means that this is a root pane - FOREIGN KEY(pane_id) REFERENCES panes(pane_id) - ON DELETE CASCADE, - FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE - ) STRICT; - - CREATE TABLE items( - item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique - workspace_id INTEGER NOT NULL, - pane_id INTEGER NOT NULL, - kind TEXT NOT NULL, - position INTEGER NOT NULL, - active INTEGER NOT NULL, - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ON UPDATE CASCADE, - FOREIGN KEY(pane_id) REFERENCES panes(pane_id) - ON DELETE CASCADE, - PRIMARY KEY(item_id, workspace_id) - ) STRICT; - ), - sql!( - ALTER TABLE workspaces ADD COLUMN window_state TEXT; - ALTER TABLE workspaces ADD COLUMN window_x REAL; - ALTER TABLE workspaces ADD COLUMN window_y REAL; - ALTER TABLE workspaces ADD COLUMN window_width REAL; - ALTER TABLE workspaces ADD COLUMN window_height REAL; - ALTER TABLE workspaces ADD COLUMN display BLOB; - ), - // Drop foreign key constraint from workspaces.dock_pane to panes table. - sql!( - CREATE TABLE workspaces_2( - workspace_id INTEGER PRIMARY KEY, - workspace_location BLOB UNIQUE, - dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. - dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. - dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. - left_sidebar_open INTEGER, // Boolean - timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, - window_state TEXT, - window_x REAL, - window_y REAL, - window_width REAL, - window_height REAL, - display BLOB - ) STRICT; - INSERT INTO workspaces_2 SELECT * FROM workspaces; - DROP TABLE workspaces; - ALTER TABLE workspaces_2 RENAME TO workspaces; - ), - // Add panels related information - sql!( - ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT; - ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT; - ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT; - ), - // Add panel zoom persistence - sql!( - ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool - ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool - ), - // Add pane group flex data - sql!( - ALTER TABLE pane_groups ADD COLUMN flexes TEXT; - ), - // Add fullscreen field to workspace - // Deprecated, `WindowBounds` holds the fullscreen state now. - // Preserving so users can downgrade Zed. - sql!( - ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool - ), - // Add preview field to items - sql!( - ALTER TABLE items ADD COLUMN preview INTEGER; //bool - ), - // Add centered_layout field to workspace - sql!( - ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool - ), - sql!( - CREATE TABLE remote_projects ( - remote_project_id INTEGER NOT NULL UNIQUE, - path TEXT, - dev_server_name TEXT - ); - ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER; - ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths; - ), - sql!( - DROP TABLE remote_projects; - CREATE TABLE dev_server_projects ( - id INTEGER NOT NULL UNIQUE, - path TEXT, - dev_server_name TEXT - ); - ALTER TABLE workspaces DROP COLUMN remote_project_id; - ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER; - ), - sql!( - ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB; - ), - sql!( - ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL; - ), - sql!( - ALTER TABLE workspaces ADD COLUMN window_id INTEGER DEFAULT NULL; - ), - sql!( - ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0; - ), - sql!( - CREATE TABLE ssh_projects ( - id INTEGER PRIMARY KEY, - host TEXT NOT NULL, - port INTEGER, - path TEXT NOT NULL, - user TEXT - ); - ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE; - ), - sql!( - ALTER TABLE ssh_projects RENAME COLUMN path TO paths; - ), - sql!( - CREATE TABLE toolchains ( - workspace_id INTEGER, - worktree_id INTEGER, - language_name TEXT NOT NULL, - name TEXT NOT NULL, - path TEXT NOT NULL, - PRIMARY KEY (workspace_id, worktree_id, language_name) - ); - ), - sql!( - ALTER TABLE toolchains ADD COLUMN raw_json TEXT DEFAULT "{}"; - ), - sql!( + CREATE TABLE items( + item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique + workspace_id INTEGER NOT NULL, + pane_id INTEGER NOT NULL, + kind TEXT NOT NULL, + position INTEGER NOT NULL, + active INTEGER NOT NULL, + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) + ON DELETE CASCADE + ON UPDATE CASCADE, + FOREIGN KEY(pane_id) REFERENCES panes(pane_id) + ON DELETE CASCADE, + PRIMARY KEY(item_id, workspace_id) + ) STRICT; + ), + sql!( + ALTER TABLE workspaces ADD COLUMN window_state TEXT; + ALTER TABLE workspaces ADD COLUMN window_x REAL; + ALTER TABLE workspaces ADD COLUMN window_y REAL; + ALTER TABLE workspaces ADD COLUMN window_width REAL; + ALTER TABLE workspaces ADD COLUMN window_height REAL; + ALTER TABLE workspaces ADD COLUMN display BLOB; + ), + // Drop foreign key constraint from workspaces.dock_pane to panes table. + sql!( + CREATE TABLE workspaces_2( + workspace_id INTEGER PRIMARY KEY, + workspace_location BLOB UNIQUE, + dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. + dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. + dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. + left_sidebar_open INTEGER, // Boolean + timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, + window_state TEXT, + window_x REAL, + window_y REAL, + window_width REAL, + window_height REAL, + display BLOB + ) STRICT; + INSERT INTO workspaces_2 SELECT * FROM workspaces; + DROP TABLE workspaces; + ALTER TABLE workspaces_2 RENAME TO workspaces; + ), + // Add panels related information + sql!( + ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT; + ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT; + ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT; + ), + // Add panel zoom persistence + sql!( + ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool + ), + // Add pane group flex data + sql!( + ALTER TABLE pane_groups ADD COLUMN flexes TEXT; + ), + // Add fullscreen field to workspace + // Deprecated, `WindowBounds` holds the fullscreen state now. + // Preserving so users can downgrade Zed. + sql!( + ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool + ), + // Add preview field to items + sql!( + ALTER TABLE items ADD COLUMN preview INTEGER; //bool + ), + // Add centered_layout field to workspace + sql!( + ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool + ), + sql!( + CREATE TABLE remote_projects ( + remote_project_id INTEGER NOT NULL UNIQUE, + path TEXT, + dev_server_name TEXT + ); + ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER; + ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths; + ), + sql!( + DROP TABLE remote_projects; + CREATE TABLE dev_server_projects ( + id INTEGER NOT NULL UNIQUE, + path TEXT, + dev_server_name TEXT + ); + ALTER TABLE workspaces DROP COLUMN remote_project_id; + ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER; + ), + sql!( + ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB; + ), + sql!( + ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL; + ), + sql!( + ALTER TABLE workspaces ADD COLUMN window_id INTEGER DEFAULT NULL; + ), + sql!( + ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0; + ), + sql!( + CREATE TABLE ssh_projects ( + id INTEGER PRIMARY KEY, + host TEXT NOT NULL, + port INTEGER, + path TEXT NOT NULL, + user TEXT + ); + ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE; + ), + sql!( + ALTER TABLE ssh_projects RENAME COLUMN path TO paths; + ), + sql!( + CREATE TABLE toolchains ( + workspace_id INTEGER, + worktree_id INTEGER, + language_name TEXT NOT NULL, + name TEXT NOT NULL, + path TEXT NOT NULL, + PRIMARY KEY (workspace_id, worktree_id, language_name) + ); + ), + sql!( + ALTER TABLE toolchains ADD COLUMN raw_json TEXT DEFAULT "{}"; + ), + sql!( CREATE TABLE breakpoints ( workspace_id INTEGER NOT NULL, path TEXT NOT NULL, @@ -473,172 +526,39 @@ impl Domain for WorkspaceDb { ON UPDATE CASCADE ); ), - sql!( - ALTER TABLE workspaces ADD COLUMN local_paths_array TEXT; - CREATE UNIQUE INDEX local_paths_array_uq ON workspaces(local_paths_array); - ALTER TABLE workspaces ADD COLUMN local_paths_order_array TEXT; - ), - sql!( - ALTER TABLE breakpoints ADD COLUMN state INTEGER DEFAULT(0) NOT NULL - ), - sql!( - ALTER TABLE breakpoints DROP COLUMN kind - ), - sql!(ALTER TABLE toolchains ADD COLUMN relative_worktree_path TEXT DEFAULT "" NOT NULL), - sql!( - ALTER TABLE breakpoints ADD COLUMN condition TEXT; - ALTER TABLE breakpoints ADD COLUMN hit_condition TEXT; - ), - sql!(CREATE TABLE toolchains2 ( - workspace_id INTEGER, - worktree_id INTEGER, - language_name TEXT NOT NULL, - name TEXT NOT NULL, - path TEXT NOT NULL, - raw_json TEXT NOT NULL, - relative_worktree_path TEXT NOT NULL, - PRIMARY KEY (workspace_id, worktree_id, language_name, relative_worktree_path)) STRICT; - INSERT INTO toolchains2 - SELECT * FROM toolchains; - DROP TABLE toolchains; - ALTER TABLE toolchains2 RENAME TO toolchains; - ), - sql!( - CREATE TABLE ssh_connections ( - id INTEGER PRIMARY KEY, - host TEXT NOT NULL, - port INTEGER, - user TEXT - ); - - INSERT INTO ssh_connections (host, port, user) - SELECT DISTINCT host, port, user - FROM ssh_projects; - - CREATE TABLE workspaces_2( - workspace_id INTEGER PRIMARY KEY, - paths TEXT, - paths_order TEXT, - ssh_connection_id INTEGER REFERENCES ssh_connections(id), - timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, - window_state TEXT, - window_x REAL, - window_y REAL, - window_width REAL, - window_height REAL, - display BLOB, - left_dock_visible INTEGER, - left_dock_active_panel TEXT, - right_dock_visible INTEGER, - right_dock_active_panel TEXT, - bottom_dock_visible INTEGER, - bottom_dock_active_panel TEXT, - left_dock_zoom INTEGER, - right_dock_zoom INTEGER, - bottom_dock_zoom INTEGER, - fullscreen INTEGER, - centered_layout INTEGER, - session_id TEXT, - window_id INTEGER - ) STRICT; - - INSERT - INTO workspaces_2 - SELECT - workspaces.workspace_id, - CASE - WHEN ssh_projects.id IS NOT NULL THEN ssh_projects.paths - ELSE - CASE - WHEN workspaces.local_paths_array IS NULL OR workspaces.local_paths_array = "" THEN - NULL - ELSE - replace(workspaces.local_paths_array, ',', CHAR(10)) - END - END as paths, - - CASE - WHEN ssh_projects.id IS NOT NULL THEN "" - ELSE workspaces.local_paths_order_array - END as paths_order, - - CASE - WHEN ssh_projects.id IS NOT NULL THEN ( - SELECT ssh_connections.id - FROM ssh_connections - WHERE - ssh_connections.host IS ssh_projects.host AND - ssh_connections.port IS ssh_projects.port AND - ssh_connections.user IS ssh_projects.user - ) - ELSE NULL - END as ssh_connection_id, - - workspaces.timestamp, - workspaces.window_state, - workspaces.window_x, - workspaces.window_y, - workspaces.window_width, - workspaces.window_height, - workspaces.display, - workspaces.left_dock_visible, - workspaces.left_dock_active_panel, - workspaces.right_dock_visible, - workspaces.right_dock_active_panel, - workspaces.bottom_dock_visible, - workspaces.bottom_dock_active_panel, - workspaces.left_dock_zoom, - workspaces.right_dock_zoom, - workspaces.bottom_dock_zoom, - workspaces.fullscreen, - workspaces.centered_layout, - workspaces.session_id, - workspaces.window_id - FROM - workspaces LEFT JOIN - ssh_projects ON - workspaces.ssh_project_id = ssh_projects.id; - - DELETE FROM workspaces_2 - WHERE workspace_id NOT IN ( - SELECT MAX(workspace_id) - FROM workspaces_2 - GROUP BY ssh_connection_id, paths - ); - - DROP TABLE ssh_projects; - DROP TABLE workspaces; - ALTER TABLE workspaces_2 RENAME TO workspaces; - - CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(ssh_connection_id, paths); - ), - // Fix any data from when workspaces.paths were briefly encoded as JSON arrays - sql!( - UPDATE workspaces - SET paths = CASE - WHEN substr(paths, 1, 2) = '[' || '"' AND substr(paths, -2, 2) = '"' || ']' THEN - replace( - substr(paths, 3, length(paths) - 4), - '"' || ',' || '"', - CHAR(10) - ) - ELSE - replace(paths, ',', CHAR(10)) - END - WHERE paths IS NOT NULL - ), + sql!( + ALTER TABLE workspaces ADD COLUMN local_paths_array TEXT; + CREATE UNIQUE INDEX local_paths_array_uq ON workspaces(local_paths_array); + ALTER TABLE workspaces ADD COLUMN local_paths_order_array TEXT; + ), + sql!( + ALTER TABLE breakpoints ADD COLUMN state INTEGER DEFAULT(0) NOT NULL + ), + sql!( + ALTER TABLE breakpoints DROP COLUMN kind + ), + sql!(ALTER TABLE toolchains ADD COLUMN relative_worktree_path TEXT DEFAULT "" NOT NULL), + sql!( + ALTER TABLE breakpoints ADD COLUMN condition TEXT; + ALTER TABLE breakpoints ADD COLUMN hit_condition TEXT; + ), + sql!(CREATE TABLE toolchains2 ( + workspace_id INTEGER, + worktree_id INTEGER, + language_name TEXT NOT NULL, + name TEXT NOT NULL, + path TEXT NOT NULL, + raw_json TEXT NOT NULL, + relative_worktree_path TEXT NOT NULL, + PRIMARY KEY (workspace_id, worktree_id, language_name, relative_worktree_path)) STRICT; + INSERT INTO toolchains2 + SELECT * FROM toolchains; + DROP TABLE toolchains; + ALTER TABLE toolchains2 RENAME TO toolchains; + ) ]; - - // Allow recovering from bad migration that was initially shipped to nightly - // when introducing the ssh_connections table. - fn should_allow_migration_change(_index: usize, old: &str, new: &str) -> bool { - old.starts_with("CREATE TABLE ssh_connections") - && new.starts_with("CREATE TABLE ssh_connections") - } } -db::static_connection!(DB, WorkspaceDb, []); - impl WorkspaceDb { /// Returns a serialized workspace for the given worktree_roots. If the passed array /// is empty, the most recent workspace is returned instead. If no workspace for the @@ -646,33 +566,17 @@ impl WorkspaceDb { pub(crate) fn workspace_for_roots>( &self, worktree_roots: &[P], - ) -> Option { - self.workspace_for_roots_internal(worktree_roots, None) - } - - pub(crate) fn ssh_workspace_for_roots>( - &self, - worktree_roots: &[P], - ssh_project_id: SshConnectionId, - ) -> Option { - self.workspace_for_roots_internal(worktree_roots, Some(ssh_project_id)) - } - - pub(crate) fn workspace_for_roots_internal>( - &self, - worktree_roots: &[P], - ssh_connection_id: Option, ) -> Option { // paths are sorted before db interactions to ensure that the order of the paths // doesn't affect the workspace selection for existing workspaces - let root_paths = PathList::new(worktree_roots); + let local_paths = LocalPaths::new(worktree_roots); // Note that we re-assign the workspace_id here in case it's empty // and we've grabbed the most recent workspace let ( workspace_id, - paths, - paths_order, + local_paths, + local_paths_order, window_bounds, display, centered_layout, @@ -680,8 +584,8 @@ impl WorkspaceDb { window_id, ): ( WorkspaceId, - String, - String, + Option, + Option, Option, Option, Option, @@ -691,8 +595,8 @@ impl WorkspaceDb { .select_row_bound(sql! { SELECT workspace_id, - paths, - paths_order, + local_paths, + local_paths_order, window_state, window_x, window_y, @@ -711,31 +615,25 @@ impl WorkspaceDb { bottom_dock_zoom, window_id FROM workspaces - WHERE - paths IS ? AND - ssh_connection_id IS ? - LIMIT 1 - }) - .map(|mut prepared_statement| { - (prepared_statement)(( - root_paths.serialize().paths, - ssh_connection_id.map(|id| id.0 as i32), - )) - .unwrap() + WHERE local_paths = ? }) + .and_then(|mut prepared_statement| (prepared_statement)(&local_paths)) .context("No workspaces found") .warn_on_err() .flatten()?; - let paths = PathList::deserialize(&SerializedPathList { - paths, - order: paths_order, - }); + let local_paths = local_paths?; + let location = match local_paths_order { + Some(order) => SerializedWorkspaceLocation::Local(local_paths, order), + None => { + let order = LocalPathsOrder::default_for_paths(&local_paths); + SerializedWorkspaceLocation::Local(local_paths, order) + } + }; Some(SerializedWorkspace { id: workspace_id, - location: SerializedWorkspaceLocation::Local, - paths, + location, center_group: self .get_center_pane_group(workspace_id) .context("Getting center group") @@ -750,6 +648,63 @@ impl WorkspaceDb { }) } + pub(crate) fn workspace_for_ssh_project( + &self, + ssh_project: &SerializedSshProject, + ) -> Option { + let (workspace_id, window_bounds, display, centered_layout, docks, window_id): ( + WorkspaceId, + Option, + Option, + Option, + DockStructure, + Option, + ) = self + .select_row_bound(sql! { + SELECT + workspace_id, + window_state, + window_x, + window_y, + window_width, + window_height, + display, + centered_layout, + left_dock_visible, + left_dock_active_panel, + left_dock_zoom, + right_dock_visible, + right_dock_active_panel, + right_dock_zoom, + bottom_dock_visible, + bottom_dock_active_panel, + bottom_dock_zoom, + window_id + FROM workspaces + WHERE ssh_project_id = ? + }) + .and_then(|mut prepared_statement| (prepared_statement)(ssh_project.id.0)) + .context("No workspaces found") + .warn_on_err() + .flatten()?; + + Some(SerializedWorkspace { + id: workspace_id, + location: SerializedWorkspaceLocation::Ssh(ssh_project.clone()), + center_group: self + .get_center_pane_group(workspace_id) + .context("Getting center group") + .log_err()?, + window_bounds, + centered_layout: centered_layout.unwrap_or(false), + breakpoints: self.breakpoints(workspace_id), + display, + docks, + session_id: None, + window_id, + }) + } + fn breakpoints(&self, workspace_id: WorkspaceId) -> BTreeMap, Vec> { let breakpoints: Result> = self .select_bound(sql! { @@ -799,34 +754,16 @@ impl WorkspaceDb { /// Saves a workspace using the worktree roots. Will garbage collect any workspaces /// that used this workspace previously pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) { - let paths = workspace.paths.serialize(); log::debug!("Saving workspace at location: {:?}", workspace.location); self.write(move |conn| { conn.with_savepoint("update_worktrees", || { - let ssh_connection_id = match &workspace.location { - SerializedWorkspaceLocation::Local => None, - SerializedWorkspaceLocation::Ssh(connection) => { - Some(Self::get_or_create_ssh_connection_query( - conn, - connection.host.clone(), - connection.port, - connection.user.clone(), - )?.0) - } - }; - // Clear out panes and pane_groups conn.exec_bound(sql!( DELETE FROM pane_groups WHERE workspace_id = ?1; DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id) .context("Clearing old panes")?; - conn.exec_bound( - sql!( - DELETE FROM breakpoints WHERE workspace_id = ?1; - DELETE FROM toolchains WHERE workspace_id = ?1; - ) - )?(workspace.id).context("Clearing old breakpoints")?; + conn.exec_bound(sql!(DELETE FROM breakpoints WHERE workspace_id = ?1))?(workspace.id).context("Clearing old breakpoints")?; for (path, breakpoints) in workspace.breakpoints { for bp in breakpoints { @@ -853,73 +790,115 @@ impl WorkspaceDb { } } } + } - conn.exec_bound(sql!( - DELETE - FROM workspaces - WHERE - workspace_id != ?1 AND - paths IS ?2 AND - ssh_connection_id IS ?3 - ))?(( - workspace.id, - paths.paths.clone(), - ssh_connection_id, - )) - .context("clearing out old locations")?; - // Upsert - let query = sql!( - INSERT INTO workspaces( - workspace_id, - paths, - paths_order, - ssh_connection_id, - left_dock_visible, - left_dock_active_panel, - left_dock_zoom, - right_dock_visible, - right_dock_active_panel, - right_dock_zoom, - bottom_dock_visible, - bottom_dock_active_panel, - bottom_dock_zoom, - session_id, - window_id, - timestamp - ) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, CURRENT_TIMESTAMP) - ON CONFLICT DO - UPDATE SET - paths = ?2, - paths_order = ?3, - ssh_connection_id = ?4, - left_dock_visible = ?5, - left_dock_active_panel = ?6, - left_dock_zoom = ?7, - right_dock_visible = ?8, - right_dock_active_panel = ?9, - right_dock_zoom = ?10, - bottom_dock_visible = ?11, - bottom_dock_active_panel = ?12, - bottom_dock_zoom = ?13, - session_id = ?14, - window_id = ?15, - timestamp = CURRENT_TIMESTAMP - ); - let mut prepared_query = conn.exec_bound(query)?; - let args = ( - workspace.id, - paths.paths.clone(), - paths.order.clone(), - ssh_connection_id, - workspace.docks, - workspace.session_id, - workspace.window_id, - ); + match workspace.location { + SerializedWorkspaceLocation::Local(local_paths, local_paths_order) => { + conn.exec_bound(sql!( + DELETE FROM toolchains WHERE workspace_id = ?1; + DELETE FROM workspaces WHERE local_paths = ? AND workspace_id != ? + ))?((&local_paths, workspace.id)) + .context("clearing out old locations")?; - prepared_query(args).context("Updating workspace")?; + // Upsert + let query = sql!( + INSERT INTO workspaces( + workspace_id, + local_paths, + local_paths_order, + left_dock_visible, + left_dock_active_panel, + left_dock_zoom, + right_dock_visible, + right_dock_active_panel, + right_dock_zoom, + bottom_dock_visible, + bottom_dock_active_panel, + bottom_dock_zoom, + session_id, + window_id, + timestamp, + local_paths_array, + local_paths_order_array + ) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, CURRENT_TIMESTAMP, ?15, ?16) + ON CONFLICT DO + UPDATE SET + local_paths = ?2, + local_paths_order = ?3, + left_dock_visible = ?4, + left_dock_active_panel = ?5, + left_dock_zoom = ?6, + right_dock_visible = ?7, + right_dock_active_panel = ?8, + right_dock_zoom = ?9, + bottom_dock_visible = ?10, + bottom_dock_active_panel = ?11, + bottom_dock_zoom = ?12, + session_id = ?13, + window_id = ?14, + timestamp = CURRENT_TIMESTAMP, + local_paths_array = ?15, + local_paths_order_array = ?16 + ); + let mut prepared_query = conn.exec_bound(query)?; + let args = (workspace.id, &local_paths, &local_paths_order, workspace.docks, workspace.session_id, workspace.window_id, local_paths.paths().iter().map(|path| path.to_string_lossy().to_string()).join(","), local_paths_order.order().iter().map(|order| order.to_string()).join(",")); + + prepared_query(args).context("Updating workspace")?; + } + SerializedWorkspaceLocation::Ssh(ssh_project) => { + conn.exec_bound(sql!( + DELETE FROM toolchains WHERE workspace_id = ?1; + DELETE FROM workspaces WHERE ssh_project_id = ? AND workspace_id != ? + ))?((ssh_project.id.0, workspace.id)) + .context("clearing out old locations")?; + + // Upsert + conn.exec_bound(sql!( + INSERT INTO workspaces( + workspace_id, + ssh_project_id, + left_dock_visible, + left_dock_active_panel, + left_dock_zoom, + right_dock_visible, + right_dock_active_panel, + right_dock_zoom, + bottom_dock_visible, + bottom_dock_active_panel, + bottom_dock_zoom, + session_id, + window_id, + timestamp + ) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, CURRENT_TIMESTAMP) + ON CONFLICT DO + UPDATE SET + ssh_project_id = ?2, + left_dock_visible = ?3, + left_dock_active_panel = ?4, + left_dock_zoom = ?5, + right_dock_visible = ?6, + right_dock_active_panel = ?7, + right_dock_zoom = ?8, + bottom_dock_visible = ?9, + bottom_dock_active_panel = ?10, + bottom_dock_zoom = ?11, + session_id = ?12, + window_id = ?13, + timestamp = CURRENT_TIMESTAMP + ))?(( + workspace.id, + ssh_project.id.0, + workspace.docks, + workspace.session_id, + workspace.window_id + )) + .context("Updating workspace")?; + } + } // Save center pane group Self::save_pane_group(conn, workspace.id, &workspace.center_group, None) @@ -932,95 +911,89 @@ impl WorkspaceDb { .await; } - pub(crate) async fn get_or_create_ssh_connection( + pub(crate) async fn get_or_create_ssh_project( &self, host: String, port: Option, + paths: Vec, user: Option, - ) -> Result { - self.write(move |conn| Self::get_or_create_ssh_connection_query(conn, host, port, user)) - .await - } - - fn get_or_create_ssh_connection_query( - this: &Connection, - host: String, - port: Option, - user: Option, - ) -> Result { - if let Some(id) = this.select_row_bound(sql!( - SELECT id FROM ssh_connections WHERE host IS ? AND port IS ? AND user IS ? LIMIT 1 - ))?((host.clone(), port, user.clone()))? + ) -> Result { + let paths = serde_json::to_string(&paths)?; + if let Some(project) = self + .get_ssh_project(host.clone(), port, paths.clone(), user.clone()) + .await? { - Ok(SshConnectionId(id)) + Ok(project) } else { log::debug!("Inserting SSH project at host {host}"); - let id = this.select_row_bound(sql!( - INSERT INTO ssh_connections ( - host, - port, - user - ) VALUES (?1, ?2, ?3) - RETURNING id - ))?((host, port, user))? - .context("failed to insert ssh project")?; - Ok(SshConnectionId(id)) + self.insert_ssh_project(host, port, paths, user) + .await? + .context("failed to insert ssh project") } } + query! { + async fn get_ssh_project(host: String, port: Option, paths: String, user: Option) -> Result> { + SELECT id, host, port, paths, user + FROM ssh_projects + WHERE host IS ? AND port IS ? AND paths IS ? AND user IS ? + LIMIT 1 + } + } + + query! { + async fn insert_ssh_project(host: String, port: Option, paths: String, user: Option) -> Result> { + INSERT INTO ssh_projects( + host, + port, + paths, + user + ) VALUES (?1, ?2, ?3, ?4) + RETURNING id, host, port, paths, user + } + } + + query! { + pub async fn update_ssh_project_paths_query(ssh_project_id: u64, paths: String) -> Result> { + UPDATE ssh_projects + SET paths = ?2 + WHERE id = ?1 + RETURNING id, host, port, paths, user + } + } + + pub(crate) async fn update_ssh_project_paths( + &self, + ssh_project_id: SshProjectId, + new_paths: Vec, + ) -> Result { + let paths = serde_json::to_string(&new_paths)?; + self.update_ssh_project_paths_query(ssh_project_id.0, paths) + .await? + .context("failed to update ssh project paths") + } + query! { pub async fn next_id() -> Result { INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id } } - fn recent_workspaces(&self) -> Result)>> { - Ok(self - .recent_workspaces_query()? - .into_iter() - .map(|(id, paths, order, ssh_connection_id)| { - ( - id, - PathList::deserialize(&SerializedPathList { paths, order }), - ssh_connection_id, - ) - }) - .collect()) - } - query! { - fn recent_workspaces_query() -> Result)>> { - SELECT workspace_id, paths, paths_order, ssh_connection_id + fn recent_workspaces() -> Result)>> { + SELECT workspace_id, local_paths, local_paths_order, ssh_project_id FROM workspaces - WHERE - paths IS NOT NULL OR - ssh_connection_id IS NOT NULL + WHERE local_paths IS NOT NULL + OR ssh_project_id IS NOT NULL ORDER BY timestamp DESC } } - fn session_workspaces( - &self, - session_id: String, - ) -> Result, Option)>> { - Ok(self - .session_workspaces_query(session_id)? - .into_iter() - .map(|(paths, order, window_id, ssh_connection_id)| { - ( - PathList::deserialize(&SerializedPathList { paths, order }), - window_id, - ssh_connection_id.map(SshConnectionId), - ) - }) - .collect()) - } - query! { - fn session_workspaces_query(session_id: String) -> Result, Option)>> { - SELECT paths, paths_order, window_id, ssh_connection_id + fn session_workspaces(session_id: String) -> Result, Option)>> { + SELECT local_paths, local_paths_order, window_id, ssh_project_id FROM workspaces - WHERE session_id = ?1 + WHERE session_id = ?1 AND dev_server_project_id IS NULL ORDER BY timestamp DESC } } @@ -1040,39 +1013,17 @@ impl WorkspaceDb { } } - fn ssh_connections(&self) -> Result> { - Ok(self - .ssh_connections_query()? - .into_iter() - .map(|(id, host, port, user)| { - ( - SshConnectionId(id), - SerializedSshConnection { host, port, user }, - ) - }) - .collect()) - } - query! { - pub fn ssh_connections_query() -> Result, Option)>> { - SELECT id, host, port, user - FROM ssh_connections + fn ssh_projects() -> Result> { + SELECT id, host, port, paths, user + FROM ssh_projects } } - pub(crate) fn ssh_connection(&self, id: SshConnectionId) -> Result { - let row = self.ssh_connection_query(id.0)?; - Ok(SerializedSshConnection { - host: row.0, - port: row.1, - user: row.2, - }) - } - query! { - fn ssh_connection_query(id: u64) -> Result<(String, Option, Option)> { - SELECT host, port, user - FROM ssh_connections + fn ssh_project(id: u64) -> Result { + SELECT id, host, port, paths, user + FROM ssh_projects WHERE id = ? } } @@ -1086,7 +1037,7 @@ impl WorkspaceDb { display, window_state, window_x, window_y, window_width, window_height FROM workspaces - WHERE paths + WHERE local_paths IS NOT NULL ORDER BY timestamp DESC LIMIT 1 @@ -1103,33 +1054,46 @@ impl WorkspaceDb { } } + pub async fn delete_workspace_by_dev_server_project_id( + &self, + id: DevServerProjectId, + ) -> Result<()> { + self.write(move |conn| { + conn.exec_bound(sql!( + DELETE FROM dev_server_projects WHERE id = ? + ))?(id.0)?; + conn.exec_bound(sql!( + DELETE FROM toolchains WHERE workspace_id = ?1; + DELETE FROM workspaces + WHERE dev_server_project_id IS ? + ))?(id.0) + }) + .await + } + // Returns the recent locations which are still valid on disk and deletes ones which no longer // exist. pub async fn recent_workspaces_on_disk( &self, - ) -> Result> { + ) -> Result> { let mut result = Vec::new(); let mut delete_tasks = Vec::new(); - let ssh_connections = self.ssh_connections()?; + let ssh_projects = self.ssh_projects()?; - for (id, paths, ssh_connection_id) in self.recent_workspaces()? { - if let Some(ssh_connection_id) = ssh_connection_id.map(SshConnectionId) { - if let Some(ssh_connection) = ssh_connections.get(&ssh_connection_id) { - result.push(( - id, - SerializedWorkspaceLocation::Ssh(ssh_connection.clone()), - paths, - )); + for (id, location, order, ssh_project_id) in self.recent_workspaces()? { + if let Some(ssh_project_id) = ssh_project_id.map(SshProjectId) { + if let Some(ssh_project) = ssh_projects.iter().find(|rp| rp.id == ssh_project_id) { + result.push((id, SerializedWorkspaceLocation::Ssh(ssh_project.clone()))); } else { delete_tasks.push(self.delete_workspace_by_id(id)); } continue; } - if paths.paths().iter().all(|path| path.exists()) - && paths.paths().iter().any(|path| path.is_dir()) + if location.paths().iter().all(|path| path.exists()) + && location.paths().iter().any(|path| path.is_dir()) { - result.push((id, SerializedWorkspaceLocation::Local, paths)); + result.push((id, SerializedWorkspaceLocation::Local(location, order))); } else { delete_tasks.push(self.delete_workspace_by_id(id)); } @@ -1139,13 +1103,13 @@ impl WorkspaceDb { Ok(result) } - pub async fn last_workspace(&self) -> Result> { + pub async fn last_workspace(&self) -> Result> { Ok(self .recent_workspaces_on_disk() .await? .into_iter() .next() - .map(|(_, location, paths)| (location, paths))) + .map(|(_, location)| location)) } // Returns the locations of the workspaces that were still opened when the last @@ -1156,31 +1120,25 @@ impl WorkspaceDb { &self, last_session_id: &str, last_session_window_stack: Option>, - ) -> Result> { + ) -> Result> { let mut workspaces = Vec::new(); - for (paths, window_id, ssh_connection_id) in + for (location, order, window_id, ssh_project_id) in self.session_workspaces(last_session_id.to_owned())? { - if let Some(ssh_connection_id) = ssh_connection_id { - workspaces.push(( - SerializedWorkspaceLocation::Ssh(self.ssh_connection(ssh_connection_id)?), - paths, - window_id.map(WindowId::from), - )); - } else if paths.paths().iter().all(|path| path.exists()) - && paths.paths().iter().any(|path| path.is_dir()) + if let Some(ssh_project_id) = ssh_project_id { + let location = SerializedWorkspaceLocation::Ssh(self.ssh_project(ssh_project_id)?); + workspaces.push((location, window_id.map(WindowId::from))); + } else if location.paths().iter().all(|path| path.exists()) + && location.paths().iter().any(|path| path.is_dir()) { - workspaces.push(( - SerializedWorkspaceLocation::Local, - paths, - window_id.map(WindowId::from), - )); + let location = SerializedWorkspaceLocation::Local(location, order); + workspaces.push((location, window_id.map(WindowId::from))); } } if let Some(stack) = last_session_window_stack { - workspaces.sort_by_key(|(_, _, window_id)| { + workspaces.sort_by_key(|(_, window_id)| { window_id .and_then(|id| stack.iter().position(|&order_id| order_id == id)) .unwrap_or(usize::MAX) @@ -1189,7 +1147,7 @@ impl WorkspaceDb { Ok(workspaces .into_iter() - .map(|(location, paths, _)| (location, paths)) + .map(|(paths, _)| paths) .collect::>()) } @@ -1541,13 +1499,13 @@ pub fn delete_unloaded_items( #[cfg(test)] mod tests { + use std::thread; + use std::time::Duration; + use super::*; - use crate::persistence::model::{ - SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace, - }; + use crate::persistence::model::SerializedWorkspace; + use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup}; use gpui; - use pretty_assertions::assert_eq; - use std::{thread, time::Duration}; #[gpui::test] async fn test_breakpoints() { @@ -1600,8 +1558,7 @@ mod tests { let workspace = SerializedWorkspace { id, - paths: PathList::new(&["/tmp"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -1754,8 +1711,7 @@ mod tests { let workspace = SerializedWorkspace { id, - paths: PathList::new(&["/tmp"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -1801,8 +1757,7 @@ mod tests { let workspace_without_breakpoint = SerializedWorkspace { id, - paths: PathList::new(&["/tmp"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -1841,7 +1796,6 @@ mod tests { ON DELETE CASCADE ) STRICT; )], - |_, _, _| false, ) .unwrap(); }) @@ -1890,7 +1844,6 @@ mod tests { REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT;)], - |_, _, _| false, ) }) .await @@ -1898,8 +1851,7 @@ mod tests { let mut workspace_1 = SerializedWorkspace { id: WorkspaceId(1), - paths: PathList::new(&["/tmp", "/tmp2"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp2"]), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -1912,8 +1864,7 @@ mod tests { let workspace_2 = SerializedWorkspace { id: WorkspaceId(2), - paths: PathList::new(&["/tmp"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -1942,7 +1893,7 @@ mod tests { }) .await; - workspace_1.paths = PathList::new(&["/tmp", "/tmp3"]); + workspace_1.location = SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp3"]); db.save_workspace(workspace_1.clone()).await; db.save_workspace(workspace_1).await; db.save_workspace(workspace_2).await; @@ -2018,8 +1969,10 @@ mod tests { let workspace = SerializedWorkspace { id: WorkspaceId(5), - paths: PathList::new(&["/tmp", "/tmp2"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::Local( + LocalPaths::new(["/tmp", "/tmp2"]), + LocalPathsOrder::new([1, 0]), + ), center_group, window_bounds: Default::default(), breakpoints: Default::default(), @@ -2051,8 +2004,10 @@ mod tests { let workspace_1 = SerializedWorkspace { id: WorkspaceId(1), - paths: PathList::new(&["/tmp", "/tmp2"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::Local( + LocalPaths::new(["/tmp", "/tmp2"]), + LocalPathsOrder::new([0, 1]), + ), center_group: Default::default(), window_bounds: Default::default(), breakpoints: Default::default(), @@ -2065,8 +2020,7 @@ mod tests { let mut workspace_2 = SerializedWorkspace { id: WorkspaceId(2), - paths: PathList::new(&["/tmp"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2095,7 +2049,7 @@ mod tests { assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None); // Test 'mutate' case of updating a pre-existing id - workspace_2.paths = PathList::new(&["/tmp", "/tmp2"]); + workspace_2.location = SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp2"]); db.save_workspace(workspace_2.clone()).await; assert_eq!( @@ -2106,8 +2060,10 @@ mod tests { // Test other mechanism for mutating let mut workspace_3 = SerializedWorkspace { id: WorkspaceId(3), - paths: PathList::new(&["/tmp2", "/tmp"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::Local( + LocalPaths::new(["/tmp", "/tmp2"]), + LocalPathsOrder::new([1, 0]), + ), center_group: Default::default(), window_bounds: Default::default(), breakpoints: Default::default(), @@ -2125,7 +2081,8 @@ mod tests { ); // Make sure that updating paths differently also works - workspace_3.paths = PathList::new(&["/tmp3", "/tmp4", "/tmp2"]); + workspace_3.location = + SerializedWorkspaceLocation::from_local_paths(["/tmp3", "/tmp4", "/tmp2"]); db.save_workspace(workspace_3.clone()).await; assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None); assert_eq!( @@ -2143,8 +2100,7 @@ mod tests { let workspace_1 = SerializedWorkspace { id: WorkspaceId(1), - paths: PathList::new(&["/tmp1"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(["/tmp1"]), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2157,8 +2113,7 @@ mod tests { let workspace_2 = SerializedWorkspace { id: WorkspaceId(2), - paths: PathList::new(&["/tmp2"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(["/tmp2"]), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2171,8 +2126,7 @@ mod tests { let workspace_3 = SerializedWorkspace { id: WorkspaceId(3), - paths: PathList::new(&["/tmp3"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(["/tmp3"]), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2185,8 +2139,7 @@ mod tests { let workspace_4 = SerializedWorkspace { id: WorkspaceId(4), - paths: PathList::new(&["/tmp4"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(["/tmp4"]), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2197,15 +2150,14 @@ mod tests { window_id: None, }; - let connection_id = db - .get_or_create_ssh_connection("my-host".to_string(), Some(1234), None) + let ssh_project = db + .get_or_create_ssh_project("my-host".to_string(), Some(1234), vec![], None) .await .unwrap(); let workspace_5 = SerializedWorkspace { id: WorkspaceId(5), - paths: PathList::default(), - location: SerializedWorkspaceLocation::Ssh(db.ssh_connection(connection_id).unwrap()), + location: SerializedWorkspaceLocation::Ssh(ssh_project.clone()), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2218,8 +2170,10 @@ mod tests { let workspace_6 = SerializedWorkspace { id: WorkspaceId(6), - paths: PathList::new(&["/tmp6a", "/tmp6b", "/tmp6c"]), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::Local( + LocalPaths::new(["/tmp6a", "/tmp6b", "/tmp6c"]), + LocalPathsOrder::new([2, 1, 0]), + ), center_group: Default::default(), window_bounds: Default::default(), breakpoints: Default::default(), @@ -2241,36 +2195,41 @@ mod tests { let locations = db.session_workspaces("session-id-1".to_owned()).unwrap(); assert_eq!(locations.len(), 2); - assert_eq!(locations[0].0, PathList::new(&["/tmp2"])); - assert_eq!(locations[0].1, Some(20)); - assert_eq!(locations[1].0, PathList::new(&["/tmp1"])); - assert_eq!(locations[1].1, Some(10)); + assert_eq!(locations[0].0, LocalPaths::new(["/tmp2"])); + assert_eq!(locations[0].1, LocalPathsOrder::new([0])); + assert_eq!(locations[0].2, Some(20)); + assert_eq!(locations[1].0, LocalPaths::new(["/tmp1"])); + assert_eq!(locations[1].1, LocalPathsOrder::new([0])); + assert_eq!(locations[1].2, Some(10)); let locations = db.session_workspaces("session-id-2".to_owned()).unwrap(); assert_eq!(locations.len(), 2); - assert_eq!(locations[0].0, PathList::default()); - assert_eq!(locations[0].1, Some(50)); - assert_eq!(locations[0].2, Some(connection_id)); - assert_eq!(locations[1].0, PathList::new(&["/tmp3"])); - assert_eq!(locations[1].1, Some(30)); + let empty_paths: Vec<&str> = Vec::new(); + assert_eq!(locations[0].0, LocalPaths::new(empty_paths.iter())); + assert_eq!(locations[0].1, LocalPathsOrder::new([])); + assert_eq!(locations[0].2, Some(50)); + assert_eq!(locations[0].3, Some(ssh_project.id.0)); + assert_eq!(locations[1].0, LocalPaths::new(["/tmp3"])); + assert_eq!(locations[1].1, LocalPathsOrder::new([0])); + assert_eq!(locations[1].2, Some(30)); let locations = db.session_workspaces("session-id-3".to_owned()).unwrap(); assert_eq!(locations.len(), 1); assert_eq!( locations[0].0, - PathList::new(&["/tmp6a", "/tmp6b", "/tmp6c"]), + LocalPaths::new(["/tmp6a", "/tmp6b", "/tmp6c"]), ); - assert_eq!(locations[0].1, Some(60)); + assert_eq!(locations[0].1, LocalPathsOrder::new([2, 1, 0])); + assert_eq!(locations[0].2, Some(60)); } fn default_workspace>( - paths: &[P], + workspace_id: &[P], center_group: &SerializedPaneGroup, ) -> SerializedWorkspace { SerializedWorkspace { id: WorkspaceId(4), - paths: PathList::new(paths), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::from_local_paths(workspace_id), center_group: center_group.clone(), window_bounds: Default::default(), display: Default::default(), @@ -2293,18 +2252,30 @@ mod tests { WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces").await; let workspaces = [ - (1, vec![dir1.path()], 9), - (2, vec![dir2.path()], 5), - (3, vec![dir3.path()], 8), - (4, vec![dir4.path()], 2), - (5, vec![dir1.path(), dir2.path(), dir3.path()], 3), - (6, vec![dir4.path(), dir3.path(), dir2.path()], 4), + (1, vec![dir1.path()], vec![0], 9), + (2, vec![dir2.path()], vec![0], 5), + (3, vec![dir3.path()], vec![0], 8), + (4, vec![dir4.path()], vec![0], 2), + ( + 5, + vec![dir1.path(), dir2.path(), dir3.path()], + vec![0, 1, 2], + 3, + ), + ( + 6, + vec![dir2.path(), dir3.path(), dir4.path()], + vec![2, 1, 0], + 4, + ), ] .into_iter() - .map(|(id, paths, window_id)| SerializedWorkspace { + .map(|(id, locations, order, window_id)| SerializedWorkspace { id: WorkspaceId(id), - paths: PathList::new(paths.as_slice()), - location: SerializedWorkspaceLocation::Local, + location: SerializedWorkspaceLocation::Local( + LocalPaths::new(locations), + LocalPathsOrder::new(order), + ), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2329,37 +2300,39 @@ mod tests { WindowId::from(4), // Bottom ])); - let locations = db + let have = db .last_session_workspace_locations("one-session", stack) .unwrap(); + assert_eq!(have.len(), 6); assert_eq!( - locations, - [ - ( - SerializedWorkspaceLocation::Local, - PathList::new(&[dir4.path()]) - ), - ( - SerializedWorkspaceLocation::Local, - PathList::new(&[dir3.path()]) - ), - ( - SerializedWorkspaceLocation::Local, - PathList::new(&[dir2.path()]) - ), - ( - SerializedWorkspaceLocation::Local, - PathList::new(&[dir1.path()]) - ), - ( - SerializedWorkspaceLocation::Local, - PathList::new(&[dir1.path(), dir2.path(), dir3.path()]) - ), - ( - SerializedWorkspaceLocation::Local, - PathList::new(&[dir4.path(), dir3.path(), dir2.path()]) - ), - ] + have[0], + SerializedWorkspaceLocation::from_local_paths(&[dir4.path()]) + ); + assert_eq!( + have[1], + SerializedWorkspaceLocation::from_local_paths([dir3.path()]) + ); + assert_eq!( + have[2], + SerializedWorkspaceLocation::from_local_paths([dir2.path()]) + ); + assert_eq!( + have[3], + SerializedWorkspaceLocation::from_local_paths([dir1.path()]) + ); + assert_eq!( + have[4], + SerializedWorkspaceLocation::Local( + LocalPaths::new([dir1.path(), dir2.path(), dir3.path()]), + LocalPathsOrder::new([0, 1, 2]), + ), + ); + assert_eq!( + have[5], + SerializedWorkspaceLocation::Local( + LocalPaths::new([dir2.path(), dir3.path(), dir4.path()]), + LocalPathsOrder::new([2, 1, 0]), + ), ); } @@ -2370,7 +2343,7 @@ mod tests { ) .await; - let ssh_connections = [ + let ssh_projects = [ ("host-1", "my-user-1"), ("host-2", "my-user-2"), ("host-3", "my-user-3"), @@ -2378,30 +2351,24 @@ mod tests { ] .into_iter() .map(|(host, user)| async { - db.get_or_create_ssh_connection(host.to_string(), None, Some(user.to_string())) + db.get_or_create_ssh_project(host.to_string(), None, vec![], Some(user.to_string())) .await - .unwrap(); - SerializedSshConnection { - host: host.into(), - port: None, - user: Some(user.into()), - } + .unwrap() }) .collect::>(); - let ssh_connections = futures::future::join_all(ssh_connections).await; + let ssh_projects = futures::future::join_all(ssh_projects).await; let workspaces = [ - (1, ssh_connections[0].clone(), 9), - (2, ssh_connections[1].clone(), 5), - (3, ssh_connections[2].clone(), 8), - (4, ssh_connections[3].clone(), 2), + (1, ssh_projects[0].clone(), 9), + (2, ssh_projects[1].clone(), 5), + (3, ssh_projects[2].clone(), 8), + (4, ssh_projects[3].clone(), 2), ] .into_iter() - .map(|(id, ssh_connection, window_id)| SerializedWorkspace { + .map(|(id, ssh_project, window_id)| SerializedWorkspace { id: WorkspaceId(id), - paths: PathList::default(), - location: SerializedWorkspaceLocation::Ssh(ssh_connection), + location: SerializedWorkspaceLocation::Ssh(ssh_project), center_group: Default::default(), window_bounds: Default::default(), display: Default::default(), @@ -2430,31 +2397,19 @@ mod tests { assert_eq!(have.len(), 4); assert_eq!( have[0], - ( - SerializedWorkspaceLocation::Ssh(ssh_connections[3].clone()), - PathList::default() - ) + SerializedWorkspaceLocation::Ssh(ssh_projects[3].clone()) ); assert_eq!( have[1], - ( - SerializedWorkspaceLocation::Ssh(ssh_connections[2].clone()), - PathList::default() - ) + SerializedWorkspaceLocation::Ssh(ssh_projects[2].clone()) ); assert_eq!( have[2], - ( - SerializedWorkspaceLocation::Ssh(ssh_connections[1].clone()), - PathList::default() - ) + SerializedWorkspaceLocation::Ssh(ssh_projects[1].clone()) ); assert_eq!( have[3], - ( - SerializedWorkspaceLocation::Ssh(ssh_connections[0].clone()), - PathList::default() - ) + SerializedWorkspaceLocation::Ssh(ssh_projects[0].clone()) ); } @@ -2462,110 +2417,116 @@ mod tests { async fn test_get_or_create_ssh_project() { let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project").await; - let host = "example.com".to_string(); - let port = Some(22_u16); - let user = Some("user".to_string()); + let (host, port, paths, user) = ( + "example.com".to_string(), + Some(22_u16), + vec!["/home/user".to_string(), "/etc/nginx".to_string()], + Some("user".to_string()), + ); - let connection_id = db - .get_or_create_ssh_connection(host.clone(), port, user.clone()) + let project = db + .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone()) .await .unwrap(); + assert_eq!(project.host, host); + assert_eq!(project.paths, paths); + assert_eq!(project.user, user); + // Test that calling the function again with the same parameters returns the same project - let same_connection = db - .get_or_create_ssh_connection(host.clone(), port, user.clone()) + let same_project = db + .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone()) .await .unwrap(); - assert_eq!(connection_id, same_connection); + assert_eq!(project.id, same_project.id); // Test with different parameters - let host2 = "otherexample.com".to_string(); - let port2 = None; - let user2 = Some("otheruser".to_string()); + let (host2, paths2, user2) = ( + "otherexample.com".to_string(), + vec!["/home/otheruser".to_string()], + Some("otheruser".to_string()), + ); - let different_connection = db - .get_or_create_ssh_connection(host2.clone(), port2, user2.clone()) + let different_project = db + .get_or_create_ssh_project(host2.clone(), None, paths2.clone(), user2.clone()) .await .unwrap(); - assert_ne!(connection_id, different_connection); + assert_ne!(project.id, different_project.id); + assert_eq!(different_project.host, host2); + assert_eq!(different_project.paths, paths2); + assert_eq!(different_project.user, user2); } #[gpui::test] async fn test_get_or_create_ssh_project_with_null_user() { let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project_with_null_user").await; - let (host, port, user) = ("example.com".to_string(), None, None); + let (host, port, paths, user) = ( + "example.com".to_string(), + None, + vec!["/home/user".to_string()], + None, + ); - let connection_id = db - .get_or_create_ssh_connection(host.clone(), port, None) + let project = db + .get_or_create_ssh_project(host.clone(), port, paths.clone(), None) .await .unwrap(); - let same_connection_id = db - .get_or_create_ssh_connection(host.clone(), port, user.clone()) + assert_eq!(project.host, host); + assert_eq!(project.paths, paths); + assert_eq!(project.user, None); + + // Test that calling the function again with the same parameters returns the same project + let same_project = db + .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone()) .await .unwrap(); - assert_eq!(connection_id, same_connection_id); + assert_eq!(project.id, same_project.id); } #[gpui::test] - async fn test_get_ssh_connections() { - let db = WorkspaceDb::open_test_db("test_get_ssh_connections").await; + async fn test_get_ssh_projects() { + let db = WorkspaceDb::open_test_db("test_get_ssh_projects").await; - let connections = [ - ("example.com".to_string(), None, None), + let projects = vec![ + ( + "example.com".to_string(), + None, + vec!["/home/user".to_string()], + None, + ), ( "anotherexample.com".to_string(), Some(123_u16), + vec!["/home/user2".to_string()], Some("user2".to_string()), ), - ("yetanother.com".to_string(), Some(345_u16), None), + ( + "yetanother.com".to_string(), + Some(345_u16), + vec!["/home/user3".to_string(), "/proc/1234/exe".to_string()], + None, + ), ]; - let mut ids = Vec::new(); - for (host, port, user) in connections.iter() { - ids.push( - db.get_or_create_ssh_connection(host.clone(), *port, user.clone()) - .await - .unwrap(), - ); + for (host, port, paths, user) in projects.iter() { + let project = db + .get_or_create_ssh_project(host.clone(), *port, paths.clone(), user.clone()) + .await + .unwrap(); + + assert_eq!(&project.host, host); + assert_eq!(&project.port, port); + assert_eq!(&project.paths, paths); + assert_eq!(&project.user, user); } - let stored_projects = db.ssh_connections().unwrap(); - assert_eq!( - stored_projects, - [ - ( - ids[0], - SerializedSshConnection { - host: "example.com".into(), - port: None, - user: None, - } - ), - ( - ids[1], - SerializedSshConnection { - host: "anotherexample.com".into(), - port: Some(123), - user: Some("user2".into()), - } - ), - ( - ids[2], - SerializedSshConnection { - host: "yetanother.com".into(), - port: Some(345), - user: None, - } - ), - ] - .into_iter() - .collect::>(), - ); + let stored_projects = db.ssh_projects().unwrap(); + assert_eq!(stored_projects.len(), projects.len()); } #[gpui::test] @@ -2698,4 +2659,56 @@ mod tests { assert_eq!(workspace.center_group, new_workspace.center_group); } + + #[gpui::test] + async fn test_update_ssh_project_paths() { + zlog::init_test(); + + let db = WorkspaceDb::open_test_db("test_update_ssh_project_paths").await; + + let (host, port, initial_paths, user) = ( + "example.com".to_string(), + Some(22_u16), + vec!["/home/user".to_string(), "/etc/nginx".to_string()], + Some("user".to_string()), + ); + + let project = db + .get_or_create_ssh_project(host.clone(), port, initial_paths.clone(), user.clone()) + .await + .unwrap(); + + assert_eq!(project.host, host); + assert_eq!(project.paths, initial_paths); + assert_eq!(project.user, user); + + let new_paths = vec![ + "/home/user".to_string(), + "/etc/nginx".to_string(), + "/var/log".to_string(), + "/opt/app".to_string(), + ]; + + let updated_project = db + .update_ssh_project_paths(project.id, new_paths.clone()) + .await + .unwrap(); + + assert_eq!(updated_project.id, project.id); + assert_eq!(updated_project.paths, new_paths); + + let retrieved_project = db + .get_ssh_project( + host.clone(), + port, + serde_json::to_string(&new_paths).unwrap(), + user.clone(), + ) + .await + .unwrap() + .unwrap(); + + assert_eq!(retrieved_project.id, project.id); + assert_eq!(retrieved_project.paths, new_paths); + } } diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index 04757d0495..15a54ac62f 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -1,48 +1,256 @@ use super::{SerializedAxis, SerializedWindowBounds}; use crate::{ Member, Pane, PaneAxis, SerializableItemRegistry, Workspace, WorkspaceId, item::ItemHandle, - path_list::PathList, }; -use anyhow::Result; +use anyhow::{Context as _, Result}; use async_recursion::async_recursion; use db::sqlez::{ bindable::{Bind, Column, StaticColumnCount}, statement::Statement, }; use gpui::{AsyncWindowContext, Entity, WeakEntity}; - +use itertools::Itertools as _; use project::{Project, debugger::breakpoint_store::SourceBreakpoint}; +use remote::ssh_session::SshProjectId; use serde::{Deserialize, Serialize}; use std::{ collections::BTreeMap, path::{Path, PathBuf}, sync::Arc, }; -use util::ResultExt; +use util::{ResultExt, paths::SanitizedPath}; use uuid::Uuid; -#[derive( - Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize, -)] -pub(crate) struct SshConnectionId(pub u64); - #[derive(Debug, Clone, PartialEq, Deserialize, Serialize)] -pub struct SerializedSshConnection { +pub struct SerializedSshProject { + pub id: SshProjectId, pub host: String, pub port: Option, + pub paths: Vec, pub user: Option, } +impl SerializedSshProject { + pub fn ssh_urls(&self) -> Vec { + self.paths + .iter() + .map(|path| { + let mut result = String::new(); + if let Some(user) = &self.user { + result.push_str(user); + result.push('@'); + } + result.push_str(&self.host); + if let Some(port) = &self.port { + result.push(':'); + result.push_str(&port.to_string()); + } + result.push_str(path); + PathBuf::from(result) + }) + .collect() + } +} + +impl StaticColumnCount for SerializedSshProject { + fn column_count() -> usize { + 5 + } +} + +impl Bind for &SerializedSshProject { + fn bind(&self, statement: &Statement, start_index: i32) -> Result { + let next_index = statement.bind(&self.id.0, start_index)?; + let next_index = statement.bind(&self.host, next_index)?; + let next_index = statement.bind(&self.port, next_index)?; + let raw_paths = serde_json::to_string(&self.paths)?; + let next_index = statement.bind(&raw_paths, next_index)?; + statement.bind(&self.user, next_index) + } +} + +impl Column for SerializedSshProject { + fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { + let id = statement.column_int64(start_index)?; + let host = statement.column_text(start_index + 1)?.to_string(); + let (port, _) = Option::::column(statement, start_index + 2)?; + let raw_paths = statement.column_text(start_index + 3)?.to_string(); + let paths: Vec = serde_json::from_str(&raw_paths)?; + + let (user, _) = Option::::column(statement, start_index + 4)?; + + Ok(( + Self { + id: SshProjectId(id as u64), + host, + port, + paths, + user, + }, + start_index + 5, + )) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct LocalPaths(Arc>); + +impl LocalPaths { + pub fn new>(paths: impl IntoIterator) -> Self { + let mut paths: Vec = paths + .into_iter() + .map(|p| SanitizedPath::from(p).into()) + .collect(); + // Ensure all future `zed workspace1 workspace2` and `zed workspace2 workspace1` calls are using the same workspace. + // The actual workspace order is stored in the `LocalPathsOrder` struct. + paths.sort(); + Self(Arc::new(paths)) + } + + pub fn paths(&self) -> &Arc> { + &self.0 + } +} + +impl StaticColumnCount for LocalPaths {} +impl Bind for &LocalPaths { + fn bind(&self, statement: &Statement, start_index: i32) -> Result { + statement.bind(&bincode::serialize(&self.0)?, start_index) + } +} + +impl Column for LocalPaths { + fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { + let path_blob = statement.column_blob(start_index)?; + let paths: Arc> = if path_blob.is_empty() { + Default::default() + } else { + bincode::deserialize(path_blob).context("Bincode deserialization of paths failed")? + }; + + Ok((Self(paths), start_index + 1)) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct LocalPathsOrder(Vec); + +impl LocalPathsOrder { + pub fn new(order: impl IntoIterator) -> Self { + Self(order.into_iter().collect()) + } + + pub fn order(&self) -> &[usize] { + self.0.as_slice() + } + + pub fn default_for_paths(paths: &LocalPaths) -> Self { + Self::new(0..paths.0.len()) + } +} + +impl StaticColumnCount for LocalPathsOrder {} +impl Bind for &LocalPathsOrder { + fn bind(&self, statement: &Statement, start_index: i32) -> Result { + statement.bind(&bincode::serialize(&self.0)?, start_index) + } +} + +impl Column for LocalPathsOrder { + fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { + let order_blob = statement.column_blob(start_index)?; + let order = if order_blob.is_empty() { + Vec::new() + } else { + bincode::deserialize(order_blob).context("deserializing workspace root order")? + }; + + Ok((Self(order), start_index + 1)) + } +} + #[derive(Debug, PartialEq, Clone)] pub enum SerializedWorkspaceLocation { - Local, - Ssh(SerializedSshConnection), + Local(LocalPaths, LocalPathsOrder), + Ssh(SerializedSshProject), } impl SerializedWorkspaceLocation { + /// Create a new `SerializedWorkspaceLocation` from a list of local paths. + /// + /// The paths will be sorted and the order will be stored in the `LocalPathsOrder` struct. + /// + /// # Examples + /// + /// ``` + /// use std::path::Path; + /// use zed_workspace::SerializedWorkspaceLocation; + /// + /// let location = SerializedWorkspaceLocation::from_local_paths(vec![ + /// Path::new("path/to/workspace1"), + /// Path::new("path/to/workspace2"), + /// ]); + /// assert_eq!(location, SerializedWorkspaceLocation::Local( + /// LocalPaths::new(vec![ + /// Path::new("path/to/workspace1"), + /// Path::new("path/to/workspace2"), + /// ]), + /// LocalPathsOrder::new(vec![0, 1]), + /// )); + /// ``` + /// + /// ``` + /// use std::path::Path; + /// use zed_workspace::SerializedWorkspaceLocation; + /// + /// let location = SerializedWorkspaceLocation::from_local_paths(vec![ + /// Path::new("path/to/workspace2"), + /// Path::new("path/to/workspace1"), + /// ]); + /// + /// assert_eq!(location, SerializedWorkspaceLocation::Local( + /// LocalPaths::new(vec![ + /// Path::new("path/to/workspace1"), + /// Path::new("path/to/workspace2"), + /// ]), + /// LocalPathsOrder::new(vec![1, 0]), + /// )); + /// ``` + pub fn from_local_paths>(paths: impl IntoIterator) -> Self { + let mut indexed_paths: Vec<_> = paths + .into_iter() + .map(|p| p.as_ref().to_path_buf()) + .enumerate() + .collect(); + + indexed_paths.sort_by(|(_, a), (_, b)| a.cmp(b)); + + let sorted_paths: Vec<_> = indexed_paths.iter().map(|(_, path)| path.clone()).collect(); + let order: Vec<_> = indexed_paths.iter().map(|(index, _)| *index).collect(); + + Self::Local(LocalPaths::new(sorted_paths), LocalPathsOrder::new(order)) + } + /// Get sorted paths pub fn sorted_paths(&self) -> Arc> { - unimplemented!() + match self { + SerializedWorkspaceLocation::Local(paths, order) => { + if order.order().is_empty() { + paths.paths().clone() + } else { + Arc::new( + order + .order() + .iter() + .zip(paths.paths().iter()) + .sorted_by_key(|(i, _)| **i) + .map(|(_, p)| p.clone()) + .collect(), + ) + } + } + SerializedWorkspaceLocation::Ssh(ssh_project) => Arc::new(ssh_project.ssh_urls()), + } } } @@ -50,7 +258,6 @@ impl SerializedWorkspaceLocation { pub(crate) struct SerializedWorkspace { pub(crate) id: WorkspaceId, pub(crate) location: SerializedWorkspaceLocation, - pub(crate) paths: PathList, pub(crate) center_group: SerializedPaneGroup, pub(crate) window_bounds: Option, pub(crate) centered_layout: bool, @@ -374,3 +581,80 @@ impl Column for SerializedItem { )) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_serialize_local_paths() { + let paths = vec!["b", "a", "c"]; + let serialized = SerializedWorkspaceLocation::from_local_paths(paths); + + assert_eq!( + serialized, + SerializedWorkspaceLocation::Local( + LocalPaths::new(vec!["a", "b", "c"]), + LocalPathsOrder::new(vec![1, 0, 2]) + ) + ); + } + + #[test] + fn test_sorted_paths() { + let paths = vec!["b", "a", "c"]; + let serialized = SerializedWorkspaceLocation::from_local_paths(paths); + assert_eq!( + serialized.sorted_paths(), + Arc::new(vec![ + PathBuf::from("b"), + PathBuf::from("a"), + PathBuf::from("c"), + ]) + ); + + let paths = Arc::new(vec![ + PathBuf::from("a"), + PathBuf::from("b"), + PathBuf::from("c"), + ]); + let order = vec![2, 0, 1]; + let serialized = + SerializedWorkspaceLocation::Local(LocalPaths(paths), LocalPathsOrder(order)); + assert_eq!( + serialized.sorted_paths(), + Arc::new(vec![ + PathBuf::from("b"), + PathBuf::from("c"), + PathBuf::from("a"), + ]) + ); + + let paths = Arc::new(vec![ + PathBuf::from("a"), + PathBuf::from("b"), + PathBuf::from("c"), + ]); + let order = vec![]; + let serialized = + SerializedWorkspaceLocation::Local(LocalPaths(paths.clone()), LocalPathsOrder(order)); + assert_eq!(serialized.sorted_paths(), paths); + + let urls = ["/a", "/b", "/c"]; + let serialized = SerializedWorkspaceLocation::Ssh(SerializedSshProject { + id: SshProjectId(0), + host: "host".to_string(), + port: Some(22), + paths: urls.iter().map(|s| s.to_string()).collect(), + user: Some("user".to_string()), + }); + assert_eq!( + serialized.sorted_paths(), + Arc::new( + urls.iter() + .map(|p| PathBuf::from(format!("user@host:22{}", p))) + .collect() + ) + ); + } +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 044601df97..499e4f4619 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1,12 +1,10 @@ pub mod dock; pub mod history_manager; -pub mod invalid_buffer_view; pub mod item; mod modal_layer; pub mod notifications; pub mod pane; pub mod pane_group; -mod path_list; mod persistence; pub mod searchable; pub mod shared_screen; @@ -19,7 +17,6 @@ mod workspace_settings; pub use crate::notifications::NotificationFrame; pub use dock::Panel; -pub use path_list::PathList; pub use toast_layer::{ToastAction, ToastLayer, ToastView}; use anyhow::{Context as _, Result, anyhow}; @@ -64,10 +61,13 @@ use notifications::{ }; pub use pane::*; pub use pane_group::*; -use persistence::{DB, SerializedWindowBounds, model::SerializedWorkspace}; +use persistence::{ + DB, SerializedWindowBounds, + model::{SerializedSshProject, SerializedWorkspace}, +}; pub use persistence::{ DB as WORKSPACE_DB, WorkspaceDb, delete_unloaded_items, - model::{ItemId, SerializedSshConnection, SerializedWorkspaceLocation}, + model::{ItemId, LocalPaths, SerializedWorkspaceLocation}, }; use postage::stream::Stream; use project::{ @@ -612,60 +612,21 @@ impl ProjectItemRegistry { ); self.build_project_item_for_path_fns .push(|project, project_path, window, cx| { - let project_path = project_path.clone(); - let is_file = project - .read(cx) - .entry_for_path(&project_path, cx) - .is_some_and(|entry| entry.is_file()); - let entry_abs_path = project.read(cx).absolute_path(&project_path, cx); - let is_local = project.read(cx).is_local(); let project_item = - ::try_open(project, &project_path, cx)?; + ::try_open(project, project_path, cx)?; let project = project.clone(); Some(window.spawn(cx, async move |cx| { - match project_item.await.with_context(|| { - format!( - "opening project path {:?}", - entry_abs_path.as_deref().unwrap_or(&project_path.path) - ) - }) { - Ok(project_item) => { - let project_item = project_item; - let project_entry_id: Option = - project_item.read_with(cx, project::ProjectItem::entry_id)?; - let build_workspace_item = Box::new( - |pane: &mut Pane, window: &mut Window, cx: &mut Context| { - Box::new(cx.new(|cx| { - T::for_project_item( - project, - Some(pane), - project_item, - window, - cx, - ) - })) as Box - }, - ) as Box<_>; - Ok((project_entry_id, build_workspace_item)) - } - Err(e) => match entry_abs_path.as_deref().filter(|_| is_file) { - Some(abs_path) => match cx.update(|window, cx| { - T::for_broken_project_item(abs_path, is_local, &e, window, cx) - })? { - Some(broken_project_item_view) => { - let build_workspace_item = Box::new( - move |_: &mut Pane, _: &mut Window, cx: &mut Context| { - cx.new(|_| broken_project_item_view).boxed_clone() - }, - ) - as Box<_>; - Ok((None, build_workspace_item)) - } - None => Err(e)?, - }, - None => Err(e)?, + let project_item = project_item.await?; + let project_entry_id: Option = + project_item.read_with(cx, project::ProjectItem::entry_id)?; + let build_workspace_item = Box::new( + |pane: &mut Pane, window: &mut Window, cx: &mut Context| { + Box::new(cx.new(|cx| { + T::for_project_item(project, Some(pane), project_item, window, cx) + })) as Box }, - } + ) as Box<_>; + Ok((project_entry_id, build_workspace_item)) })) }); } @@ -1052,7 +1013,7 @@ pub enum OpenVisible { enum WorkspaceLocation { // Valid local paths or SSH project to serialize - Location(SerializedWorkspaceLocation, PathList), + Location(SerializedWorkspaceLocation), // No valid location found hence clear session id DetachFromSession, // No valid location found to serialize @@ -1136,6 +1097,7 @@ pub struct Workspace { terminal_provider: Option>, debugger_provider: Option>, serializable_items_tx: UnboundedSender>, + serialized_ssh_project: Option, _items_serializer: Task>, session_id: Option, scheduled_tasks: Vec>, @@ -1184,6 +1146,8 @@ impl Workspace { project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded(_) => { this.update_window_title(window, cx); + this.update_ssh_paths(cx); + this.serialize_ssh_paths(window, cx); this.serialize_workspace(window, cx); // This event could be triggered by `AddFolderToProject` or `RemoveFromProject`. this.update_history(cx); @@ -1468,7 +1432,7 @@ impl Workspace { serializable_items_tx, _items_serializer, session_id: Some(session_id), - + serialized_ssh_project: None, scheduled_tasks: Vec::new(), } } @@ -1508,9 +1472,20 @@ impl Workspace { let serialized_workspace = persistence::DB.workspace_for_roots(paths_to_open.as_slice()); - if let Some(paths) = serialized_workspace.as_ref().map(|ws| &ws.paths) { - paths_to_open = paths.paths().to_vec(); - if !paths.is_lexicographically_ordered() { + let workspace_location = serialized_workspace + .as_ref() + .map(|ws| &ws.location) + .and_then(|loc| match loc { + SerializedWorkspaceLocation::Local(_, order) => { + Some((loc.sorted_paths(), order.order())) + } + _ => None, + }); + + if let Some((paths, order)) = workspace_location { + paths_to_open = paths.iter().cloned().collect(); + + if order.iter().enumerate().any(|(i, &j)| i != j) { project_handle .update(cx, |project, cx| { project.set_worktrees_reordered(true, cx); @@ -2030,6 +2005,14 @@ impl Workspace { self.debugger_provider.clone() } + pub fn serialized_ssh_project(&self) -> Option { + self.serialized_ssh_project.clone() + } + + pub fn set_serialized_ssh_project(&mut self, serialized_ssh_project: SerializedSshProject) { + self.serialized_ssh_project = Some(serialized_ssh_project); + } + pub fn prompt_for_open_path( &mut self, path_prompt_options: PathPromptOptions, @@ -2266,43 +2249,27 @@ impl Workspace { })?; if let Some(active_call) = active_call + && close_intent != CloseIntent::Quit && workspace_count == 1 && active_call.read_with(cx, |call, _| call.room().is_some())? { - if close_intent == CloseIntent::CloseWindow { - let answer = cx.update(|window, cx| { - window.prompt( - PromptLevel::Warning, - "Do you want to leave the current call?", - None, - &["Close window and hang up", "Cancel"], - cx, - ) - })?; + let answer = cx.update(|window, cx| { + window.prompt( + PromptLevel::Warning, + "Do you want to leave the current call?", + None, + &["Close window and hang up", "Cancel"], + cx, + ) + })?; - if answer.await.log_err() == Some(1) { - return anyhow::Ok(false); - } else { - active_call - .update(cx, |call, cx| call.hang_up(cx))? - .await - .log_err(); - } - } - if close_intent == CloseIntent::ReplaceWindow { - _ = active_call.update(cx, |this, cx| { - let workspace = cx - .windows() - .iter() - .filter_map(|window| window.downcast::()) - .next() - .unwrap(); - let project = workspace.read(cx)?.project.clone(); - if project.read(cx).is_shared() { - this.unshare_project(project, cx)?; - } - Ok::<_, anyhow::Error>(()) - })?; + if answer.await.log_err() == Some(1) { + return anyhow::Ok(false); + } else { + active_call + .update(cx, |call, cx| call.hang_up(cx))? + .await + .log_err(); } } @@ -3396,8 +3363,9 @@ impl Workspace { window: &mut Window, cx: &mut App, ) -> Task, WorkspaceItemBuilder)>> { + let project = self.project().clone(); let registry = cx.default_global::().clone(); - registry.open_path(self.project(), &path, window, cx) + registry.open_path(&project, &path, window, cx) } pub fn find_project_item( @@ -4022,6 +3990,52 @@ impl Workspace { maybe_pane_handle } + pub fn split_pane_with_item( + &mut self, + pane_to_split: WeakEntity, + split_direction: SplitDirection, + from: WeakEntity, + item_id_to_move: EntityId, + window: &mut Window, + cx: &mut Context, + ) { + let Some(pane_to_split) = pane_to_split.upgrade() else { + return; + }; + let Some(from) = from.upgrade() else { + return; + }; + + let new_pane = self.add_pane(window, cx); + move_item(&from, &new_pane, item_id_to_move, 0, true, window, cx); + self.center + .split(&pane_to_split, &new_pane, split_direction) + .unwrap(); + cx.notify(); + } + + pub fn split_pane_with_project_entry( + &mut self, + pane_to_split: WeakEntity, + split_direction: SplitDirection, + project_entry: ProjectEntryId, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + let pane_to_split = pane_to_split.upgrade()?; + let new_pane = self.add_pane(window, cx); + self.center + .split(&pane_to_split, &new_pane, split_direction) + .unwrap(); + + let path = self.project.read(cx).path_for_entry(project_entry, cx)?; + let task = self.open_path(path, Some(new_pane.downgrade()), true, window, cx); + Some(cx.foreground_executor().spawn(async move { + task.await?; + Ok(()) + })) + } + pub fn join_all_panes(&mut self, window: &mut Window, cx: &mut Context) { let active_item = self.active_pane.read(cx).active_item(); for pane in &self.panes { @@ -5030,12 +5044,59 @@ impl Workspace { self.session_id.clone() } - pub fn root_paths(&self, cx: &App) -> Vec> { + fn local_paths(&self, cx: &App) -> Option>> { let project = self.project().read(cx); - project - .visible_worktrees(cx) - .map(|worktree| worktree.read(cx).abs_path()) - .collect::>() + + if project.is_local() { + Some( + project + .visible_worktrees(cx) + .map(|worktree| worktree.read(cx).abs_path()) + .collect::>(), + ) + } else { + None + } + } + + fn update_ssh_paths(&mut self, cx: &App) { + let project = self.project().read(cx); + if !project.is_local() { + let paths: Vec = project + .visible_worktrees(cx) + .map(|worktree| worktree.read(cx).abs_path().to_string_lossy().to_string()) + .collect(); + if let Some(ssh_project) = &mut self.serialized_ssh_project { + ssh_project.paths = paths; + } + } + } + + fn serialize_ssh_paths(&mut self, window: &mut Window, cx: &mut Context) { + if self._schedule_serialize_ssh_paths.is_none() { + self._schedule_serialize_ssh_paths = + Some(cx.spawn_in(window, async move |this, cx| { + cx.background_executor() + .timer(SERIALIZATION_THROTTLE_TIME) + .await; + this.update_in(cx, |this, window, cx| { + let task = if let Some(ssh_project) = &this.serialized_ssh_project { + let ssh_project_id = ssh_project.id; + let ssh_project_paths = ssh_project.paths.clone(); + window.spawn(cx, async move |_| { + persistence::DB + .update_ssh_project_paths(ssh_project_id, ssh_project_paths) + .await + }) + } else { + Task::ready(Err(anyhow::anyhow!("No SSH project to serialize"))) + }; + task.detach(); + this._schedule_serialize_ssh_paths.take(); + }) + .log_err(); + })); + } } fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context) { @@ -5208,7 +5269,7 @@ impl Workspace { } match self.serialize_workspace_location(cx) { - WorkspaceLocation::Location(location, paths) => { + WorkspaceLocation::Location(location) => { let breakpoints = self.project.update(cx, |project, cx| { project .breakpoint_store() @@ -5222,7 +5283,6 @@ impl Workspace { let serialized_workspace = SerializedWorkspace { id: database_id, location, - paths, center_group, window_bounds, display: Default::default(), @@ -5248,19 +5308,13 @@ impl Workspace { } fn serialize_workspace_location(&self, cx: &App) -> WorkspaceLocation { - let paths = PathList::new(&self.root_paths(cx)); - if let Some(connection) = self.project.read(cx).ssh_connection_options(cx) { - WorkspaceLocation::Location( - SerializedWorkspaceLocation::Ssh(SerializedSshConnection { - host: connection.host, - port: connection.port, - user: connection.username, - }), - paths, - ) - } else if self.project.read(cx).is_local() { - if !paths.is_empty() { - WorkspaceLocation::Location(SerializedWorkspaceLocation::Local, paths) + if let Some(ssh_project) = &self.serialized_ssh_project { + WorkspaceLocation::Location(SerializedWorkspaceLocation::Ssh(ssh_project.clone())) + } else if let Some(local_paths) = self.local_paths(cx) { + if !local_paths.is_empty() { + WorkspaceLocation::Location(SerializedWorkspaceLocation::from_local_paths( + local_paths, + )) } else { WorkspaceLocation::DetachFromSession } @@ -5273,13 +5327,13 @@ impl Workspace { let Some(id) = self.database_id() else { return; }; - if !self.project.read(cx).is_local() { - return; - } + let location = match self.serialize_workspace_location(cx) { + WorkspaceLocation::Location(location) => location, + _ => return, + }; if let Some(manager) = HistoryManager::global(cx) { - let paths = PathList::new(&self.root_paths(cx)); manager.update(cx, |this, cx| { - this.update_history(id, HistoryManagerEntry::new(id, &paths), cx); + this.update_history(id, HistoryManagerEntry::new(id, &location), cx); }); } } @@ -6587,29 +6641,15 @@ impl Render for Workspace { } }) .children(self.zoomed.as_ref().and_then(|view| { - let zoomed_view = view.upgrade()?; - let div = div() + Some(div() .occlude() .absolute() .overflow_hidden() .border_color(colors.border) .bg(colors.background) - .child(zoomed_view) + .child(view.upgrade()?) .inset_0() - .shadow_lg(); - - if !WorkspaceSettings::get_global(cx).zoomed_padding { - return Some(div); - } - - Some(match self.zoomed_position { - Some(DockPosition::Left) => div.right_2().border_r_1(), - Some(DockPosition::Right) => div.left_2().border_l_1(), - Some(DockPosition::Bottom) => div.top_2().border_t_1(), - None => { - div.top_2().bottom_2().left_2().right_2().border_1() - } - }) + .shadow_lg()) })) .children(self.render_notifications(window, cx)), ) @@ -6759,14 +6799,14 @@ impl WorkspaceHandle for Entity { } } -pub async fn last_opened_workspace_location() -> Option<(SerializedWorkspaceLocation, PathList)> { +pub async fn last_opened_workspace_location() -> Option { DB.last_workspace().await.log_err().flatten() } pub fn last_session_workspace_locations( last_session_id: &str, last_session_window_stack: Option>, -) -> Option> { +) -> Option> { DB.last_session_workspace_locations(last_session_id, last_session_window_stack) .log_err() } @@ -7269,7 +7309,7 @@ pub fn open_ssh_project_with_new_connection( cx: &mut App, ) -> Task> { cx.spawn(async move |cx| { - let (workspace_id, serialized_workspace) = + let (serialized_ssh_project, workspace_id, serialized_workspace) = serialize_ssh_project(connection_options.clone(), paths.clone(), cx).await?; let session = match cx @@ -7303,6 +7343,7 @@ pub fn open_ssh_project_with_new_connection( open_ssh_project_inner( project, paths, + serialized_ssh_project, workspace_id, serialized_workspace, app_state, @@ -7322,12 +7363,13 @@ pub fn open_ssh_project_with_existing_connection( cx: &mut AsyncApp, ) -> Task> { cx.spawn(async move |cx| { - let (workspace_id, serialized_workspace) = + let (serialized_ssh_project, workspace_id, serialized_workspace) = serialize_ssh_project(connection_options.clone(), paths.clone(), cx).await?; open_ssh_project_inner( project, paths, + serialized_ssh_project, workspace_id, serialized_workspace, app_state, @@ -7341,6 +7383,7 @@ pub fn open_ssh_project_with_existing_connection( async fn open_ssh_project_inner( project: Entity, paths: Vec, + serialized_ssh_project: SerializedSshProject, workspace_id: WorkspaceId, serialized_workspace: Option, app_state: Arc, @@ -7393,6 +7436,7 @@ async fn open_ssh_project_inner( let mut workspace = Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx); + workspace.set_serialized_ssh_project(serialized_ssh_project); workspace.update_history(cx); if let Some(ref serialized) = serialized_workspace { @@ -7429,18 +7473,28 @@ fn serialize_ssh_project( connection_options: SshConnectionOptions, paths: Vec, cx: &AsyncApp, -) -> Task)>> { +) -> Task< + Result<( + SerializedSshProject, + WorkspaceId, + Option, + )>, +> { cx.background_spawn(async move { - let ssh_connection_id = persistence::DB - .get_or_create_ssh_connection( + let serialized_ssh_project = persistence::DB + .get_or_create_ssh_project( connection_options.host.clone(), connection_options.port, + paths + .iter() + .map(|path| path.to_string_lossy().to_string()) + .collect::>(), connection_options.username.clone(), ) .await?; let serialized_workspace = - persistence::DB.ssh_workspace_for_roots(&paths, ssh_connection_id); + persistence::DB.workspace_for_ssh_project(&serialized_ssh_project); let workspace_id = if let Some(workspace_id) = serialized_workspace.as_ref().map(|workspace| workspace.id) @@ -7450,7 +7504,7 @@ fn serialize_ssh_project( persistence::DB.next_id().await? }; - Ok((workspace_id, serialized_workspace)) + Ok((serialized_ssh_project, workspace_id, serialized_workspace)) }) } @@ -7997,15 +8051,18 @@ pub fn ssh_workspace_position_from_db( paths_to_open: &[PathBuf], cx: &App, ) -> Task> { - let paths = paths_to_open.to_vec(); + let paths = paths_to_open + .iter() + .map(|path| path.to_string_lossy().to_string()) + .collect::>(); cx.background_spawn(async move { - let ssh_connection_id = persistence::DB - .get_or_create_ssh_connection(host, port, user) + let serialized_ssh_project = persistence::DB + .get_or_create_ssh_project(host, port, paths, user) .await .context("fetching serialized ssh project")?; let serialized_workspace = - persistence::DB.ssh_workspace_for_roots(&paths, ssh_connection_id); + persistence::DB.workspace_for_ssh_project(&serialized_ssh_project); let (window_bounds, display) = if let Some(bounds) = window_bounds_env_override() { (Some(WindowBounds::Windowed(bounds)), None) diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 3b6bc1ea97..5635347514 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -29,7 +29,6 @@ pub struct WorkspaceSettings { pub on_last_window_closed: OnLastWindowClosed, pub resize_all_panels_in_dock: Vec, pub close_on_file_delete: bool, - pub zoomed_padding: bool, } #[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema)] @@ -203,12 +202,6 @@ pub struct WorkspaceSettingsContent { /// /// Default: false pub close_on_file_delete: Option, - /// Whether to show padding for zoomed panels. - /// When enabled, zoomed bottom panels will have some top padding, - /// while zoomed left/right panels will have padding to the right/left (respectively). - /// - /// Default: true - pub zoomed_padding: Option, } #[derive(Deserialize)] diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 6f4ead9ebb..508dd3861d 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -2,7 +2,7 @@ description = "The fast, collaborative code editor." edition.workspace = true name = "zed" -version = "0.202.0" +version = "0.201.4" publish.workspace = true license = "GPL-3.0-or-later" authors = ["Zed Team "] @@ -34,7 +34,6 @@ audio.workspace = true auto_update.workspace = true auto_update_ui.workspace = true backtrace = "0.3" -bincode.workspace = true breadcrumbs.workspace = true call.workspace = true channel.workspace = true @@ -62,7 +61,6 @@ extensions_ui.workspace = true feature_flags.workspace = true feedback.workspace = true file_finder.workspace = true -system_specs.workspace = true fs.workspace = true futures.workspace = true git.workspace = true diff --git a/crates/zed/RELEASE_CHANNEL b/crates/zed/RELEASE_CHANNEL index 38f8e886e1..4de2f126df 100644 --- a/crates/zed/RELEASE_CHANNEL +++ b/crates/zed/RELEASE_CHANNEL @@ -1 +1 @@ -dev +preview \ No newline at end of file diff --git a/crates/zed/resources/info/SupportedPlatforms.plist b/crates/zed/resources/info/SupportedPlatforms.plist deleted file mode 100644 index fd2a4101d8..0000000000 --- a/crates/zed/resources/info/SupportedPlatforms.plist +++ /dev/null @@ -1,4 +0,0 @@ -CFBundleSupportedPlatforms - - MacOSX - diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index e99c8b564b..7c81d99a7c 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -16,7 +16,7 @@ use extension_host::ExtensionStore; use fs::{Fs, RealFs}; use futures::{StreamExt, channel::oneshot, future}; use git::GitHostingProviderRegistry; -use gpui::{App, AppContext, Application, AsyncApp, Focusable as _, UpdateGlobal as _}; +use gpui::{App, AppContext as _, Application, AsyncApp, Focusable as _, UpdateGlobal as _}; use gpui_tokio::Tokio; use http_client::{Url, read_proxy_from_env}; @@ -47,8 +47,8 @@ use theme::{ use util::{ResultExt, TryFutureExt, maybe}; use uuid::Uuid; use workspace::{ - AppState, PathList, SerializedWorkspaceLocation, Toast, Workspace, WorkspaceSettings, - WorkspaceStore, notifications::NotificationId, + AppState, SerializedWorkspaceLocation, Toast, Workspace, WorkspaceSettings, WorkspaceStore, + notifications::NotificationId, }; use zed::{ OpenListener, OpenRequest, RawOpenRequest, app_menus, build_window_options, @@ -240,7 +240,7 @@ pub fn main() { option_env!("ZED_COMMIT_SHA").map(|commit_sha| AppCommitSha::new(commit_sha.to_string())); if args.system_specs { - let system_specs = system_specs::SystemSpecs::new_stateless( + let system_specs = feedback::system_specs::SystemSpecs::new_stateless( app_version, app_commit_sha, *release_channel::RELEASE_CHANNEL, @@ -599,7 +599,7 @@ pub fn main() { repl::notebook::init(cx); diagnostics::init(cx); - audio::init(cx); + audio::init(Assets, cx); workspace::init(app_state.clone(), cx); ui_prompt::init(cx); @@ -949,14 +949,15 @@ async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp if let Some(locations) = restorable_workspace_locations(cx, &app_state).await { let mut tasks = Vec::new(); - for (location, paths) in locations { + for location in locations { match location { - SerializedWorkspaceLocation::Local => { + SerializedWorkspaceLocation::Local(location, _) => { let app_state = app_state.clone(); + let paths = location.paths().to_vec(); let task = cx.spawn(async move |cx| { let open_task = cx.update(|cx| { workspace::open_paths( - &paths.paths(), + &paths, app_state, workspace::OpenOptions::default(), cx, @@ -978,7 +979,7 @@ async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp match connection_options { Ok(connection_options) => recent_projects::open_ssh_project( connection_options, - paths.paths().into_iter().map(PathBuf::from).collect(), + ssh.paths.into_iter().map(PathBuf::from).collect(), app_state, workspace::OpenOptions::default(), cx, @@ -1069,7 +1070,7 @@ async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp pub(crate) async fn restorable_workspace_locations( cx: &mut AsyncApp, app_state: &Arc, -) -> Option> { +) -> Option> { let mut restore_behavior = cx .update(|cx| WorkspaceSettings::get(None, cx).restore_on_startup) .ok()?; diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs index ac06f1fd9f..646a3af5bb 100644 --- a/crates/zed/src/reliability.rs +++ b/crates/zed/src/reliability.rs @@ -60,9 +60,7 @@ pub fn init_panic_hook( .or_else(|| info.payload().downcast_ref::().cloned()) .unwrap_or_else(|| "Box".to_string()); - if *release_channel::RELEASE_CHANNEL != ReleaseChannel::Dev { - crashes::handle_panic(payload.clone(), info.location()); - } + crashes::handle_panic(payload.clone(), info.location()); let thread = thread::current(); let thread_name = thread.name().unwrap_or(""); @@ -89,9 +87,7 @@ pub fn init_panic_hook( }, backtrace, ); - if MINIDUMP_ENDPOINT.is_none() { - std::process::exit(-1); - } + std::process::exit(-1); } let main_module_base_address = get_main_module_base_address(); @@ -150,9 +146,7 @@ pub fn init_panic_hook( } zlog::flush(); - if (!is_pty || MINIDUMP_ENDPOINT.is_some()) - && let Some(panic_data_json) = serde_json::to_string(&panic_data).log_err() - { + if !is_pty && let Some(panic_data_json) = serde_json::to_string(&panic_data).log_err() { let timestamp = chrono::Utc::now().format("%Y_%m_%d %H_%M_%S").to_string(); let panic_file_path = paths::logs_dir().join(format!("zed-{timestamp}.panic")); let panic_file = fs::OpenOptions::new() @@ -618,9 +612,10 @@ async fn upload_minidump( let mut panic_message = "".to_owned(); if let Some(panic_info) = metadata.panic.as_ref() { panic_message = panic_info.message.clone(); - form = form - .text("sentry[logentry][formatted]", panic_info.message.clone()) - .text("span", panic_info.span.clone()); + form = form.text("sentry[logentry][formatted]", panic_info.message.clone()); + form = form.text("span", panic_info.span.clone()); + // TODO: add gpu-context, feature-flag-context, and more of device-context like gpu + // name, screen resolution, available ram, device model, etc } if let Some(minidump_error) = metadata.minidump_error.clone() { form = form.text("minidump_error", minidump_error); @@ -636,63 +631,6 @@ async fn upload_minidump( commit_sha = metadata.init.commit_sha.clone(), ); - let gpu_count = metadata.gpus.len(); - for (index, gpu) in metadata.gpus.iter().cloned().enumerate() { - let system_specs::GpuInfo { - device_name, - device_pci_id, - vendor_name, - vendor_pci_id, - driver_version, - driver_name, - } = gpu; - let num = if gpu_count == 1 && metadata.active_gpu.is_none() { - String::new() - } else { - index.to_string() - }; - let name = format!("gpu{num}"); - let root = format!("sentry[contexts][{name}]"); - form = form - .text( - format!("{root}[Description]"), - "A GPU found on the users system. May or may not be the GPU Zed is running on", - ) - .text(format!("{root}[type]"), "gpu") - .text(format!("{root}[name]"), device_name.unwrap_or(name)) - .text(format!("{root}[id]"), format!("{:#06x}", device_pci_id)) - .text( - format!("{root}[vendor_id]"), - format!("{:#06x}", vendor_pci_id), - ) - .text_if_some(format!("{root}[vendor_name]"), vendor_name) - .text_if_some(format!("{root}[driver_version]"), driver_version) - .text_if_some(format!("{root}[driver_name]"), driver_name); - } - if let Some(active_gpu) = metadata.active_gpu.clone() { - form = form - .text( - "sentry[contexts][Active_GPU][Description]", - "The GPU Zed is running on", - ) - .text("sentry[contexts][Active_GPU][type]", "gpu") - .text("sentry[contexts][Active_GPU][name]", active_gpu.device_name) - .text( - "sentry[contexts][Active_GPU][driver_version]", - active_gpu.driver_info, - ) - .text( - "sentry[contexts][Active_GPU][driver_name]", - active_gpu.driver_name, - ) - .text( - "sentry[contexts][Active_GPU][is_software_emulated]", - active_gpu.is_software_emulated.to_string(), - ); - } - - // TODO: feature-flag-context, and more of device-context like screen resolution, available ram, device model, etc - let mut response_text = String::new(); let mut response = http.send_multipart_form(endpoint, form).await?; response @@ -706,27 +644,6 @@ async fn upload_minidump( Ok(()) } -trait FormExt { - fn text_if_some( - self, - label: impl Into>, - value: Option>>, - ) -> Self; -} - -impl FormExt for Form { - fn text_if_some( - self, - label: impl Into>, - value: Option>>, - ) -> Self { - match value { - Some(value) => self.text(label.into(), value.into()), - None => self, - } - } -} - async fn upload_panic( http: &Arc, panic_report_url: &Url, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 553444ebdb..958149825a 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -344,17 +344,7 @@ pub fn initialize_workspace( if let Some(specs) = window.gpu_specs() { log::info!("Using GPU: {:?}", specs); - show_software_emulation_warning_if_needed(specs.clone(), window, cx); - if let Some((crash_server, message)) = crashes::CRASH_HANDLER - .get() - .zip(bincode::serialize(&specs).ok()) - && let Err(err) = crash_server.send_message(3, message) - { - log::warn!( - "Failed to store active gpu info for crash reporting: {}", - err - ); - } + show_software_emulation_warning_if_needed(specs, window, cx); } let edit_prediction_menu_handle = PopoverMenuHandle::default(); @@ -1308,11 +1298,11 @@ pub fn handle_keymap_file_changes( }) .detach(); - let mut current_layout_id = cx.keyboard_layout().id().to_string(); + let mut current_mapping = settings::get_key_equivalents(cx.keyboard_layout().id()); cx.on_keyboard_layout_change(move |cx| { - let next_layout_id = cx.keyboard_layout().id(); - if next_layout_id != current_layout_id { - current_layout_id = next_layout_id.to_string(); + let next_mapping = settings::get_key_equivalents(cx.keyboard_layout().id()); + if next_mapping != current_mapping { + current_mapping = next_mapping; keyboard_layout_tx.unbounded_send(()).ok(); } }) @@ -4624,7 +4614,7 @@ mod tests { gpui_tokio::init(cx); vim_mode_setting::init(cx); theme::init(theme::LoadThemes::JustBase, cx); - audio::init(cx); + audio::init((), cx); channel::init(&app_state.client, app_state.user_store.clone(), cx); call::init(app_state.client.clone(), app_state.user_store.clone(), cx); notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx); @@ -4729,7 +4719,7 @@ mod tests { // and key strokes contain the given key bindings .into_iter() - .any(|binding| binding.keystrokes().iter().any(|k| k.display_key == key)), + .any(|binding| binding.keystrokes().iter().any(|k| k.key == key)), "On {} Failed to find {} with key binding {}", line, action.name(), diff --git a/crates/zed/src/zed/component_preview/persistence.rs b/crates/zed/src/zed/component_preview/persistence.rs index c37a4cc389..780f7f7626 100644 --- a/crates/zed/src/zed/component_preview/persistence.rs +++ b/crates/zed/src/zed/component_preview/persistence.rs @@ -1,17 +1,10 @@ use anyhow::Result; -use db::{ - query, - sqlez::{domain::Domain, statement::Statement, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, -}; +use db::{define_connection, query, sqlez::statement::Statement, sqlez_macros::sql}; use workspace::{ItemId, WorkspaceDb, WorkspaceId}; -pub struct ComponentPreviewDb(ThreadSafeConnection); - -impl Domain for ComponentPreviewDb { - const NAME: &str = stringify!(ComponentPreviewDb); - - const MIGRATIONS: &[&str] = &[sql!( +define_connection! { + pub static ref COMPONENT_PREVIEW_DB: ComponentPreviewDb = + &[sql!( CREATE TABLE component_previews ( workspace_id INTEGER, item_id INTEGER UNIQUE, @@ -20,11 +13,9 @@ impl Domain for ComponentPreviewDb { FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE ) STRICT; - )]; + )]; } -db::static_connection!(COMPONENT_PREVIEW_DB, ComponentPreviewDb, [WorkspaceDb]); - impl ComponentPreviewDb { pub async fn save_active_page( &self, diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 2194fb7af5..827c7754fa 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -26,7 +26,6 @@ use std::thread; use std::time::Duration; use util::ResultExt; use util::paths::PathWithPosition; -use workspace::PathList; use workspace::item::ItemHandle; use workspace::{AppState, OpenOptions, SerializedWorkspaceLocation, Workspace}; @@ -362,14 +361,12 @@ async fn open_workspaces( if open_new_workspace == Some(true) { Vec::new() } else { - restorable_workspace_locations(cx, &app_state) - .await - .unwrap_or_default() + let locations = restorable_workspace_locations(cx, &app_state).await; + locations.unwrap_or_default() } } else { - vec![( - SerializedWorkspaceLocation::Local, - PathList::new(&paths.into_iter().map(PathBuf::from).collect::>()), + vec![SerializedWorkspaceLocation::from_local_paths( + paths.into_iter().map(PathBuf::from), )] }; @@ -397,9 +394,9 @@ async fn open_workspaces( // If there are paths to open, open a workspace for each grouping of paths let mut errored = false; - for (location, workspace_paths) in grouped_locations { + for location in grouped_locations { match location { - SerializedWorkspaceLocation::Local => { + SerializedWorkspaceLocation::Local(workspace_paths, _) => { let workspace_paths = workspace_paths .paths() .iter() @@ -432,7 +429,7 @@ async fn open_workspaces( cx.spawn(async move |cx| { open_ssh_project( connection_options, - workspace_paths.paths().to_vec(), + ssh.paths.into_iter().map(PathBuf::from).collect(), app_state, OpenOptions::default(), cx, diff --git a/crates/zed/src/zed/quick_action_bar/preview.rs b/crates/zed/src/zed/quick_action_bar/preview.rs index fb5a75f78d..3772104f39 100644 --- a/crates/zed/src/zed/quick_action_bar/preview.rs +++ b/crates/zed/src/zed/quick_action_bar/preview.rs @@ -72,10 +72,7 @@ impl QuickActionBar { Tooltip::with_meta( tooltip_text, Some(open_action_for_tooltip), - format!( - "{} to open in a split", - text_for_keystroke(&alt_click.modifiers, &alt_click.key, cx) - ), + format!("{} to open in a split", text_for_keystroke(&alt_click, cx)), window, cx, ) diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 8f4c42ca49..74f0a9779f 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -156,10 +156,7 @@ pub mod workspace { #[action(deprecated_aliases = ["editor::CopyPath", "outline_panel::CopyPath", "project_panel::CopyPath"])] CopyPath, #[action(deprecated_aliases = ["editor::CopyRelativePath", "outline_panel::CopyRelativePath", "project_panel::CopyRelativePath"])] - CopyRelativePath, - /// Opens the selected file with the system's default application. - #[action(deprecated_aliases = ["project_panel::OpenWithSystem"])] - OpenWithSystem, + CopyRelativePath ] ); } diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 251cad6234..c7af36f431 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -16,7 +16,6 @@ - [Configuring Zed](./configuring-zed.md) - [Configuring Languages](./configuring-languages.md) - [Key bindings](./key-bindings.md) - - [All Actions](./all-actions.md) - [Snippets](./snippets.md) - [Themes](./themes.md) - [Icon Themes](./icon-themes.md) diff --git a/docs/src/all-actions.md b/docs/src/all-actions.md deleted file mode 100644 index d20f7cfd63..0000000000 --- a/docs/src/all-actions.md +++ /dev/null @@ -1,3 +0,0 @@ -## All Actions - -{#ACTIONS_TABLE#} diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index a8a4689689..39d172ea5f 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -2425,7 +2425,6 @@ Examples: { "completions": { "words": "fallback", - "words_min_length": 3, "lsp": true, "lsp_fetch_timeout_ms": 0, "lsp_insert_mode": "replace_suffix" @@ -2445,17 +2444,6 @@ Examples: 2. `fallback` - Only if LSP response errors or times out, use document's words to show completions 3. `disabled` - Never fetch or complete document's words for completions (word-based completions can still be queried via a separate action) -### Min Words Query Length - -- Description: Minimum number of characters required to automatically trigger word-based completions. - Before that value, it's still possible to trigger the words-based completion manually with the corresponding editor command. -- Setting: `words_min_length` -- Default: `3` - -**Options** - -Positive integer values - ### LSP - Description: Whether to fetch LSP completions or not. @@ -3243,11 +3231,9 @@ Run the `theme selector: toggle` action in the command palette to see a current "indent_size": 20, "auto_reveal_entries": true, "auto_fold_dirs": true, - "drag_and_drop": true, "scrollbar": { "show": null }, - "sticky_scroll": true, "show_diagnostics": "all", "indent_guides": { "show": "always" diff --git a/docs/src/diagnostics.md b/docs/src/diagnostics.md index 9603c8197c..a015fbebf8 100644 --- a/docs/src/diagnostics.md +++ b/docs/src/diagnostics.md @@ -51,7 +51,7 @@ To configure, use ```json5 "project_panel": { - "show_diagnostics": "all", + "diagnostics": "all", } ``` diff --git a/docs/src/languages/ruby.md b/docs/src/languages/ruby.md index ef4b026db1..6f530433bd 100644 --- a/docs/src/languages/ruby.md +++ b/docs/src/languages/ruby.md @@ -299,7 +299,6 @@ To run tests in your Ruby project, you can set up custom tasks in your local `.z "-n", "\"$ZED_CUSTOM_RUBY_TEST_NAME\"" ], - "cwd": "$ZED_WORKTREE_ROOT", "tags": ["ruby-test"] } ] @@ -322,7 +321,6 @@ Plain minitest does not support running tests by line number, only by name, so w "-n", "\"$ZED_CUSTOM_RUBY_TEST_NAME\"" ], - "cwd": "$ZED_WORKTREE_ROOT", "tags": ["ruby-test"] } ] @@ -336,7 +334,6 @@ Plain minitest does not support running tests by line number, only by name, so w "label": "test $ZED_RELATIVE_FILE:$ZED_ROW", "command": "bundle", "args": ["exec", "rspec", "\"$ZED_RELATIVE_FILE:$ZED_ROW\""], - "cwd": "$ZED_WORKTREE_ROOT", "tags": ["ruby-test"] } ] @@ -372,7 +369,7 @@ The Ruby extension provides a debug adapter for debugging Ruby code. Zed's name "label": "Debug Rails server", "adapter": "rdbg", "request": "launch", - "command": "./bin/rails", + "command": "$ZED_WORKTREE_ROOT/bin/rails", "args": ["server"], "cwd": "$ZED_WORKTREE_ROOT", "env": { diff --git a/docs/src/languages/rust.md b/docs/src/languages/rust.md index 0bfa3ecac7..7695280275 100644 --- a/docs/src/languages/rust.md +++ b/docs/src/languages/rust.md @@ -136,7 +136,22 @@ This is enabled by default and can be configured as ## Manual Cargo Diagnostics fetch By default, rust-analyzer has `checkOnSave: true` enabled, which causes every buffer save to trigger a `cargo check --workspace --all-targets` command. -If disabled with `checkOnSave: false` (see the example of the server configuration json above), it's still possible to fetch the diagnostics manually, with the `editor: run/clear/cancel flycheck` commands in Rust files to refresh cargo diagnostics; the project diagnostics editor will also refresh cargo diagnostics with `editor: run flycheck` command when the setting is enabled. +For lager projects this might introduce excessive wait times, so a more fine-grained triggering could be enabled by altering the + +```json +"diagnostics": { + "cargo": { + // When enabled, Zed disables rust-analyzer's check on save and starts to query + // Cargo diagnostics separately. + "fetch_cargo_diagnostics": false + } +} +``` + +default settings. + +This will stop rust-analyzer from running `cargo check ...` on save, yet still allow to run +`editor: run/clear/cancel flycheck` commands in Rust files to refresh cargo diagnostics; the project diagnostics editor will also refresh cargo diagnostics with `editor: run flycheck` command when the setting is enabled. ## More server configuration diff --git a/docs/src/tasks.md b/docs/src/tasks.md index bff3eac860..9550563432 100644 --- a/docs/src/tasks.md +++ b/docs/src/tasks.md @@ -45,9 +45,9 @@ Zed supports ways to spawn (and rerun) commands using its integrated terminal to // Whether to show the task line in the output of the spawned task, defaults to `true`. "show_summary": true, // Whether to show the command line in the output of the spawned task, defaults to `true`. - "show_output": true + "show_output": true, // Represents the tags for inline runnable indicators, or spawning multiple tasks at once. - // "tags": [] + "tags": [] } ] ``` diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index 4fc5a9ba88..3ad1e381d9 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -430,8 +430,6 @@ Project panel can be shown/hidden with {#action project_panel::ToggleFocus} ({#k "indent_size": 20, // Pixels for each successive indent "auto_reveal_entries": true, // Show file in panel when activating its buffer "auto_fold_dirs": true, // Fold dirs with single subdir - "sticky_scroll": true, // Stick parent directories at top of the project panel. - "drag_and_drop": true, // Whether drag and drop is enabled "scrollbar": { // Project panel scrollbar settings "show": null // Show/hide: (auto, system, always, never) },